From 7d8ca9e1d62d9a55ec638deaa64e1675c24af742 Mon Sep 17 00:00:00 2001 From: thiemok Date: Mon, 15 Aug 2016 21:20:01 +0200 Subject: [PATCH 1/2] Reimplemented discord bot using python --- config.py.example | 6 +++ messages.py | 43 +++++++++++++++ notifications.py | 132 ++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + watchlist.json | 3 ++ web.py | 8 +-- worker.py | 28 ++++------ 7 files changed, 195 insertions(+), 26 deletions(-) create mode 100644 messages.py create mode 100644 notifications.py create mode 100644 watchlist.json diff --git a/config.py.example b/config.py.example index 8d1ebd80ce..e7c22fc8a2 100644 --- a/config.py.example +++ b/config.py.example @@ -29,3 +29,9 @@ REPORT_SINCE = datetime(2016, 7, 29) GOOGLE_MAPS_KEY = 's3cr3t' MAP_PROVIDER_URL = '//{s}.tile.osm.org/{z}/{x}/{y}.png' MAP_PROVIDER_ATTRIBUTION = '© OpenStreetMap contributors' + +#Discord stuff required if using discord notifications +DISCORD_APP_ID = 'secret' +DISCORD_TOKEN = 'secret' +DISCORD_CHANNELS = ['id',] #List of channel id's the bot should post to +DISCORD_UPDATE_INTERVAL = 2 #Time in minutes between checks for new sightings diff --git a/messages.py b/messages.py new file mode 100644 index 0000000000..335dec4f8a --- /dev/null +++ b/messages.py @@ -0,0 +1,43 @@ +import config + +EN_MESSAGES = { + 'ready':['[INFO] To add me visit this url: {}', '[INFO] READY to begin!', '[INFO] You are connected to {} servers!'], + 'ready_msg': 'Looking for new Pokemon!', + 'error': 'Caught error: {}', + 'shutdown': 'Let me logout first...', + 'sighting': '**{}** ({}) sighted!\n{}\nDisappearing in {:.0f} minutes.', + 'bot_desc': 'Reports sightings of rare Pokemon', + 'cmd_read_desc': 'Lists all tracked Pokemon', + 'cmd_read_msg': 'These are the tracked Pokemon:\n', + 'cmd_add_desc': 'Adds Pokemon to the watchlist', + 'cmd_add_added': 'Tracking {} now :rainbow:', + 'cmd_add_already_added': '{} is already on the watchlist :warning:', + 'cmd_add_usage': 'Usage: "!add pokemon_id" :point_up:', + 'cmd_remove_desc': 'Removes Pokemon from the Watchlist', + 'cmd_remove_removed': 'Stopped tracking {} :rainbow:', + 'cmd_remove_no_on_list' : '{} Is not being tracked :warning:', + 'cmd_remove_usage': 'Usage: "!remove pokemon_id" :point_up:', +} + +DE_MESSAGES = { + 'ready':['[INFO] Um mich hinzuzufügen besuche diese url: {}', '[INFO] BEREIT!', '[INFO] Du bist mit {} servern verbunden!'], + 'ready_msg': 'Auf der Suche nach neuen Pokemon!', + 'error': 'Fehler gefunden: {}', + 'shutdown': 'Logge aus...', + 'sighting': '**{}** ({}) gesichtet!\n{}\nVerschwindet in {:.0f} Minuten.', + 'bot_desc': 'Berichtet über Sichtungen seltener Pokemon', + 'cmd_read_desc': 'Listet alle beobachteten Pokemon auf', + 'cmd_read_msg': 'Dies sind die beobachteten Pokemon:\n', + 'cmd_add_desc': 'Fügt ein Pokemon zu den beobachteten Pokemon hinzu', + 'cmd_add_added': 'Beobachte nun {} :rainbow:', + 'cmd_add_already_added': '{} wird bereits beobachtet :warning:', + 'cmd_add_usage': 'Verwendung: "!add pokemon_id" :point_up:', + 'cmd_remove_desc': 'Entferne ein Pokemon von den beobachteten Pokemon', + 'cmd_remove_removed': 'Beobachte {} nichtmehr :rainbow:', + 'cmd_remove_no_on_list' : '{} Wird nicht beobachtet :warning:', + 'cmd_remove_usage': 'Verwendung: "!remove pokemon_id" :point_up:', +} + +DISCORD_MESSAGES = { + 'DE': DE_MESSAGES, +}.get(config.LANGUAGE.upper(), EN_MESSAGES) \ No newline at end of file diff --git a/notifications.py b/notifications.py new file mode 100644 index 0000000000..f0b995f577 --- /dev/null +++ b/notifications.py @@ -0,0 +1,132 @@ +import discord +from discord.ext import commands +import asyncio +import signal +import sys +import time +from geopy.geocoders import GoogleV3 +import json + +import config +import db +from names import POKEMON_NAMES +from messages import DISCORD_MESSAGES + +# Check whether config has all necessary attributes +REQUIRED_SETTINGS = ( + 'DISCORD_APP_ID', + 'DISCORD_TOKEN', + 'DISCORD_CHANNELS', + 'DISCORD_UPDATE_INTERVAL', + 'GOOGLE_MAPS_KEY', +) +for setting_name in REQUIRED_SETTINGS: + if not hasattr(config, setting_name): + raise RuntimeError('Please set "{}" in config'.format(setting_name)) + +join_url = "https://discordapp.com/oauth2/authorize?client_id=" + config.DISCORD_APP_ID + "&scope=bot&permissions=" + +client = commands.Bot(command_prefix='!', description=DISCORD_MESSAGES['bot_desc']) +already_seen = {} +geolocator = GoogleV3(api_key=config.GOOGLE_MAPS_KEY) + +watchlist_path = 'watchlist.json' +with open(watchlist_path) as json_data: + watchlist = json.load(json_data)['watchlist'] + +@client.event +async def on_ready(): + print(DISCORD_MESSAGES['ready'][0].format(join_url)) + print(DISCORD_MESSAGES['ready'][1]) + print(DISCORD_MESSAGES['ready'][2].format(count_iterable(client.servers))) + client.loop.create_task(check_pokemon()) + for id in config.DISCORD_CHANNELS: + channel = client.get_channel(id) + await client.send_message(channel, DISCORD_MESSAGES['ready_msg']) + + +@client.event +async def on_error(event, *args, **kwargs): + print(DISCORD_MESSAGES['error'].format(event)) + +@client.command(description=DISCORD_MESSAGES['cmd_read_desc'], help=DISCORD_MESSAGES['cmd_read_desc']) +async def read(): #Displays all tracked pokemon + tracked_pokemon = "```\n" + for id in watchlist: + tracked_pokemon += '{} {}\n'.format(id, POKEMON_NAMES[id]) + tracked_pokemon += "```" + await client.say(DISCORD_MESSAGES['cmd_read_msg'] + tracked_pokemon) + +@client.command(description=DISCORD_MESSAGES['cmd_add_desc'], help=DISCORD_MESSAGES['cmd_add_desc']) +async def add(id: int): + if id not in watchlist: + watchlist.append(id) + with open(watchlist_path, 'w') as json_data: + json.dump({'watchlist': watchlist}, json_data) + await client.say(DISCORD_MESSAGES['cmd_add_added'].format(POKEMON_NAMES[id])) + else: + await client.say(DISCORD_MESSAGES['cmd_add_already_added'].format(POKEMON_NAMES[id])) + +@add.error +async def add_error(error, id): + await client.say(DISCORD_MESSAGES['cmd_add_usage']) + +@client.command(description=DISCORD_MESSAGES['cmd_remove_desc'], help=DISCORD_MESSAGES['cmd_remove_desc']) +async def remove(id: int): + if id is None: + await client.say(DISCORD_MESSAGES['cmd_remove_usage']) + elif id in watchlist: + watchlist.remove(id) + with open(watchlist_path, 'w') as json_data: + json.dump({'watchlist': watchlist}, json_data) + await client.say(DISCORD_MESSAGES['cmd_remove_removed'].format(POKEMON_NAMES[id])) + else: + await client.say(DISCORD_MESSAGES['cmd_remove_no_on_list'].format(POKEMON_NAMES[id])) + +@remove.error +async def remove_error(error, id): + await client.say(DISCORD_MESSAGES['cmd_remove_usage']) + +async def check_pokemon(): + session = db.Session() + pokemons = db.get_sightings(session) + session.close() + + for sighting in pokemons: + await process_sighting(sighting) + + remove_stale_sightings() + +async def process_sighting(sighting): + if sighting.pokemon_id in watchlist: + if sighting.id not in already_seen: + already_seen[sighting.id] = sighting + await report_sighting(sighting) + +async def report_sighting(sighting): + name = POKEMON_NAMES[sighting.pokemon_id] + location = geolocator.reverse('' + sighting.lat + ', ' + sighting.lon, exactly_one=True) + street = location.address + disapper_time_diff = ((sighting.expire_timestamp - time.time()) // 60) + message = DISCORD_MESSAGES['sighting'].format(name, sighting.pokemon_id, street, disapper_time_diff) + + for id in config.DISCORD_CHANNELS: + channel = client.get_channel(id) + await client.send_message(channel, message) + +async def sightings_update_task(): + await client.wait_until_ready() + while not client.is_closed: + await check_pokemon() + await asyncio.sleep(config.DISCORD_UPDATE_INTERVAL * 60) + +def remove_stale_sightings(): + old_seen = already_seen.copy() + for key in old_seen: + if old_seen[key].expire_timestamp > time.time(): + del already_seen[key] + +def count_iterable(i): + return sum(1 for e in i) + +client.run(config.DISCORD_TOKEN) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 46916409c4..973c25caf6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,4 @@ werkzeug==0.11.10 sqlalchemy==1.0.14 -e git+https://github.com/keyphact/pgoapi.git@39ea20d31b770dd7bc83180d60283e171090e16d#egg=pgoapi enum34==1.1.6 +discord.py==0.11.0 diff --git a/watchlist.json b/watchlist.json new file mode 100644 index 0000000000..e7c9f764fe --- /dev/null +++ b/watchlist.json @@ -0,0 +1,3 @@ +{ + "watchlist": [3, 6, 9, 26, 38, 40, 51, 76, 94, 95, 104, 107, 110, 115, 122, 123, 128, 132, 139, 141, 142, 143, 148, 149, 150, 151] +} \ No newline at end of file diff --git a/web.py b/web.py index 08e054d068..c0f2315c9f 100644 --- a/web.py +++ b/web.py @@ -22,14 +22,13 @@ 'AREA_NAME', 'REPORT_SINCE', 'SCAN_RADIUS', - 'MAP_PROVIDER_URL', - 'MAP_PROVIDER_ATTRIBUTION', ) for setting_name in REQUIRED_SETTINGS: if not hasattr(config, setting_name): raise RuntimeError('Please set "{}" in config'.format(setting_name)) + def get_args(): parser = argparse.ArgumentParser() parser.add_argument( @@ -59,7 +58,6 @@ def get_args(): def pokemon_data(): return json.dumps(get_pokemarkers()) - @app.route('/workers_data') def workers_data(): return json.dumps({ @@ -67,7 +65,6 @@ def workers_data(): 'scan_radius': config.SCAN_RADIUS, }) - @app.route('/') def fullmap(): map_center = utils.get_map_center() @@ -75,11 +72,8 @@ def fullmap(): 'newmap.html', area_name=config.AREA_NAME, map_center=map_center, - map_provider_url=config.MAP_PROVIDER_URL, - map_provider_attribution=config.MAP_PROVIDER_ATTRIBUTION, ) - def get_pokemarkers(): markers = [] session = db.Session() diff --git a/worker.py b/worker.py index 6942803663..eea977421f 100644 --- a/worker.py +++ b/worker.py @@ -30,6 +30,7 @@ 'ACCOUNTS', 'SCAN_RADIUS', 'SCAN_DELAY', + 'WORKER_LOG_PATH', ) for setting_name in REQUIRED_SETTINGS: if not hasattr(config, setting_name): @@ -40,11 +41,11 @@ local_data = threading.local() -class MalformedResponse(Exception): - """Raised when server response is malformed""" +class CannotProcessStep(Exception): + """Raised when servers are too busy""" -def configure_logger(filename='worker.log'): +def configure_logger(filename=config.WORKER_LOG_PATH): logging.basicConfig( filename=filename, format=( @@ -107,22 +108,18 @@ def run(self): self.restart() return except pgoapi_exceptions.AuthException: - logger.warning('Login failed!') self.error_code = 'LOGIN FAIL' self.restart() return except pgoapi_exceptions.NotLoggedInException: - logger.error('Invalid credentials') self.error_code = 'BAD LOGIN' self.restart() return except pgoapi_exceptions.ServerBusyOrOfflineException: - logger.info('Server too busy - restarting') self.error_code = 'RETRYING' self.restart() return except pgoapi_exceptions.ServerSideRequestThrottlingException: - logger.info('Server throttling - sleeping for a bit') time.sleep(random.uniform(1, 5)) continue except Exception: @@ -137,8 +134,7 @@ def run(self): return try: self.main() - except MalformedResponse: - logger.warning('Malformed response received!') + except CannotProcessStep: self.error_code = 'RESTART' self.restart() except Exception: @@ -151,11 +147,9 @@ def run(self): return self.cycle += 1 if self.cycle <= config.CYCLES_PER_WORKER: - logger.info('Going to sleep for a bit') self.error_code = 'SLEEP' self.running = False time.sleep(random.randint(30, 60)) - logger.info('AWAKEN MY MASTERS') self.running = True self.error_code = None self.error_code = 'RESTART' @@ -178,13 +172,9 @@ def main(self): longitude=pgoapi_utils.f2i(point[1]), cell_id=cell_ids ) - if not isinstance(response_dict, dict): - logger.warning('Response: %s', response_dict) - raise MalformedResponse - responses = response_dict.get('responses') - if not responses: - logger.warning('Response: %s', response_dict) - raise MalformedResponse + logger.debug('Response: %s', response_dict) + if response_dict is False: + raise CannotProcessStep map_objects = response_dict['responses'].get('GET_MAP_OBJECTS', {}) pokemons = [] forts = [] @@ -391,7 +381,7 @@ def parse_args(): if __name__ == '__main__': args = parse_args() if args.status_bar: - configure_logger(filename='worker.log') + configure_logger(filename=config.WORKER_LOG_PATH) logger.info('-' * 30) logger.info('Starting up!') else: From 9c3e3e14e169be833156d9522b83cc6baf014bb4 Mon Sep 17 00:00:00 2001 From: thiemok Date: Tue, 16 Aug 2016 13:56:55 +0200 Subject: [PATCH 2/2] Fixed wrong method call and duplicate output, added pictures to sighting messages, merged upstream woker.py and web.py --- notifications.py | 11 ++++++----- web.py | 8 +++++++- worker.py | 28 +++++++++++++++++++--------- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/notifications.py b/notifications.py index f0b995f577..8a0a43b099 100644 --- a/notifications.py +++ b/notifications.py @@ -30,6 +30,7 @@ already_seen = {} geolocator = GoogleV3(api_key=config.GOOGLE_MAPS_KEY) +icon_path = './static/icons/{}.png' watchlist_path = 'watchlist.json' with open(watchlist_path) as json_data: watchlist = json.load(json_data)['watchlist'] @@ -39,7 +40,7 @@ async def on_ready(): print(DISCORD_MESSAGES['ready'][0].format(join_url)) print(DISCORD_MESSAGES['ready'][1]) print(DISCORD_MESSAGES['ready'][2].format(count_iterable(client.servers))) - client.loop.create_task(check_pokemon()) + client.loop.create_task(sightings_update_task()) for id in config.DISCORD_CHANNELS: channel = client.get_channel(id) await client.send_message(channel, DISCORD_MESSAGES['ready_msg']) @@ -99,7 +100,7 @@ async def check_pokemon(): async def process_sighting(sighting): if sighting.pokemon_id in watchlist: - if sighting.id not in already_seen: + if sighting.id not in already_seen.keys(): already_seen[sighting.id] = sighting await report_sighting(sighting) @@ -112,18 +113,18 @@ async def report_sighting(sighting): for id in config.DISCORD_CHANNELS: channel = client.get_channel(id) - await client.send_message(channel, message) + await client.send_file(channel, icon_path.format(sighting.pokemon_id), content=message) async def sightings_update_task(): await client.wait_until_ready() while not client.is_closed: await check_pokemon() - await asyncio.sleep(config.DISCORD_UPDATE_INTERVAL * 60) + await asyncio.sleep(60 * config.DISCORD_UPDATE_INTERVAL) def remove_stale_sightings(): old_seen = already_seen.copy() for key in old_seen: - if old_seen[key].expire_timestamp > time.time(): + if old_seen[key].expire_timestamp < time.time(): del already_seen[key] def count_iterable(i): diff --git a/web.py b/web.py index c0f2315c9f..08e054d068 100644 --- a/web.py +++ b/web.py @@ -22,13 +22,14 @@ 'AREA_NAME', 'REPORT_SINCE', 'SCAN_RADIUS', + 'MAP_PROVIDER_URL', + 'MAP_PROVIDER_ATTRIBUTION', ) for setting_name in REQUIRED_SETTINGS: if not hasattr(config, setting_name): raise RuntimeError('Please set "{}" in config'.format(setting_name)) - def get_args(): parser = argparse.ArgumentParser() parser.add_argument( @@ -58,6 +59,7 @@ def get_args(): def pokemon_data(): return json.dumps(get_pokemarkers()) + @app.route('/workers_data') def workers_data(): return json.dumps({ @@ -65,6 +67,7 @@ def workers_data(): 'scan_radius': config.SCAN_RADIUS, }) + @app.route('/') def fullmap(): map_center = utils.get_map_center() @@ -72,8 +75,11 @@ def fullmap(): 'newmap.html', area_name=config.AREA_NAME, map_center=map_center, + map_provider_url=config.MAP_PROVIDER_URL, + map_provider_attribution=config.MAP_PROVIDER_ATTRIBUTION, ) + def get_pokemarkers(): markers = [] session = db.Session() diff --git a/worker.py b/worker.py index eea977421f..6942803663 100644 --- a/worker.py +++ b/worker.py @@ -30,7 +30,6 @@ 'ACCOUNTS', 'SCAN_RADIUS', 'SCAN_DELAY', - 'WORKER_LOG_PATH', ) for setting_name in REQUIRED_SETTINGS: if not hasattr(config, setting_name): @@ -41,11 +40,11 @@ local_data = threading.local() -class CannotProcessStep(Exception): - """Raised when servers are too busy""" +class MalformedResponse(Exception): + """Raised when server response is malformed""" -def configure_logger(filename=config.WORKER_LOG_PATH): +def configure_logger(filename='worker.log'): logging.basicConfig( filename=filename, format=( @@ -108,18 +107,22 @@ def run(self): self.restart() return except pgoapi_exceptions.AuthException: + logger.warning('Login failed!') self.error_code = 'LOGIN FAIL' self.restart() return except pgoapi_exceptions.NotLoggedInException: + logger.error('Invalid credentials') self.error_code = 'BAD LOGIN' self.restart() return except pgoapi_exceptions.ServerBusyOrOfflineException: + logger.info('Server too busy - restarting') self.error_code = 'RETRYING' self.restart() return except pgoapi_exceptions.ServerSideRequestThrottlingException: + logger.info('Server throttling - sleeping for a bit') time.sleep(random.uniform(1, 5)) continue except Exception: @@ -134,7 +137,8 @@ def run(self): return try: self.main() - except CannotProcessStep: + except MalformedResponse: + logger.warning('Malformed response received!') self.error_code = 'RESTART' self.restart() except Exception: @@ -147,9 +151,11 @@ def run(self): return self.cycle += 1 if self.cycle <= config.CYCLES_PER_WORKER: + logger.info('Going to sleep for a bit') self.error_code = 'SLEEP' self.running = False time.sleep(random.randint(30, 60)) + logger.info('AWAKEN MY MASTERS') self.running = True self.error_code = None self.error_code = 'RESTART' @@ -172,9 +178,13 @@ def main(self): longitude=pgoapi_utils.f2i(point[1]), cell_id=cell_ids ) - logger.debug('Response: %s', response_dict) - if response_dict is False: - raise CannotProcessStep + if not isinstance(response_dict, dict): + logger.warning('Response: %s', response_dict) + raise MalformedResponse + responses = response_dict.get('responses') + if not responses: + logger.warning('Response: %s', response_dict) + raise MalformedResponse map_objects = response_dict['responses'].get('GET_MAP_OBJECTS', {}) pokemons = [] forts = [] @@ -381,7 +391,7 @@ def parse_args(): if __name__ == '__main__': args = parse_args() if args.status_bar: - configure_logger(filename=config.WORKER_LOG_PATH) + configure_logger(filename='worker.log') logger.info('-' * 30) logger.info('Starting up!') else: