├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── cover.png ├── main.py ├── pokeconfig.py ├── pokedata.csv ├── pokedata.py ├── pokesearch.py ├── pokeslack.py ├── pokeutil.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .env 3 | .DS_Store 4 | cached_pokedata.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 timwah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: python main.py 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PokéSlack 2 | Get Slack messages posted to a #pokealert channel for rare Pokemon close to you. Get walking directions from your search position and the time it expires. 3 | 4 | ![PokeSlack](cover.png?raw=true) 5 | 6 | ## Create your [Slack Webhook](https://api.slack.com/incoming-webhooks) 7 | Create a new webhook for a channel named #pokealerts 8 | https://my.slack.com/services/new/incoming-webhook/ 9 | And use the web hook url as the SLACK_WEBHOOK_URL in your .env / Heroku config. 10 | 11 | ## Configuration 12 | Create an .env file at the project root with the following content filled out. Do not use your main PokemonGo account information. 13 | 14 | AUTH_SERVICE=google/ptc 15 | USERNAME=account@gmail.com 16 | PASSWORD=password 17 | LOCATION_NAME=Some location, USA 18 | RARITY_LIMIT=3 19 | SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX 20 | DISTANCE_UNIT=meters/miles 21 | NUM_STEPS=5 22 | 23 | ### Pokemon Data 24 | This project contains a file `pokedata.csv` where you can customize the assigned rarity to each Pokemon. 25 | Receive notifications for any Pokemon with rarity at `RARITY_LIMIT` or higher and at a distance walkable before the expiration time. 26 | 27 | ## Running 28 | 29 | Locally: 30 | 31 | pip install -r requirements.txt 32 | python main.py 33 | 34 | Using Heroku: 35 | 36 | heroku local 37 | 38 | ## Deploying to Heroku 39 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 40 | 41 | ## Ideas for improvement 42 | Check out the [wiki](https://github.com/timwah/pokeslack/wiki) for a roadmap 43 | 44 | ## Credits 45 | This project builds on existing PokemonGo APIs and integrations: 46 | https://github.com/tejado/pgoapi 47 | https://github.com/AHAAAAAAA/PokemonGo-Map 48 | [@mastermindmatt](https://github.com/mastermindmatt) for the rarity data in pokedata.csv 49 | 50 | ## Donations 51 | [Donate Bitcoins](https://www.coinbase.com/checkouts/2dba5a7fe26b5073e47c50f5d666469b) 52 | 53 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pokéslack", 3 | "description": "Get nearby Pokémon notifications to a Slack channel.", 4 | "repository": "https://github.com/timwah/pokeslack", 5 | "keywords": ["pokemon", "pokemon go", "slack"], 6 | "env": { 7 | "AUTH_SERVICE": { 8 | "description": "Authentication service to use. (ptc or google)" 9 | }, 10 | "USERNAME": { 11 | "description": "Your username to login to the specified authentication service." 12 | }, 13 | "PASSWORD": { 14 | "description": "Your password to login to the specified authentication service." 15 | }, 16 | "LOCATION_NAME": { 17 | "description": "Location on which the map should be centered. This can be an address, co-ordinates or anything that Google Maps accepts." 18 | }, 19 | "RARITY_LIMIT": { 20 | "description": "Only notified for Pokémon at this rarity or higher.", 21 | "value": "3" 22 | }, 23 | "SLACK_WEBHOOK_URL": { 24 | "description": "Looks like https://hooks.slack.com/services/XXX" 25 | }, 26 | "NUM_STEPS": { 27 | "description": "Number of steps to search.", 28 | "value": "5" 29 | }, 30 | "DISTANCE_UNIT": { 31 | "description": "Unit of measurement to use (meters or miles)", 32 | "value": "miles" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timwah/pokeslack/8b0c3f500f8332df4f842d14a04af2b4c5061a5f/cover.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import sys 5 | import time 6 | 7 | from datetime import datetime 8 | from pgoapi import PGoApi 9 | 10 | from pokeconfig import Pokeconfig 11 | from pokedata import json_deserializer, json_serializer 12 | from pokesearch import Pokesearch 13 | from pokeslack import Pokeslack 14 | from pokeutil import get_pos_by_name 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | if __name__ == '__main__': 19 | 20 | logging.basicConfig(stream=sys.stdout, level=logging.INFO) 21 | logging.getLogger('requests').setLevel(logging.WARNING) 22 | logging.getLogger('pgoapi.pgoapi').setLevel(logging.WARNING) 23 | logging.getLogger('pgoapi.rpc_api').setLevel(logging.WARNING) 24 | 25 | logging.info('Pokeslack starting...') 26 | 27 | config = Pokeconfig() 28 | config.load_config('.env') 29 | 30 | auth_service = config.auth_service 31 | username = config.username 32 | password = config.password 33 | location_name = config.location_name 34 | rarity_limit = config.rarity_limit 35 | slack_webhook_url = config.slack_webhook_url 36 | num_steps = config.num_steps 37 | 38 | # debug vars, used to test slack integration w/o waiting 39 | use_cache = False 40 | cached_filename = 'cached_pokedata.json' 41 | search_timeout = 30 42 | 43 | position, address = get_pos_by_name(location_name) 44 | config.position = position 45 | logger.info('location_name: %s', address) 46 | 47 | api = PGoApi() 48 | pokesearch = Pokesearch(api, auth_service, username, password, position) 49 | pokeslack = Pokeslack(rarity_limit, slack_webhook_url) 50 | 51 | if not use_cache or not os.path.exists(cached_filename): 52 | logger.info('searching starting at latlng: (%s, %s)', position[0], position[1]) 53 | pokesearch.login() 54 | while True: 55 | pokemons = [] 56 | for pokemon in pokesearch.search(position, num_steps): 57 | logger.info('adding pokemon: %s', pokemon) 58 | pokeslack.try_send_pokemon(pokemon, debug=False) 59 | pokemons.append(pokemon) 60 | with open(cached_filename, 'w') as fp: 61 | json.dump(pokemons, fp, default=json_serializer, indent=4) 62 | logging.info('done searching, waiting %s seconds...', search_timeout) 63 | time.sleep(search_timeout) 64 | else: 65 | with open(cached_filename, 'r') as fp: 66 | pokemons = json.load(fp, object_hook=json_deserializer) 67 | # for pokemon in pokemons: 68 | # logger.info('loaded pokemon: %s', pokemon) 69 | # pokeslack.try_send_pokemon(pokemon, position, distance, debug=True) 70 | logger.info('loaded cached pokemon data for %s pokemon', len(pokemons)) 71 | -------------------------------------------------------------------------------- /pokeconfig.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | class Pokeconfig: 7 | # constants 8 | WALK_MILES_PER_SECOND = 0.0008333 # assumes 3mph (or 0.0008333 miles per second) walking speed 9 | WALK_METERS_PER_SECOND = 1.34112 # conversion from 3mph 10 | EXPIRE_BUFFER_SECONDS = 5 # if a pokemon expires in 5 seconds or less (includes negative/stale pokemon), dont send it 11 | DEFAULT_NUM_STEPS = 5 12 | DEFAULT_DISTANCE_UNIT = 'miles' 13 | 14 | # configured via env 15 | auth_service = None 16 | username = None 17 | password = None 18 | location_name = None 19 | rarity_limit = 3 20 | slack_webhook_url = None 21 | num_steps = DEFAULT_NUM_STEPS 22 | distance_unit = DEFAULT_DISTANCE_UNIT 23 | position = () 24 | 25 | def load_config(self, config_path): 26 | is_local = False 27 | if not 'DYNO' in os.environ: 28 | is_local = True 29 | if not os.path.exists(config_path): 30 | logging.error('please create a .env file in this directory in order to run locally!') 31 | exit(-1) 32 | 33 | # used for local testing without starting up heroku 34 | if is_local: 35 | env = {} 36 | logger.info('running locally, reading config from %s', config_path) 37 | with open(config_path, 'r') as fp: 38 | for line in fp: 39 | idx = line.index('=') 40 | key = line[:idx] 41 | value = line[idx + 1:].strip() 42 | env[key] = value 43 | else: 44 | logger.info('running on heroku, reading config from environment') 45 | env = os.environ 46 | 47 | try: 48 | self.auth_service = str(env['AUTH_SERVICE']) 49 | self.username = str(env['USERNAME']) 50 | self.password = str(env['PASSWORD']) 51 | self.location_name = str(env['LOCATION_NAME']) 52 | self.rarity_limit = int(env['RARITY_LIMIT']) 53 | self.slack_webhook_url = str(env['SLACK_WEBHOOK_URL']) 54 | if 'NUM_STEPS' in env: 55 | self.num_steps = int(env['NUM_STEPS']) 56 | else: 57 | logging.warn('NUM_STEPS not defined defaulting to: %s', self.num_steps) 58 | if 'DISTANCE_UNIT' in env: 59 | self.distance_unit = str(env['DISTANCE_UNIT']) 60 | else: 61 | logging.warn('DISTANCE_UNIT not defined defaulting to: %s', self.distance_unit) 62 | except KeyError as ke: 63 | logging.error('key must be defined in config: %s!', ke) 64 | exit(-1) 65 | 66 | Pokeconfig._instance = self 67 | logger.info('loaded config with params') 68 | for key, value in vars(self).iteritems(): 69 | if key == 'password': 70 | value = '****' 71 | logger.info('%s=%s', key, value) 72 | 73 | _instance = None 74 | @staticmethod 75 | def get(): 76 | return Pokeconfig._instance 77 | -------------------------------------------------------------------------------- /pokedata.csv: -------------------------------------------------------------------------------- 1 | 1,Bulbasaur,2 2 | 2,Ivysaur,3 3 | 3,Venusaur,4 4 | 4,Charmander,2 5 | 5,Charmeleon,3 6 | 6,Charizard,4 7 | 7,Squirtle,2 8 | 8,Wartortle,3 9 | 9,Blastoise,4 10 | 10,Caterpie,1 11 | 11,Metapod,2 12 | 12,Butterfree,3 13 | 13,Weedle,1 14 | 14,Kakuna,2 15 | 15,Beedrill,3 16 | 16,Pidgey,1 17 | 17,Pidgeotto,2 18 | 18,Pidgeot,3 19 | 19,Rattata,1 20 | 20,Raticate,2 21 | 21,Spearow,1 22 | 22,Fearow,2 23 | 23,Ekans,2 24 | 24,Arbok,3 25 | 25,Pikachu,3 26 | 26,Raichu,4 27 | 27,Sandshrew,2 28 | 28,Sandslash,3 29 | 29,Nidoran_F,1 30 | 30,Nidorina,2 31 | 31,Nidoqueen,3 32 | 32,Nidoran_M,1 33 | 33,Nidorino,2 34 | 34,Nidoking,3 35 | 35,Clefairy,1 36 | 36,Clefable,3 37 | 37,Vulpix,2 38 | 38,Ninetales,3 39 | 39,Jigglypuff,2 40 | 40,Wigglytuff,3 41 | 41,Zubat,1 42 | 42,Golbat,2 43 | 43,Oddish,2 44 | 44,Gloom,3 45 | 45,Vileplume,4 46 | 46,Paras,1 47 | 47,Parasect,2 48 | 48,Venonat,1 49 | 49,Venomoth,2 50 | 50,Diglett,2 51 | 51,Dugtrio,3 52 | 52,Meowth,2 53 | 53,Persian,3 54 | 54,Psyduck,2 55 | 55,Golduck,3 56 | 56,Mankey,2 57 | 57,Primeape,3 58 | 58,Growlithe,2 59 | 59,Arcanine,4 60 | 60,Poliwag,1 61 | 61,Poliwhirl,2 62 | 62,Poliwrath,3 63 | 63,Abra,2 64 | 64,Kadabra,3 65 | 65,Alakazam,4 66 | 66,Machop,2 67 | 67,Machoke,3 68 | 68,Machamp,4 69 | 69,Bellsprout,2 70 | 70,Weepinbell,3 71 | 71,Victreebel,4 72 | 72,Tentacool,1 73 | 73,Tentacruel,3 74 | 74,Geodude,2 75 | 75,Graveler,3 76 | 76,Golem,4 77 | 77,Ponyta,2 78 | 78,Rapidash,3 79 | 79,Slowpoke,2 80 | 80,Slowbro,3 81 | 81,Magnemite,3 82 | 82,Magneton,4 83 | 83,Farfetch'd,4 84 | 84,Doduo,1 85 | 85,Dodrio,2 86 | 86,Seel,1 87 | 87,Dewgong,2 88 | 88,Grimer,2 89 | 89,Muk,3 90 | 90,Shellder,3 91 | 91,Cloyster,4 92 | 92,Gastly,2 93 | 93,Haunter,3 94 | 94,Gengar,4 95 | 95,Onix,4 96 | 96,Drowzee,2 97 | 97,Hypno,3 98 | 98,Krabby,1 99 | 99,Kingler,2 100 | 100,Voltorb,2 101 | 101,Electrode,3 102 | 102,Exeggcute,2 103 | 103,Exeggutor,3 104 | 104,Cubone,2 105 | 105,Marowak,3 106 | 106,Hitmonlee,3 107 | 107,Hitmonchan,3 108 | 108,Lickitung,3 109 | 109,Koffing,2 110 | 110,Weezing,3 111 | 111,Rhyhorn,3 112 | 112,Rhydon,4 113 | 113,Chansey,4 114 | 114,Tangela,2 115 | 115,Kangaskhan,4 116 | 116,Horsea,1 117 | 117,Seadra,3 118 | 118,Goldeen,1 119 | 119,Seaking,3 120 | 120,Staryu,1 121 | 121,Starmie,3 122 | 122,Mr.Mime,4 123 | 123,Scyther,4 124 | 124,Jynx,3 125 | 125,Electabuzz,4 126 | 126,Magmar,4 127 | 127,Pinsir,2 128 | 128,Tauros,2 129 | 129,Magikarp,1 130 | 130,Gyarados,4 131 | 131,Lapras,4 132 | 132,Ditto,4 133 | 133,Eevee,2 134 | 134,Vaporeon,4 135 | 135,Jolteon,4 136 | 136,Flareon,4 137 | 137,Porygon,4 138 | 138,Omanyte,3 139 | 139,Omastar,4 140 | 140,Kabuto,3 141 | 141,Kabutops,4 142 | 142,Aerodactyl,4 143 | 143,Snorlax,4 144 | 144,Articuno,5 145 | 145,Zapdos,5 146 | 146,Moltres,5 147 | 147,Dratini,2 148 | 148,Dragonair,3 149 | 149,Dragonite,4 150 | 150,Mewtwo,5 151 | 151,Mew,5 152 | -------------------------------------------------------------------------------- /pokedata.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | import csv 3 | 4 | from base64 import b64encode 5 | from datetime import datetime 6 | from geopy.distance import vincenty 7 | 8 | from pokeconfig import Pokeconfig 9 | 10 | class Pokedata: 11 | pokedata = None 12 | @staticmethod 13 | def get(pokemon_id): 14 | if not Pokedata.pokedata: 15 | Pokedata.pokedata = {} 16 | with open('pokedata.csv', 'rU') as csvfile: 17 | reader = csv.reader(csvfile) 18 | for row in reader: 19 | id = int(row[0]) 20 | name = row[1] 21 | rarity = int(row[2]) 22 | Pokedata.pokedata[id] = { 23 | 'name': name, 24 | 'rarity': rarity 25 | } 26 | return Pokedata.pokedata[pokemon_id] 27 | 28 | class Pokemon: 29 | position = () 30 | latitude = None 31 | longitude = None 32 | pokemon_id = 0 33 | encounter_id = None 34 | spawnpoint_id = None 35 | disappear_time = None 36 | from_lure = False 37 | pokestop_id = 0 38 | name = None 39 | rarity = 1 40 | key = None 41 | 42 | @staticmethod 43 | def from_pokemon(pokemon): 44 | p = Pokemon() 45 | p.encounter_id = b64encode(str(pokemon['encounter_id'])) 46 | p.spawnpoint_id = pokemon['spawnpoint_id'] 47 | p.pokemon_id = pokemon['pokemon_data']['pokemon_id'] 48 | p.position = (pokemon['latitude'], pokemon['longitude'], 0) 49 | p.disappear_time = datetime.utcfromtimestamp( 50 | (pokemon['last_modified_timestamp_ms'] + 51 | pokemon['time_till_hidden_ms']) / 1000.0) 52 | p._get_pokedata() 53 | return p 54 | 55 | @staticmethod 56 | def from_pokestop(pokestop): 57 | p = Pokemon() 58 | p.position = (pokestop['latitude'], pokestop['longitude'], 0) 59 | p.pokemon_id = pokestop['active_pokemon_id'] 60 | p.disappear_time = pokestop['lure_expiration'] 61 | p.from_lure = True 62 | p.pokestop_id = pokestop['pokestop_id'] 63 | p._get_pokedata() 64 | return p 65 | 66 | def _get_pokedata(self): 67 | pokedata = Pokedata.get(self.pokemon_id) 68 | self.name = pokedata['name'] 69 | self.rarity = pokedata['rarity'] 70 | self.key = self._get_key() 71 | 72 | def _get_key(self): 73 | if self.from_lure: 74 | key = '%s_%s' % (self.pokestop_id, self.pokemon_id) 75 | else: 76 | key = self.encounter_id 77 | return key 78 | 79 | def expires_in(self): 80 | return self.disappear_time - datetime.utcnow() 81 | 82 | def expires_in_str(self): 83 | min_remaining = int(self.expires_in().total_seconds() / 60) 84 | return '%s%ss' % ('%dm' % min_remaining if min_remaining > 0 else '', self.expires_in().seconds - 60 * min_remaining) 85 | 86 | def get_distance(self): 87 | position = Pokeconfig.get().position 88 | distance = vincenty(position, self.position) 89 | if Pokeconfig.get().distance_unit == 'meters': 90 | return distance.meters 91 | else: 92 | return distance.miles 93 | 94 | def get_distance_str(self): 95 | if Pokeconfig.get().distance_unit == 'meters': 96 | return '{:.0f} meters'.format(self.get_distance()) 97 | else: 98 | return '{:.3f} miles'.format(self.get_distance()) 99 | 100 | def __str__(self): 101 | return '%s' % (self.name, self.pokemon_id, self.key, self.rarity, self.expires_in_str(), self.get_distance_str()) 102 | 103 | def parse_map(map_dict): 104 | pokemons = {} 105 | pokestops = {} 106 | 107 | cells = map_dict['responses']['GET_MAP_OBJECTS']['map_cells'] 108 | for cell in cells: 109 | for p in cell.get('wild_pokemons', []): 110 | pokemon = Pokemon.from_pokemon(p) 111 | pokemons[pokemon.key] = pokemon 112 | 113 | for f in cell.get('forts', []): 114 | if f.get('type') == 1: # Pokestops 115 | if 'lure_info' in f: 116 | lure_expiration = datetime.utcfromtimestamp( 117 | f['lure_info']['lure_expires_timestamp_ms'] / 1000.0) 118 | active_pokemon_id = f['lure_info']['active_pokemon_id'] 119 | # logger.debug("at fort: %s, have active pokemon_id: %s", f['lure_info']['fort_id'], active_pokemon_id) 120 | else: 121 | lure_expiration, active_pokemon_id = None, None 122 | 123 | pokestops[f['id']] = { 124 | 'pokestop_id': f['id'], 125 | 'enabled': f['enabled'], 126 | 'latitude': f['latitude'], 127 | 'longitude': f['longitude'], 128 | 'last_modified': datetime.utcfromtimestamp( 129 | f['last_modified_timestamp_ms'] / 1000.0), 130 | 'lure_expiration': lure_expiration, 131 | 'active_pokemon_id': active_pokemon_id 132 | } 133 | if pokestops: 134 | for key in pokestops.keys(): 135 | pokestop = pokestops[key] 136 | pokemon_id = pokestop['active_pokemon_id'] 137 | if pokemon_id: 138 | pokemon = Pokemon.from_pokestop(pokestop) 139 | if pokemon.pokemon_id and not pokemon.key in pokemons: 140 | pokemons[pokemon.key] = pokemon 141 | 142 | return pokemons 143 | 144 | def json_deserializer(obj): 145 | for key, value in obj.items(): 146 | if key == 'disappear_time': 147 | value = datetime.utcfromtimestamp(value / 1000.0) 148 | obj[key] = value 149 | return obj 150 | 151 | def json_serializer(obj): 152 | try: 153 | if isinstance(obj, datetime): 154 | if obj.utcoffset() is not None: 155 | obj = obj - obj.utcoffset() 156 | millis = int( 157 | calendar.timegm(obj.timetuple()) * 1000 + 158 | obj.microsecond / 1000 159 | ) 160 | return millis 161 | iterable = iter(obj) 162 | except TypeError: 163 | pass 164 | else: 165 | return list(iterable) 166 | -------------------------------------------------------------------------------- /pokesearch.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import math 4 | import random 5 | import time 6 | 7 | from datetime import datetime 8 | from pgoapi.utilities import f2i 9 | from s2sphere import CellId, LatLng 10 | 11 | from pokedata import Pokedata, parse_map 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | REQ_SLEEP = 5 16 | MAX_NUM_RETRIES = 10 17 | 18 | #Constants for Hex Grid 19 | #Gap between vertical and horzonal "rows" 20 | lat_gap_meters = 150 21 | lng_gap_meters = 86.6 22 | 23 | #111111m is approx 1 degree Lat, which is close enough for this 24 | meters_per_degree = 111111 25 | lat_gap_degrees = float(lat_gap_meters) / meters_per_degree 26 | 27 | def calculate_lng_degrees(lat): 28 | return float(lng_gap_meters) / (meters_per_degree * math.cos(math.radians(lat))) 29 | 30 | class Pokesearch: 31 | def __init__(self, api, auth_service, username, password, position): 32 | self.api = api 33 | self.auth_service = auth_service 34 | self.username = username 35 | self.password = password 36 | self.position = position 37 | self.visible_range_meters = 70 38 | 39 | 40 | def login(self): 41 | logger.info('login start with service: %s', self.auth_service) 42 | 43 | self.api.set_position(*self.position) 44 | 45 | num_retries = 0 46 | while not self.api.login(self.auth_service, self.username, self.password): 47 | num_retries += 1 48 | timeout = REQ_SLEEP * (int(num_retries / 5.0) + 1) 49 | logger.warn('failed to login to pokemon go, retrying... timeout: %s, num_retries: %s', timeout, num_retries) 50 | time.sleep(REQ_SLEEP * (int(num_retries / 5.0) + 1)) 51 | 52 | self._update_download_settings() 53 | 54 | logger.info('login successful') 55 | 56 | def search(self, position, num_steps): 57 | if self.api._auth_provider and self.api._auth_provider._ticket_expire: 58 | if isinstance(self.api._auth_provider._ticket_expire, (int, long)): 59 | remaining_time = self.api._auth_provider._ticket_expire / 1000.0 - time.time() 60 | if remaining_time > 60: 61 | logger.info("Skipping Pokemon Go login process since already logged in for another {:.2f} seconds".format(remaining_time)) 62 | else: 63 | self.login() 64 | else: 65 | logger.warn("skipping login since _ticket_expire was a token.") 66 | else: 67 | self.login() 68 | 69 | all_pokemon = {} 70 | num_retries = 0 71 | 72 | for step, coord in enumerate(generate_location_steps(position, num_steps, self.visible_range_meters), 1): 73 | lat = coord[0] 74 | lng = coord[1] 75 | self.api.set_position(*coord) 76 | 77 | cell_ids = get_cell_ids(lat, lng) 78 | timestamps = [0,] * len(cell_ids) 79 | 80 | response_dict = None 81 | while not response_dict: 82 | try: 83 | self.api.get_map_objects(latitude = f2i(lat), longitude = f2i(lng), since_timestamp_ms = timestamps, cell_id = cell_ids) 84 | response_dict = self.api.call() 85 | except: 86 | logging.warn('exception happened on get_map_objects api call', exc_info=True) 87 | if not response_dict: 88 | if num_retries < MAX_NUM_RETRIES: 89 | num_retries += 1 90 | logger.warn('get_map_objects failed, retrying in %s seconds, %s retries', REQ_SLEEP, num_retries) 91 | time.sleep(REQ_SLEEP) 92 | else: 93 | logger.warn('MAX_NUM_RETRIES exceeded, retrying login...') 94 | self.login() 95 | raise StopIteration 96 | 97 | # try: 98 | pokemons = parse_map(response_dict) 99 | # except KeyError as e: 100 | # logger.error('failed to parse map with key error: %s', e) 101 | 102 | for key in pokemons.keys(): 103 | if not key in all_pokemon: 104 | pokemon = pokemons[key] 105 | all_pokemon[key] = pokemon 106 | yield pokemon 107 | # else: 108 | # logger.info("have duplicate poke: %s", key) 109 | total_steps = (3 * (num_steps**2)) - (3 * num_steps) + 1 110 | logger.info('Completed {:5.2f}% of scan.'.format(float(step) / total_steps * 100)) 111 | time.sleep(REQ_SLEEP) 112 | 113 | def _update_download_settings(self): 114 | visible_range_meters = 0 115 | while visible_range_meters == 0: 116 | try: 117 | logger.info('fetching download settings...') 118 | self.api.download_settings(hash="05daf51635c82611d1aac95c0b051d3ec088a930") 119 | response_dict = self.api.call() 120 | visible_range_meters = response_dict['responses']['DOWNLOAD_SETTINGS']['settings']['map_settings']['pokemon_visible_range'] 121 | self.visible_range_meters = float(visible_range_meters) 122 | except: 123 | logging.warn('exception happened on download_settings api call', exc_info=True) 124 | logger.info('download settings[pokemon_visible_range]: %s', self.visible_range_meters) 125 | 126 | def generate_location_steps(position, num_steps, visible_range_meters): 127 | #Bearing (degrees) 128 | NORTH = 0 129 | EAST = 90 130 | SOUTH = 180 131 | WEST = 270 132 | 133 | pulse_radius = visible_range_meters / 1000.0 # km - radius of players heartbeat is 100m 134 | xdist = math.sqrt(3)*pulse_radius # dist between column centers 135 | ydist = 3*(pulse_radius/2) # dist between row centers 136 | 137 | yield (position[0], position[1], 0) #insert initial location 138 | 139 | ring = 1 140 | loc = position 141 | while ring < num_steps: 142 | #Set loc to start at top left 143 | loc = get_new_coords(loc, ydist, NORTH) 144 | loc = get_new_coords(loc, xdist/2, WEST) 145 | for direction in range(6): 146 | for i in range(ring): 147 | if direction == 0: # RIGHT 148 | loc = get_new_coords(loc, xdist, EAST) 149 | if direction == 1: # DOWN + RIGHT 150 | loc = get_new_coords(loc, ydist, SOUTH) 151 | loc = get_new_coords(loc, xdist/2, EAST) 152 | if direction == 2: # DOWN + LEFT 153 | loc = get_new_coords(loc, ydist, SOUTH) 154 | loc = get_new_coords(loc, xdist/2, WEST) 155 | if direction == 3: # LEFT 156 | loc = get_new_coords(loc, xdist, WEST) 157 | if direction == 4: # UP + LEFT 158 | loc = get_new_coords(loc, ydist, NORTH) 159 | loc = get_new_coords(loc, xdist/2, WEST) 160 | if direction == 5: # UP + RIGHT 161 | loc = get_new_coords(loc, ydist, NORTH) 162 | loc = get_new_coords(loc, xdist/2, EAST) 163 | yield (loc[0], loc[1], 0) 164 | ring += 1 165 | 166 | def get_new_coords(init_loc, distance, bearing): 167 | """ Given an initial lat/lng, a distance(in kms), and a bearing (degrees), 168 | this will calculate the resulting lat/lng coordinates. 169 | """ 170 | R = 6378.1 #km radius of the earth 171 | bearing = math.radians(bearing) 172 | 173 | init_coords = [math.radians(init_loc[0]), math.radians(init_loc[1])] # convert lat/lng to radians 174 | 175 | new_lat = math.asin( math.sin(init_coords[0])*math.cos(distance/R) + 176 | math.cos(init_coords[0])*math.sin(distance/R)*math.cos(bearing)) 177 | 178 | new_lon = init_coords[1] + math.atan2(math.sin(bearing)*math.sin(distance/R)*math.cos(init_coords[0]), 179 | math.cos(distance/R)-math.sin(init_coords[0])*math.sin(new_lat)) 180 | 181 | return [math.degrees(new_lat), math.degrees(new_lon)] 182 | 183 | def get_cell_ids(lat, lng, radius = 10): 184 | origin = CellId.from_lat_lng(LatLng.from_degrees(lat, lng)).parent(15) 185 | walk = [origin.id()] 186 | right = origin.next() 187 | left = origin.prev() 188 | 189 | # Search around provided radius 190 | for i in range(radius): 191 | walk.append(right.id()) 192 | walk.append(left.id()) 193 | right = right.next() 194 | left = left.prev() 195 | 196 | # Return everything 197 | return sorted(walk) 198 | -------------------------------------------------------------------------------- /pokeslack.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | 3 | import json 4 | import logging 5 | import requests 6 | 7 | from datetime import datetime 8 | from pokeconfig import Pokeconfig 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | class Pokeslack: 13 | def __init__(self, rarity_limit, slack_webhook_url): 14 | self.sent_pokemon = {} 15 | self.rarity_limit = rarity_limit 16 | self.slack_webhook_url = slack_webhook_url 17 | 18 | def try_send_pokemon(self, pokemon, debug): 19 | 20 | if pokemon.expires_in().total_seconds() < Pokeconfig.EXPIRE_BUFFER_SECONDS: 21 | logger.info('skipping pokemon since it expires too soon') 22 | return 23 | 24 | if pokemon.rarity < self.rarity_limit: 25 | logger.info('skipping pokemon since its rarity is too low') 26 | return 27 | 28 | padded_distance = pokemon.get_distance() * 1.1 29 | walk_distance_per_second = Pokeconfig.WALK_METERS_PER_SECOND if Pokeconfig.get().distance_unit == 'meters' else Pokeconfig.WALK_MILES_PER_SECOND 30 | travel_time = padded_distance / walk_distance_per_second 31 | if pokemon.expires_in().total_seconds() < travel_time: 32 | logger.info('skipping pokemon since it\'s too far: traveltime=%s for distance=%s', travel_time, pokemon.get_distance_str()) 33 | return 34 | 35 | pokemon_key = pokemon.key 36 | if pokemon_key in self.sent_pokemon: 37 | logger.info('already sent this pokemon to slack with key %s', pokemon_key) 38 | return 39 | 40 | from_lure = ', from a lure' if pokemon.from_lure else '' 41 | miles_away = pokemon.get_distance_str() 42 | 43 | position = Pokeconfig.get().position 44 | 45 | pokedex_url = 'http://www.pokemon.com/us/pokedex/%s' % pokemon.pokemon_id 46 | map_url = 'http://maps.google.com?saddr=%s,%s&daddr=%s,%s&directionsmode=walking' % (position[0], position[1], pokemon.position[0], pokemon.position[1]) 47 | time_remaining = pokemon.expires_in_str() 48 | stars = ''.join([':star:' for x in xrange(pokemon.rarity)]) 49 | message = 'I found a <%s|%s> %s <%s|%s away> expiring in %s%s' % (pokedex_url, pokemon.name, stars, map_url, miles_away, time_remaining, from_lure) 50 | # bold message if rarity > 4 51 | if pokemon.rarity >= 4: 52 | message = '*%s*' % message 53 | 54 | logging.info('%s: %s', pokemon_key, message) 55 | if self._send(message): 56 | self.sent_pokemon[pokemon_key] = True 57 | 58 | def _send(self, message): 59 | payload = { 60 | 'username': 'Poké Alert!', 61 | 'text': message, 62 | 'icon_emoji': ':ghost:' 63 | } 64 | s = json.dumps(payload) 65 | r = requests.post(self.slack_webhook_url, data=s) 66 | logger.info('slack post result: %s, %s', r.status_code, r.reason) 67 | return r.status_code == 200 68 | -------------------------------------------------------------------------------- /pokeutil.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from geopy.geocoders import GoogleV3 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | def get_pos_by_name(location_name): 8 | geolocator = GoogleV3() 9 | loc = geolocator.geocode(location_name, timeout=10) 10 | 11 | logger.debug('location: %s', loc.address.encode('utf-8')) 12 | logger.debug('lat, long, alt: %s, %s, %s', loc.latitude, loc.longitude, loc.altitude) 13 | 14 | return (loc.latitude, loc.longitude, loc.altitude), loc.address.encode('utf-8') 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future==0.15.2 2 | geopy==1.11.0 3 | gpsoauth==0.3.0 4 | -e git://github.com/tejado/pgoapi.git@v1.1.0#egg=pgoapi 5 | protobuf==3.0.0b4 6 | pycryptodomex==3.4.2 7 | requests==2.10.0 8 | s2sphere==0.2.4 9 | six==1.10.0 10 | wsgiref==0.1.2 11 | --------------------------------------------------------------------------------