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..8a0a43b099 --- /dev/null +++ b/notifications.py @@ -0,0 +1,133 @@ +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) + +icon_path = './static/icons/{}.png' +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(sightings_update_task()) + 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.keys(): + 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_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(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(): + 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