diff --git a/.travis.yml b/.travis.yml index a386f9e2..d0e16820 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,18 @@ +sudo: false language: python - +notifications: + email: + on_success: never + on_failure: always +cache: + pip: true + directories: + - node_modules python: - "2.7" - - "3.5" - "3.6" - install: - pip install -r requirements.txt - python setup.py install - script: - python -c 'import pgoapi' diff --git a/LICENSE.txt b/LICENSE.md similarity index 100% rename from LICENSE.txt rename to LICENSE.md diff --git a/README.md b/README.md index 41e6f250..84011fbc 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,6 @@ It allows automatic parsing of requests/responses by finding the correct protobu * Thread-safety * Advanced logging/debugging * Uses [POGOProtos](https://github.com/AeonLucid/POGOProtos) - * Mostly all available RPC calls (see [API reference](https://docs.pogodev.org) on the wiki) - -## Documentation -Documentation is available at the github [pgoapi wiki](https://wiki.pogodev.org). ## Requirements * Python 2 or 3 @@ -34,20 +30,20 @@ Documentation is available at the github [pgoapi wiki](https://wiki.pogodev.org) ## Use To use this api as part of a python project using setuptools/pip, modify your requirements.txt file to include: ``` -git+https://github.com/pogodevorg/pgoapi.git@develop#egg=pgoapi +git+https://github.com/sebastienvercammen/pgoapi.git@develop#egg=pgoapi ``` If you are not using setuptools/pip, follow the instructions in the Contributing section below to clone this repository and then install pgoapi using the appropriate method for your project. ## Contributing -Contributions are highly welcome. Please use github or [Discord](https://discord.pogodev.org) for it! +Contributions are highly welcome. Please use github or [Discord](https://discord.gg/rocketmap) for it! You can get started by cloning this repository. Note that as pgoapi uses [git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules) you must perform a recursive clone: | Protocol | Command | | -------- | ------- | -| HTTPS | `git clone --recursive https://github.com/pogodevorg/pgoapi.git` | -| SSH | `git clone --recursive git@github.com:pogodevorg/pgoapi.git` | +| HTTPS | `git clone --recursive https://github.com/sebastienvercammen/pgoapi.git` | +| SSH | `git clone --recursive git@github.com:sebastienvercammen/pgoapi.git` | If you already have a copy of the repository you can use `git submodule update --init` to fetch and update the submodules. diff --git a/examples/pogo-optimizer/pogo-optimizer-cli.py b/examples/pogo-optimizer/pogo-optimizer-cli.py index 1e78d853..39071eaf 100644 --- a/examples/pogo-optimizer/pogo-optimizer-cli.py +++ b/examples/pogo-optimizer/pogo-optimizer-cli.py @@ -50,29 +50,40 @@ log = logging.getLogger(__name__) + def encode(cellid): output = [] encoder._VarintEncoder()(output.append, cellid) return ''.join(output) + def init_config(): parser = argparse.ArgumentParser() config_file = "config.json" # If config file exists, load variables from json - load = {} + load = {} if os.path.isfile(config_file): with open(config_file) as data: load.update(json.load(data)) # Read passed in Arguments required = lambda x: not x in load - parser.add_argument("-a", "--auth_service", help="Auth Service ('ptc' or 'google')", + parser.add_argument( + "-a", + "--auth_service", + help="Auth Service ('ptc' or 'google')", required=required("auth_service")) - parser.add_argument("-u", "--username", help="Username", required=required("username")) + parser.add_argument( + "-u", "--username", help="Username", required=required("username")) parser.add_argument("-p", "--password", help="Password") - parser.add_argument("-d", "--debug", help="Debug Mode", action='store_true') - parser.add_argument("-t", "--test", help="Only parse the specified location", action='store_true') + parser.add_argument( + "-d", "--debug", help="Debug Mode", action='store_true') + parser.add_argument( + "-t", + "--test", + help="Only parse the specified location", + action='store_true') parser.set_defaults(DEBUG=False, TEST=False) config = parser.parse_args() @@ -82,19 +93,24 @@ def init_config(): config.__dict__[key] = str(load[key]) if config.__dict__["password"] is None: - log.info("Secure Password Input (if there is no password prompt, use --password ):") + log.info( + "Secure Password Input (if there is no password prompt, use --password ):" + ) config.__dict__["password"] = getpass.getpass() if config.auth_service not in ['ptc', 'google']: - log.error("Invalid Auth service specified! ('ptc' or 'google')") - return None + log.error("Invalid Auth service specified! ('ptc' or 'google')") + return None return config + def main(): # log settings # log format - logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') # log level for http request class logging.getLogger("requests").setLevel(logging.WARNING) # log level for main pgoapi class @@ -137,21 +153,39 @@ def main(): def format(i): i = i['inventory_item_data']['pokemon_data'] - i = {k: v for k, v in i.items() if k in ['nickname','move_1', 'move_2', 'pokemon_id', 'individual_defense', 'stamina', 'cp', 'individual_stamina', 'individual_attack']} - i['individual_defense'] = i.get('individual_defense', 0) - i['individual_attack'] = i.get('individual_attack', 0) - i['individual_stamina'] = i.get('individual_stamina', 0) - i['power_quotient'] = round(((float(i['individual_defense']) + float(i['individual_attack']) + float(i['individual_stamina'])) / 45) * 100) - i['name'] = list(filter(lambda j: int(j['Number']) == i['pokemon_id'], pokemon))[0]['Name'] - i['move_1'] = list(filter(lambda j: j['id'] == i['move_1'], moves))[0]['name'] - i['move_2'] = list(filter(lambda j: j['id'] == i['move_2'], moves))[0]['name'] + i = { + k: v + for k, v in i.items() + if k in [ + 'nickname', 'move_1', 'move_2', 'pokemon_id', + 'individual_defense', 'stamina', 'cp', 'individual_stamina', + 'individual_attack' + ] + } + i['individual_defense'] = i.get('individual_defense', 0) + i['individual_attack'] = i.get('individual_attack', 0) + i['individual_stamina'] = i.get('individual_stamina', 0) + i['power_quotient'] = round( + ((float(i['individual_defense']) + float(i['individual_attack']) + + float(i['individual_stamina'])) / 45) * 100) + i['name'] = list( + filter(lambda j: int(j['Number']) == i['pokemon_id'], pokemon))[0][ + 'Name'] + i['move_1'] = list( + filter(lambda j: j['id'] == i['move_1'], moves))[0]['name'] + i['move_2'] = list( + filter(lambda j: j['id'] == i['move_2'], moves))[0]['name'] return i - all_pokemon = filter(lambda i: 'pokemon_data' in i['inventory_item_data'] and 'is_egg' not in i['inventory_item_data']['pokemon_data'], response_dict['responses']['GET_INVENTORY']['inventory_delta']['inventory_items']) + all_pokemon = filter( + lambda i: 'pokemon_data' in i['inventory_item_data'] and 'is_egg' not in i['inventory_item_data']['pokemon_data'], + response_dict['responses']['GET_INVENTORY']['inventory_delta'][ + 'inventory_items']) all_pokemon = list(map(format, all_pokemon)) all_pokemon.sort(key=lambda x: x['power_quotient'], reverse=True) - print(tabulate(all_pokemon, headers = "keys")) + print(tabulate(all_pokemon, headers="keys")) + if __name__ == '__main__': main() diff --git a/examples/spiral_poi_search.py b/examples/spiral_poi_search.py index bc5b2277..a3948e86 100755 --- a/examples/spiral_poi_search.py +++ b/examples/spiral_poi_search.py @@ -46,6 +46,7 @@ log = logging.getLogger(__name__) + def get_pos_by_name(location_name): geolocator = GoogleV3() loc = geolocator.geocode(location_name) @@ -53,11 +54,13 @@ def get_pos_by_name(location_name): return None log.info('Your given location: %s', loc.address.encode('utf-8')) - log.info('lat/long/alt: %s %s %s', loc.latitude, loc.longitude, loc.altitude) + log.info('lat/long/alt: %s %s %s', loc.latitude, loc.longitude, + loc.altitude) return (loc.latitude, loc.longitude, loc.altitude) -def get_cell_ids(lat, long, radius = 10): + +def get_cell_ids(lat, long, radius=10): origin = CellId.from_lat_lng(LatLng.from_degrees(lat, long)).parent(15) walk = [origin.id()] right = origin.next() @@ -73,30 +76,43 @@ def get_cell_ids(lat, long, radius = 10): # Return everything return sorted(walk) + def encode(cellid): output = [] encoder._VarintEncoder()(output.append, cellid) return ''.join(output) + def init_config(): parser = argparse.ArgumentParser() config_file = "config.json" # If config file exists, load variables from json - load = {} + load = {} if os.path.isfile(config_file): with open(config_file) as data: load.update(json.load(data)) # Read passed in Arguments required = lambda x: x not in load - parser.add_argument("-a", "--auth_service", help="Auth Service ('ptc' or 'google')", + parser.add_argument( + "-a", + "--auth_service", + help="Auth Service ('ptc' or 'google')", required=required("auth_service")) - parser.add_argument("-u", "--username", help="Username", required=required("username")) - parser.add_argument("-p", "--password", help="Password", required=required("password")) - parser.add_argument("-l", "--location", help="Location", required=required("location")) - parser.add_argument("-d", "--debug", help="Debug Mode", action='store_true') - parser.add_argument("-t", "--test", help="Only parse the specified location", action='store_true') + parser.add_argument( + "-u", "--username", help="Username", required=required("username")) + parser.add_argument( + "-p", "--password", help="Password", required=required("password")) + parser.add_argument( + "-l", "--location", help="Location", required=required("location")) + parser.add_argument( + "-d", "--debug", help="Debug Mode", action='store_true') + parser.add_argument( + "-t", + "--test", + help="Only parse the specified location", + action='store_true') parser.set_defaults(DEBUG=False, TEST=False) config = parser.parse_args() @@ -106,15 +122,18 @@ def init_config(): config.__dict__[key] = load[key] if config.auth_service not in ['ptc', 'google']: - log.error("Invalid Auth service specified! ('ptc' or 'google')") - return None + log.error("Invalid Auth service specified! ('ptc' or 'google')") + return None return config + def main(): # log settings # log format - logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') # log level for http request class logging.getLogger("requests").setLevel(logging.WARNING) # log level for main pgoapi class @@ -134,7 +153,7 @@ def main(): position = get_pos_by_name(config.location) if not position: return - + if config.test: return @@ -153,11 +172,13 @@ def main(): # ---------------------- response_dict = api.get_player() - # apparently new dict has binary data in it, so formatting it with this method no longer works, pprint works here but there are other alternatives + # apparently new dict has binary data in it, so formatting it with this method no longer works, pprint works here but there are other alternatives # print('Response dictionary: \n\r{}'.format(json.dumps(response_dict, indent=2))) - print('Response dictionary: \n\r{}'.format(pprint.PrettyPrinter(indent=4).pformat(response_dict))) + print('Response dictionary: \n\r{}'.format( + pprint.PrettyPrinter(indent=4).pformat(response_dict))) find_poi(api, position[0], position[1]) + def find_poi(api, lat, lng): poi = {'pokemons': {}, 'forts': []} step_size = 0.0015 @@ -168,31 +189,43 @@ def find_poi(api, lat, lng): lng = coord['lng'] api.set_position(lat, lng, 0) - #get_cellid was buggy -> replaced through get_cell_ids from pokecli #timestamp gets computed a different way: cell_ids = get_cell_ids(lat, lng) - timestamps = [0,] * len(cell_ids) - response_dict = api.get_map_objects(latitude = util.f2i(lat), longitude = util.f2i(lng), since_timestamp_ms = timestamps, cell_id = cell_ids) + timestamps = [ + 0, + ] * len(cell_ids) + response_dict = api.get_map_objects( + latitude=util.f2i(lat), + longitude=util.f2i(lng), + since_timestamp_ms=timestamps, + cell_id=cell_ids) if (response_dict['responses']): if 'status' in response_dict['responses']['GET_MAP_OBJECTS']: - if response_dict['responses']['GET_MAP_OBJECTS']['status'] == 1: - for map_cell in response_dict['responses']['GET_MAP_OBJECTS']['map_cells']: + if response_dict['responses']['GET_MAP_OBJECTS'][ + 'status'] == 1: + for map_cell in response_dict['responses'][ + 'GET_MAP_OBJECTS']['map_cells']: if 'wild_pokemons' in map_cell: for pokemon in map_cell['wild_pokemons']: pokekey = get_key_from_pokemon(pokemon) - pokemon['hides_at'] = time.time() + pokemon['time_till_hidden_ms']/1000 + pokemon['hides_at'] = time.time( + ) + pokemon['time_till_hidden_ms'] / 1000 poi['pokemons'][pokekey] = pokemon # time.sleep(0.51) - # new dict, binary data - # print('POI dictionary: \n\r{}'.format(json.dumps(poi, indent=2))) - print('POI dictionary: \n\r{}'.format(pprint.PrettyPrinter(indent=4).pformat(poi))) + # new dict, binary data + # print('POI dictionary: \n\r{}'.format(json.dumps(poi, indent=2))) + print('POI dictionary: \n\r{}'.format( + pprint.PrettyPrinter(indent=4).pformat(poi))) print('Open this in a browser to see the path the spiral search took:') print_gmaps_dbug(coords) + def get_key_from_pokemon(pokemon): - return '{}-{}'.format(pokemon['spawn_point_id'], pokemon['pokemon_data']['pokemon_id']) + return '{}-{}'.format(pokemon['spawn_point_id'], + pokemon['pokemon_data']['pokemon_id']) + def print_gmaps_dbug(coords): url_string = 'http://maps.googleapis.com/maps/api/staticmap?size=400x400&path=' @@ -200,9 +233,10 @@ def print_gmaps_dbug(coords): url_string += '{},{}|'.format(coord['lat'], coord['lng']) print(url_string[:-1]) + def generate_spiral(starting_lat, starting_lng, step_size, step_limit): coords = [{'lat': starting_lat, 'lng': starting_lng}] - steps,x,y,d,m = 1, 0, 0, 1, 1 + steps, x, y, d, m = 1, 0, 0, 1, 1 rlow = 0.0 rhigh = 0.0005 @@ -224,5 +258,6 @@ def generate_spiral(starting_lat, starting_lng, step_size, step_limit): m = m + 1 return coords + if __name__ == '__main__': main() diff --git a/pgoapi/auth.py b/pgoapi/auth.py index 38fe30e8..7b8a74c8 100755 --- a/pgoapi/auth.py +++ b/pgoapi/auth.py @@ -29,8 +29,8 @@ from pgoapi.utilities import get_time, get_format_time_diff -class Auth: +class Auth: def __init__(self): self.log = logging.getLogger(__name__) @@ -38,21 +38,19 @@ def __init__(self): self._login = False - """ - oauth2 uses refresh tokens (which basically never expires) + """ + oauth2 uses refresh tokens (which basically never expires) to get an access_token which is only valid for a certain time) """ self._refresh_token = None self._access_token = None self._access_token_expiry = 0 - # TODO: can be removed - self._auth_token = None - """ - Pokemon Go uses internal tickets, like an internal + """ + Pokemon Go uses internal tickets, like an internal session to keep a user logged in over a certain time (30 minutes) """ - self._ticket_expire = None + self._ticket_expire = 0 self._ticket_start = None self._ticket_end = None @@ -66,39 +64,34 @@ def get_token(self): return self._access_token def has_ticket(self): - if self._ticket_expire and self._ticket_start and self._ticket_end: - return True - else: - return False + return (self._ticket_expire and self._ticket_start and self._ticket_end) def set_ticket(self, params): self._ticket_expire, self._ticket_start, self._ticket_end = params def is_new_ticket(self, new_ticket_time_ms): - if self._ticket_expire is None or new_ticket_time_ms > self._ticket_expire: - return True - else: - return False + return (not self._ticket_expire or new_ticket_time_ms > self._ticket_expire) def check_ticket(self): - if self.has_ticket(): - now_ms = get_time(ms = True) - if now_ms < (self._ticket_expire - 10000): - h, m, s = get_format_time_diff(now_ms, self._ticket_expire, True) - self.log.debug('Session Ticket still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, self._ticket_expire) - return True - else: - self.log.debug('Removed expired Session Ticket (%s < %s)', now_ms, self._ticket_expire) - self._ticket_expire, self._ticket_start, self._ticket_end = (None, None, None) - return False - else: + if not self.has_ticket(): return False + now_ms = get_time(ms=True) + if now_ms < (self._ticket_expire + 10000): + h, m, s = get_format_time_diff(now_ms, self._ticket_expire, True) + self.log.debug( + 'Session Ticket still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, self._ticket_expire) + return True + + self.log.debug('Removed expired Session Ticket (%s < %s)', + now_ms, self._ticket_expire) + self._ticket_expire, self._ticket_start, self._ticket_end = ( + 0, None, None) + return False def get_ticket(self): if self.check_ticket(): return (self._ticket_expire, self._ticket_start, self._ticket_end) - else: - return False + return False def user_login(self, username, password): raise NotImplementedError() @@ -106,28 +99,26 @@ def user_login(self, username, password): def set_refresh_token(self, username, password): raise NotImplementedError() - def get_access_token(self, force_refresh = False): + def get_access_token(self, force_refresh=False): raise NotImplementedError() - def check_access_token(self): - """ - Add few seconds to now so the token get refreshed - before it invalidates in the middle of the request - """ - now_s = get_time() + 120 - - if self._access_token is not None: - if self._access_token_expiry == 0: - self.log.debug('No Access Token Expiry found - assuming it is still valid!') - return True - elif self._access_token_expiry > now_s: - h, m, s = get_format_time_diff(now_s, self._access_token_expiry, False) - self.log.debug('Access Token still valid for further %02d:%02d:%02d hours (%s < %s)', h, m, s, now_s, self._access_token_expiry) - return True - else: - self.log.info('Access Token expired!') - return False - else: + if self._access_token is None: self.log.debug('No Access Token available!') - return False \ No newline at end of file + return False + + now_s = get_time() + if self._access_token_expiry == 0: + self.log.debug( + 'No Access Token Expiry found - assuming it is still valid!') + return True + elif self._access_token_expiry > now_s: + h, m, s = get_format_time_diff( + now_s, self._access_token_expiry, False) + self.log.debug( + 'Access Token still valid for further %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_s, self._access_token_expiry) + return True + + self.log.info('Access Token expired!') + return False diff --git a/pgoapi/auth_google.py b/pgoapi/auth_google.py index dab31f5d..45d4b512 100755 --- a/pgoapi/auth_google.py +++ b/pgoapi/auth_google.py @@ -32,10 +32,11 @@ from gpsoauth import perform_master_login, perform_oauth from six import string_types + class AuthGoogle(Auth): GOOGLE_LOGIN_ANDROID_ID = '9774d56d682e549c' - GOOGLE_LOGIN_SERVICE= 'audience:server:client_id:848232511240-7so421jotr2609rmqakceuu1luuq0ptb.apps.googleusercontent.com' + GOOGLE_LOGIN_SERVICE = 'audience:server:client_id:848232511240-7so421jotr2609rmqakceuu1luuq0ptb.apps.googleusercontent.com' GOOGLE_LOGIN_APP = 'com.nianticlabs.pokemongo' GOOGLE_LOGIN_CLIENT_SIG = '321187995bc7cdc2b5fc91b11a96e2baa8602c62' @@ -52,13 +53,20 @@ def set_proxy(self, proxy_config): def user_login(self, username, password): self.log.info('Google User Login for: {}'.format(username)) - if not isinstance(username, string_types) or not isinstance(password, string_types): - raise InvalidCredentialsException("Username/password not correctly specified") + if not isinstance(username, string_types) or not isinstance( + password, string_types): + raise InvalidCredentialsException( + "Username/password not correctly specified") - user_login = perform_master_login(username, password, self.GOOGLE_LOGIN_ANDROID_ID, proxy=self._proxy) + user_login = perform_master_login( + username, + password, + self.GOOGLE_LOGIN_ANDROID_ID, + proxy=self._proxy) if user_login and user_login.get('Error', None) == 'NeedsBrowser': - raise AuthGoogleTwoFactorRequiredException(user_login['Url'], user_login['ErrorDetail']) + raise AuthGoogleTwoFactorRequiredException( + user_login['Url'], user_login['ErrorDetail']) try: refresh_token = user_login.get('Token', None) @@ -79,7 +87,7 @@ def set_refresh_token(self, refresh_token): self.log.info('Google Refresh Token provided by user') self._refresh_token = refresh_token - def get_access_token(self, force_refresh = False): + def get_access_token(self, force_refresh=False): token_validity = self.check_access_token() if token_validity is True and force_refresh is False: @@ -91,8 +99,14 @@ def get_access_token(self, force_refresh = False): else: self.log.info('Request Google Access Token...') - token_data = perform_oauth(None, self._refresh_token, self.GOOGLE_LOGIN_ANDROID_ID, self.GOOGLE_LOGIN_SERVICE, self.GOOGLE_LOGIN_APP, - self.GOOGLE_LOGIN_CLIENT_SIG, proxy=self._proxy) + token_data = perform_oauth( + None, + self._refresh_token, + self.GOOGLE_LOGIN_ANDROID_ID, + self.GOOGLE_LOGIN_SERVICE, + self.GOOGLE_LOGIN_APP, + self.GOOGLE_LOGIN_CLIENT_SIG, + proxy=self._proxy) access_token = token_data.get('Auth', None) if access_token is not None: @@ -101,7 +115,8 @@ def get_access_token(self, force_refresh = False): self._login = True self.log.info('Google Access Token successfully received.') - self.log.debug('Google Access Token: %s...', self._access_token[:25]) + self.log.debug('Google Access Token: %s...', + self._access_token[:25]) return self._access_token else: self._access_token = None diff --git a/pgoapi/auth_ptc.py b/pgoapi/auth_ptc.py index afae6c87..8c9240e4 100755 --- a/pgoapi/auth_ptc.py +++ b/pgoapi/auth_ptc.py @@ -36,137 +36,169 @@ from pgoapi.utilities import get_time from pgoapi.exceptions import AuthException, AuthTimeoutException, InvalidCredentialsException -from requests.exceptions import RequestException, Timeout +from requests.exceptions import RequestException, Timeout, ProxyError, SSLError, ConnectionError -class AuthPtc(Auth): - PTC_LOGIN_URL1 = 'https://sso.pokemon.com/sso/oauth2.0/authorize?client_id=mobile-app_pokemon-go&redirect_uri=https%3A%2F%2Fwww.nianticlabs.com%2Fpokemongo%2Ferror' - PTC_LOGIN_URL2 = 'https://sso.pokemon.com/sso/login?service=http%3A%2F%2Fsso.pokemon.com%2Fsso%2Foauth2.0%2FcallbackAuthorize' - PTC_LOGIN_OAUTH = 'https://sso.pokemon.com/sso/oauth2.0/accessToken' - PTC_LOGIN_CLIENT_SECRET = 'w8ScCUXJQc6kXKw8FiOhd8Fixzht18Dq3PEVkUCP5ZPxtgyWsbTvWHFLm2wNY0JR' +class AuthPtc(Auth): - def __init__(self, username=None, password=None, user_agent=None, timeout=None): + def __init__(self, + username=None, + password=None, + user_agent=None, + timeout=None, + locale=None): Auth.__init__(self) self._auth_provider = 'ptc' - - self._session = requests.session() - self._session.headers = {'User-Agent': user_agent or 'pokemongo/1 CFNetwork/811.4.18 Darwin/16.5.0', 'Host': 'sso.pokemon.com', 'X-Unity-Version': '2017.1.2f1'} self._username = username self._password = password - self.timeout = timeout or 15 + self.timeout = timeout or 10 + self.locale = locale or 'en_US' + self.user_agent = user_agent or 'pokemongo/0 CFNetwork/893.14.2 Darwin/17.3.0' + + self._session = requests.session() + self._session.headers = { + 'Host': 'sso.pokemon.com', + 'Accept': '*/*', + 'Connection': 'keep-alive', + 'User-Agent': self.user_agent, + 'Accept-Language': self.locale.lower().replace('_', '-'), + 'Accept-Encoding': 'br, gzip, deflate', + 'X-Unity-Version': '2017.1.2f1' + } def set_proxy(self, proxy_config): self._session.proxies = proxy_config - def user_login(self, username=None, password=None, retry=True): + def user_login(self, username=None, password=None): self._username = username or self._username self._password = password or self._password - if not isinstance(self._username, string_types) or not isinstance(self._password, string_types): - raise InvalidCredentialsException("Username/password not correctly specified") + if not isinstance(self._username, string_types) or not isinstance( + self._password, string_types): + raise InvalidCredentialsException( + "Username/password not correctly specified") self.log.info('PTC User Login for: {}'.format(self._username)) self._session.cookies.clear() - now = get_time() try: - r = self._session.get(self.PTC_LOGIN_URL1, timeout=self.timeout) - except Timeout: - raise AuthTimeoutException('Auth GET timed out.') - except RequestException as e: - raise AuthException('Caught RequestException: {}'.format(e)) + now = get_time() - try: - data = r.json() + logout_params = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize' + } + r = self._session.get( + 'https://sso.pokemon.com/sso/logout', + params=logout_params, + timeout=self.timeout, + allow_redirects=False) + r.close() + + login_params_get = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize', + 'locale': self.locale + } + r = self._session.get( + 'https://sso.pokemon.com/sso/login', + params=login_params_get, + timeout=self.timeout) + + data = r.json(encoding='utf-8') + + assert 'lt' in data data.update({ '_eventId': 'submit', 'username': self._username, - 'password': self._password, + 'password': self._password }) - except (ValueError, AttributeError) as e: - self.log.error('PTC User Login Error - invalid JSON response: {}'.format(e)) - raise AuthException('Invalid JSON response: {}'.format(e)) - try: - r = self._session.post(self.PTC_LOGIN_URL2, data=data, timeout=self.timeout, allow_redirects=False) - except Timeout: - raise AuthTimeoutException('Auth POST timed out.') + login_params_post = { + 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize', + 'locale': self.locale + } + login_headers_post = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = self._session.post( + 'https://sso.pokemon.com/sso/login', + params=login_params_post, + headers=login_headers_post, + data=data, + timeout=self.timeout, + allow_redirects=False) + + try: + self._access_token = self._session.cookies['CASTGC'] + except (AttributeError, KeyError, TypeError): + try: + j = r.json(encoding='utf-8') + except ValueError as e: + raise AuthException('Unable to decode second response: {}'.format(e)) + try: + if j.get('error_code') == 'users.login.activation_required': + raise AuthException('Account email not verified.') + raise AuthException(j['errors'][0]) + except (AttributeError, IndexError, KeyError, TypeError) as e: + raise AuthException('Unable to login or get error information: {}'.format(e)) + + token_data = { + 'client_id': 'mobile-app_pokemon-go', + 'redirect_uri': 'https://www.nianticlabs.com/pokemongo/error', + 'client_secret': 'w8ScCUXJQc6kXKw8FiOhd8Fixzht18Dq3PEVkUCP5ZPxtgyWsbTvWHFLm2wNY0JR', + 'grant_type': 'refresh_token', + 'code': r.headers['Location'].split("ticket=")[1] + } + token_headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = self._session.post( + 'https://sso.pokemon.com/sso/oauth2.0/accessToken', + headers=token_headers, + data=token_data, + timeout=self.timeout) + r.close() + + profile_data = { + 'access_token': self._access_token, + 'client_id': 'mobile-app_pokemon-go', + 'locale': self.locale + } + profile_headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + r = self._session.post( + 'https://sso.pokemon.com/sso/oauth2.0/profile', + headers=profile_headers, + data=profile_data, + timeout=self.timeout) + r.close() + + except (ProxyError, SSLError, ConnectionError) as e: + raise AuthException('Proxy connection error during user_login: {}'.format(e)) + except Timeout as e: + raise AuthTimeoutException('user_login timeout') except RequestException as e: raise AuthException('Caught RequestException: {}'.format(e)) + except (AssertionError, TypeError, ValueError) as e: + raise AuthException('Invalid initial JSON response: {}'.format(e)) - try: - qs = parse_qs(urlsplit(r.headers['Location'])[3]) - self._refresh_token = qs.get('ticket')[0] - except Exception as e: - raise AuthException('Could not retrieve token! {}'.format(e)) - - self._access_token = self._session.cookies.get('CASTGC') if self._access_token: self._login = True - self._access_token_expiry = int(now) + 7200 + self._access_token_expiry = now + 7195.0 self.log.info('PTC User Login successful.') - elif self._refresh_token and retry: - self.get_access_token() - else: - self._login = False - raise AuthException("Could not retrieve a PTC Access Token") - return self._login + return self._login - def set_refresh_token(self, refresh_token): - self.log.info('PTC Refresh Token provided by user') - self._refresh_token = refresh_token + self._login = False + raise AuthException("Could not retrieve a PTC Access Token") - def get_access_token(self, force_refresh=False): - token_validity = self.check_access_token() - if token_validity is True and force_refresh is False: + def get_access_token(self, force_refresh=False): + if not force_refresh and self.check_access_token(): self.log.debug('Using cached PTC Access Token') return self._access_token - else: - if force_refresh: - self.log.info('Forced request of PTC Access Token!') - else: - self.log.info('Request PTC Access Token...') - data = { - 'client_id': 'mobile-app_pokemon-go', - 'redirect_uri': 'https://www.nianticlabs.com/pokemongo/error', - 'client_secret': self.PTC_LOGIN_CLIENT_SECRET, - 'grant_type': 'refresh_token', - 'code': self._refresh_token, - } - - try: - r = self._session.post(self.PTC_LOGIN_OAUTH, data=data, timeout=self.timeout) - except Timeout: - raise AuthTimeoutException('Auth POST timed out.') - except RequestException as e: - raise AuthException('Caught RequestException: {}'.format(e)) - - token_data = parse_qs(r.text) - - access_token = token_data.get('access_token') - if access_token is not None: - self._access_token = access_token[0] - - # set expiration to an hour less than value received because Pokemon OAuth - # login servers return an access token with an explicit expiry time of - # three hours, however, the token stops being valid after two hours. - # See issue #86 - expires = int(token_data.get('expires', [0])[0]) - 3600 - if expires > 0: - self._access_token_expiry = expires + get_time() - else: - self._access_token_expiry = 0 - - self._login = True - - self.log.info('PTC Access Token successfully retrieved.') - self.log.debug('PTC Access Token: {}'.format(self._access_token)) - else: - self._access_token = None - self._login = False - if force_refresh: - self.log.info('Reauthenticating with refresh token failed, using credentials instead.') - return self.user_login(retry=False) - raise AuthException("Could not retrieve a PTC Access Token") + self._access_token = None + self._ticket_expire = 0 + self._login = False + self.user_login() + return self._access_token diff --git a/pgoapi/exceptions.py b/pgoapi/exceptions.py index af432a65..8b0edee2 100755 --- a/pgoapi/exceptions.py +++ b/pgoapi/exceptions.py @@ -27,9 +27,11 @@ class PgoapiError(Exception): """Any custom exception in this module""" + class HashServerException(PgoapiError): """Parent class of all hashing server errors""" + class TimeoutException(PgoapiError): """Raised when a request times out.""" @@ -37,9 +39,11 @@ class TimeoutException(PgoapiError): class AuthException(PgoapiError): """Raised when logging in fails""" + class AuthTimeoutException(AuthException, TimeoutException): """Raised when an auth request times out.""" + class InvalidCredentialsException(AuthException, ValueError): """Raised when the username, password, or provider are empty/invalid""" @@ -47,6 +51,7 @@ class InvalidCredentialsException(AuthException, ValueError): class AuthTokenExpiredException(PgoapiError): """Raised when your auth token has expired (code 102)""" + class AuthGoogleTwoFactorRequiredException(Exception): def __init__(self, redirectUrl, message): self.redirectUrl = redirectUrl @@ -59,6 +64,7 @@ def __str__(self): class BadRequestException(PgoapiError): """Raised when HTTP code 400 is returned""" + class BadHashRequestException(BadRequestException): """Raised when hashing server returns code 400""" @@ -70,10 +76,13 @@ class BannedAccountException(PgoapiError): class MalformedResponseException(PgoapiError): """Raised when the response is empty or not in an expected format""" + class MalformedNianticResponseException(PgoapiError): """Raised when a Niantic response is empty or not in an expected format""" -class MalformedHashResponseException(MalformedResponseException, HashServerException): + +class MalformedHashResponseException(MalformedResponseException, + HashServerException): """Raised when the response from the hash server cannot be parsed.""" @@ -88,15 +97,20 @@ class NotLoggedInException(PgoapiError): class ServerBusyOrOfflineException(PgoapiError): """Raised when unable to establish a connection with a server""" + class NianticOfflineException(ServerBusyOrOfflineException): """Raised when unable to establish a conection with Niantic""" + class NianticTimeoutException(NianticOfflineException, TimeoutException): """Raised when an RPC request times out.""" -class HashingOfflineException(ServerBusyOrOfflineException, HashServerException): + +class HashingOfflineException(ServerBusyOrOfflineException, + HashServerException): """Raised when unable to establish a conection with the hashing server""" + class HashingTimeoutException(HashingOfflineException, TimeoutException): """Raised when a request to the hashing server times out.""" @@ -112,12 +126,16 @@ class PleaseInstallProtobufVersion3(PgoapiError): class ServerSideAccessForbiddenException(PgoapiError): """Raised when access to a server is forbidden""" + class NianticIPBannedException(ServerSideAccessForbiddenException): """Raised when Niantic returns a 403, meaning your IP is probably banned""" -class HashingForbiddenException(ServerSideAccessForbiddenException, HashServerException): + +class HashingForbiddenException(ServerSideAccessForbiddenException, + HashServerException): """Raised when the hashing server returns 403""" + class TempHashingBanException(HashingForbiddenException): """Raised when your IP is temporarily banned for sending too many requests with invalid keys.""" @@ -125,22 +143,28 @@ class TempHashingBanException(HashingForbiddenException): class ServerSideRequestThrottlingException(PgoapiError): """Raised when too many requests were made in a short period""" + class NianticThrottlingException(ServerSideRequestThrottlingException): """Raised when too many requests to Niantic were made in a short period""" -class HashingQuotaExceededException(ServerSideRequestThrottlingException, HashServerException): + +class HashingQuotaExceededException(ServerSideRequestThrottlingException, + HashServerException): """Raised when you exceed your hashing server quota""" class UnexpectedResponseException(PgoapiError): """Raised when an unhandled HTTP status code is received""" -class UnexpectedHashResponseException(UnexpectedResponseException, HashServerException): + +class UnexpectedHashResponseException(UnexpectedResponseException, + HashServerException): """Raised when an unhandled HTTP code is received from the hash server""" class ServerApiEndpointRedirectException(PgoapiError): """Raised when the API redirects you to another endpoint""" + def __init__(self): self._api_endpoint = None diff --git a/pgoapi/hash_engine.py b/pgoapi/hash_engine.py index e68e8881..88994d0f 100644 --- a/pgoapi/hash_engine.py +++ b/pgoapi/hash_engine.py @@ -4,12 +4,15 @@ def __init__(self): self.location_auth_hash = None self.request_hashes = [] - def hash(self, timestamp, latitude, longitude, altitude, authticket, sessiondata, requests): + def hash(self, timestamp, latitude, longitude, altitude, authticket, + sessiondata, requests): raise NotImplementedError() def get_location_hash(self): return self.location_hash + def get_location_auth_hash(self): return self.location_auth_hash + def get_request_hashes(self): return self.request_hashes diff --git a/pgoapi/hash_server.py b/pgoapi/hash_server.py index 9bafca41..dd4c91c1 100644 --- a/pgoapi/hash_server.py +++ b/pgoapi/hash_server.py @@ -9,51 +9,72 @@ from pgoapi.hash_engine import HashEngine from pgoapi.exceptions import BadHashRequestException, HashingOfflineException, HashingQuotaExceededException, HashingTimeoutException, MalformedHashResponseException, NoHashKeyException, TempHashingBanException, UnexpectedHashResponseException + class HashServer(HashEngine): _session = requests.session() _adapter = requests.adapters.HTTPAdapter(pool_maxsize=150, pool_block=True) _session.mount('https://', _adapter) _session.verify = True _session.headers.update({'User-Agent': 'Python pgoapi @pogodev'}) - endpoint = "https://pokehash.buddyauth.com/api/v157_5/hash" + endpoint = 'https://pokehash.buddyauth.com/api/v157_5/hash' status = {} def __init__(self, auth_token): if not auth_token: raise NoHashKeyException('Token not provided for hashing server.') - self.headers = {'content-type': 'application/json', 'Accept' : 'application/json', 'X-AuthToken' : auth_token} + self.headers = { + 'content-type': 'application/json', + 'Accept': 'application/json', + 'X-AuthToken': auth_token + } - def hash(self, timestamp, latitude, longitude, accuracy, authticket, sessiondata, requestslist): + def hash(self, timestamp, latitude, longitude, accuracy, authticket, + sessiondata, requestslist): self.location_hash = None self.location_auth_hash = None self.request_hashes = [] payload = { - 'Timestamp': timestamp, - 'Latitude64': unpack('>31))<<32)|cnt - self.log.debug("Incremented RPC Request ID: %s", reqid) - - return reqid - def decode_raw(self, raw): output = error = None try: - process = subprocess.Popen(['protoc', '--decode_raw'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + process = subprocess.Popen( + ['protoc', '--decode_raw'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + close_fds=True) output, error = process.communicate(raw) except (subprocess.SubprocessError, OSError): output = "Couldn't find protoc in your environment OR other issue..." @@ -116,7 +100,8 @@ def _make_rpc(self, endpoint, request_proto_plain): request_proto_serialized = request_proto_plain.SerializeToString() try: - http_response = self._session.post(endpoint, data=request_proto_serialized, timeout=30) + http_response = self._session.post( + endpoint, data=request_proto_serialized, timeout=30) except requests.exceptions.Timeout: raise NianticTimeoutException('RPC request timed out.') except requests.exceptions.ConnectionError as e: @@ -124,33 +109,44 @@ def _make_rpc(self, endpoint, request_proto_plain): return http_response - def request(self, endpoint, subrequests, platforms, player_position, use_dict = True): + def request(self, + endpoint, + subrequests, + platforms, + player_position, + use_dict=True): if not self._auth_provider or self._auth_provider.is_login() is False: raise NotLoggedInException() - self.request_proto = self.request_proto or self._build_main_request(subrequests, platforms, player_position) + self.request_proto = self.request_proto or self._build_main_request( + subrequests, platforms, player_position) response = self._make_rpc(endpoint, self.request_proto) - response_dict = self._parse_main_response(response, subrequests, use_dict) + response_dict = self._parse_main_response(response, subrequests, + use_dict) # some response validations if isinstance(response_dict, dict): if use_dict: status_code = response_dict.get('status_code') - if ('auth_ticket' in response_dict) and ('expire_timestamp_ms' in response_dict['auth_ticket']): + if ('auth_ticket' in response_dict) and ( + 'expire_timestamp_ms' in response_dict['auth_ticket']): ticket = response_dict['auth_ticket'] - self.check_authentication(ticket['expire_timestamp_ms'], ticket['start'], ticket['end']) + self.check_authentication(ticket['expire_timestamp_ms'], + ticket['start'], ticket['end']) else: status_code = response_dict['envelope'].status_code ticket = response_dict['envelope'].auth_ticket if ticket: - self.check_authentication(ticket.expire_timestamp_ms, ticket.start, ticket.end) - + self.check_authentication(ticket.expire_timestamp_ms, + ticket.start, ticket.end) + if status_code == 102: raise AuthTokenExpiredException elif status_code == 52: - raise NianticThrottlingException("Request throttled by server... slow down man") + raise NianticThrottlingException( + "Request throttled by server... slow down man") elif status_code == 53: api_url = response_dict.get('api_url') if api_url: @@ -172,17 +168,31 @@ def check_authentication(self, expire_timestamp_ms, start, end): h, m, s = get_format_time_diff(now_ms, expire_timestamp_ms, True) if had_ticket: - self.log.debug('Replacing old Session Ticket with new one valid for %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, expire_timestamp_ms) + self.log.debug( + 'Replacing old Session Ticket with new one valid for %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_ms, expire_timestamp_ms) else: - self.log.debug('Received Session Ticket valid for %02d:%02d:%02d hours (%s < %s)', h, m, s, now_ms, expire_timestamp_ms) + self.log.debug( + 'Received Session Ticket valid for %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_ms, expire_timestamp_ms) - def _build_main_request(self, subrequests, platforms, player_position=None): + def _build_main_request(self, subrequests, platforms, + player_position=None): self.log.debug('Generating main RPC request...') request = RequestEnvelope() request.status_code = 2 - request.request_id = self.get_rpc_id() - request.accuracy = random.choice((5, 5, 5, 5, 10, 10, 10, 30, 30, 50, 65, random.uniform(66,80))) + request.request_id = self.request_id + # 5: 43%, 10: 30%, 30: 5%, 50: 4%, 65: 10%, 200: 1%, float: 7% + request.accuracy = weighted_choice([ + (5, 43), + (10, 30), + (30, 5), + (50, 4), + (65, 10), + (200, 1), + (random.uniform(65, 200), 7) + ]) if player_position: request.latitude, request.longitude, altitude = player_position @@ -193,26 +203,31 @@ def _build_main_request(self, subrequests, platforms, player_position=None): ticket = self._auth_provider.get_ticket() if ticket: - self.log.debug('Found Session Ticket - using this instead of oauth token') + self.log.debug( + 'Found Session Ticket - using this instead of oauth token') request.auth_ticket.expire_timestamp_ms, request.auth_ticket.start, request.auth_ticket.end = ticket ticket_serialized = request.auth_ticket.SerializeToString() else: - self.log.debug('No Session Ticket found - using OAUTH Access Token') - request.auth_info.provider = self._auth_provider.get_name() - request.auth_info.token.contents = self._auth_provider.get_access_token() + self.log.debug( + 'No Session Ticket found - using OAUTH Access Token') + auth_provider = self._auth_provider + request.auth_info.provider = auth_provider.get_name() + request.auth_info.token.contents = auth_provider.get_access_token() request.auth_info.token.unknown2 = self.token2 - ticket_serialized = request.auth_info.SerializeToString() #Sig uses this when no auth_ticket available + # Sig uses this when no auth_ticket available. + ticket_serialized = request.auth_info.SerializeToString() sig = Signature() - sig.session_hash = self.session_hash + sig.session_hash = self.state.session_hash sig.timestamp = get_time(ms=True) - sig.timestamp_since_start = get_time(ms=True) - RpcApi.START_TIME - if sig.timestamp_since_start < 5000: - sig.timestamp_since_start = random.randint(5000, 8000) + sig.timestamp_since_start = get_time(ms=True) - self.start_time - self._hash_engine.hash(sig.timestamp, request.latitude, request.longitude, request.accuracy, ticket_serialized, sig.session_hash, request.requests) + self._hash_engine.hash(sig.timestamp, request.latitude, + request.longitude, request.accuracy, + ticket_serialized, sig.session_hash, + request.requests) sig.location_hash1 = self._hash_engine.get_location_auth_hash() sig.location_hash2 = self._hash_engine.get_location_hash() for req_hash in self._hash_engine.get_request_hashes(): @@ -221,52 +236,91 @@ def _build_main_request(self, subrequests, platforms, player_position=None): loc = sig.location_fix.add() sen = sig.sensor_info.add() - sen.timestamp_snapshot = random.randint(sig.timestamp_since_start - 5000, sig.timestamp_since_start - 100) - loc.timestamp_snapshot = random.randint(sig.timestamp_since_start - 5000, sig.timestamp_since_start - 1000) + sen.timestamp_snapshot = sig.timestamp_since_start - int(random.triangular(93, 4900, 3000)) + loc.timestamp_snapshot = sig.timestamp_since_start - int(random.triangular(320, 3000, 1000)) - loc.provider = random.choice(('network', 'network', 'network', 'network', 'fused')) + loc.provider = 'fused' loc.latitude = request.latitude loc.longitude = request.longitude - loc.altitude = altitude or random.triangular(300, 400, 350) + loc.altitude = altitude or random.uniform(150, 250) - if random.random() > .95: - # no reading for roughly 1 in 20 updates + if random.random() > .85: + # no reading for roughly 1 in 7 updates loc.course = -1 loc.speed = -1 else: - self.course = random.triangular(0, 360, self.course) - loc.course = self.course - loc.speed = random.triangular(0.2, 4.25, 1) + loc.course = self.state.course + loc.speed = random.triangular(0.25, 9.7, 8.2) loc.provider_status = 3 loc.location_type = 1 - if request.accuracy >= 65: - loc.vertical_accuracy = random.triangular(35, 100, 65) - loc.horizontal_accuracy = random.choice((request.accuracy, 65, 65, random.uniform(66,80), 200)) + if isinstance(request.accuracy, float): + loc.horizontal_accuracy = weighted_choice([ + (request.accuracy, 50), + (65, 40), + (200, 10) + ]) + loc.vertical_accuracy = weighted_choice([ + (random.uniform(10, 96), 50), + (10, 34), + (12, 5), + (16, 3), + (24, 4), + (32, 2), + (48, 1), + (96, 1) + ]) else: - if request.accuracy > 10: - loc.vertical_accuracy = random.choice((24, 32, 48, 48, 64, 64, 96, 128)) - else: - loc.vertical_accuracy = random.choice((3, 4, 6, 6, 8, 12, 24)) loc.horizontal_accuracy = request.accuracy - - sen.linear_acceleration_x = random.triangular(-3, 1, 0) - sen.linear_acceleration_y = random.triangular(-2, 3, 0) - sen.linear_acceleration_z = random.triangular(-4, 2, 0) - sen.magnetic_field_x = random.triangular(-50, 50, 0) - sen.magnetic_field_y = random.triangular(-60, 50, -5) - sen.magnetic_field_z = random.triangular(-60, 40, -30) - sen.magnetic_field_accuracy = random.choice((-1, 1, 1, 2, 2, 2, 2)) - sen.attitude_pitch = random.triangular(-1.5, 1.5, 0.2) - sen.attitude_yaw = random.uniform(-3, 3) - sen.attitude_roll = random.triangular(-2.8, 2.5, 0.25) - sen.rotation_rate_x = random.triangular(-6, 4, 0) - sen.rotation_rate_y = random.triangular(-5.5, 5, 0) - sen.rotation_rate_z = random.triangular(-5, 3, 0) - sen.gravity_x = random.triangular(-1, 1, 0.15) - sen.gravity_y = random.triangular(-1, 1, -.2) - sen.gravity_z = random.triangular(-1, .7, -0.8) + if request.accuracy >= 10: + loc.vertical_accuracy = weighted_choice([ + (6, 4), + (8, 34), + (10, 35), + (12, 11), + (16, 4), + (24, 8), + (32, 3), + (48, 1) + ]) + else: + loc.vertical_accuracy = weighted_choice([ + (3, 15), + (4, 39), + (6, 14), + (8, 13), + (10, 14), + (12, 5) + ]) + + sen.magnetic_field_accuracy = weighted_choice([ + (-1, 8), + (0, 2), + (1, 42), + (2, 48) + ]) + if sen.magnetic_field_accuracy == -1: + sen.magnetic_field_x = 0 + sen.magnetic_field_y = 0 + sen.magnetic_field_z = 0 + else: + sen.magnetic_field_x = self.state.magnetic_field_x + sen.magnetic_field_y = self.state.magnetic_field_y + sen.magnetic_field_z = self.state.magnetic_field_z + + sen.linear_acceleration_x = random.triangular(-1.5, 2.5, 0) + sen.linear_acceleration_y = random.triangular(-1.2, 1.4, 0) + sen.linear_acceleration_z = random.triangular(-1.4, .9, 0) + sen.attitude_pitch = random.triangular(-1.56, 1.57, 0.475) + sen.attitude_yaw = random.triangular(-1.56, 3.14, .1) + sen.attitude_roll = random.triangular(-3.14, 3.14, 0) + sen.rotation_rate_x = random.triangular(-3.2, 3.52, 0) + sen.rotation_rate_y = random.triangular(-3.1, 4.88, 0) + sen.rotation_rate_z = random.triangular(-6, 3.7, 0) + sen.gravity_x = random.triangular(-1, 1, 0.01) + sen.gravity_y = random.triangular(-1, 1, -.4) + sen.gravity_z = random.triangular(-1, 1, -.4) sen.status = 3 sig.unknown25 = 4500779412463383546 @@ -287,14 +341,15 @@ def _build_main_request(self, subrequests, platforms, player_position=None): plat8 = request.platform_requests.add() plat8.type = 8 plat8.request_message = plat_eight.SerializeToString() - + sig_request = SendEncryptedSignatureRequest() - sig_request.encrypted_signature = pycrypt(signature_proto, sig.timestamp_since_start) + sig_request.encrypted_signature = pycrypt(signature_proto, + sig.timestamp_since_start) plat = request.platform_requests.add() plat.type = 6 plat.request_message = sig_request.SerializeToString() - request.ms_since_last_locationfix = int(random.triangular(300, 30000, 10000)) + request.ms_since_last_locationfix = sig.timestamp_since_start - loc.timestamp_snapshot self.log.debug('Generated protobuf request: \n\r%s', request) @@ -307,13 +362,12 @@ def _needsPtr8(self, requests): rtype, _ = requests[0] # GetMapObjects or GetPlayer: 50% # Encounter: 10% - # Others: 3% + # Others: 3% if ((rtype in (2, 106) and randval > 0.5) - or (rtype == 102 and randval > 0.9) - or randval > 0.97): + or (rtype == 102 and randval > 0.9) or randval > 0.97): return True return False - + def _build_sub_requests(self, mainrequest, subrequest_list): self.log.debug('Generating sub RPC requests...') @@ -321,7 +375,9 @@ def _build_sub_requests(self, mainrequest, subrequest_list): if params: entry_name = RequestType.Name(entry_id) proto_name = entry_name.lower() + '_message' - bytes = self._get_proto_bytes('pogoprotos.networking.requests.messages.', proto_name, params) + bytes = self._get_proto_bytes( + 'pogoprotos.networking.requests.messages.', proto_name, + params) subrequest = mainrequest.requests.add() subrequest.request_type = entry_id @@ -342,7 +398,9 @@ def _build_platform_requests(self, mainrequest, platform_list): if entry_name == 'UNKNOWN_PTR_8': entry_name = 'UNKNOWN_PTR8' proto_name = entry_name.lower() + '_request' - bytes = self._get_proto_bytes('pogoprotos.networking.platform.requests.', proto_name, params) + bytes = self._get_proto_bytes( + 'pogoprotos.networking.platform.requests.', proto_name, + params) platform = mainrequest.platform_requests.add() platform.type = entry_id @@ -353,7 +411,6 @@ def _build_platform_requests(self, mainrequest, platform_list): platform.type = entry_id return mainrequest - def _get_proto_bytes(self, path, name, entry_content): proto_classname = path + name + '_pb2.' + name @@ -370,14 +427,18 @@ def _get_proto_bytes(self, path, name, entry_content): r = getattr(proto, key) r.append(i) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, i, proto_name, e) + self.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, i, proto_classname, e) elif isinstance(value, dict): for k in value.keys(): try: r = getattr(proto, key) setattr(r, k, value[k]) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, str(value), proto_name, e) + self.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, str(value), proto_classname, e) else: try: setattr(proto, key, value) @@ -387,23 +448,30 @@ def _get_proto_bytes(self, path, name, entry_content): r = getattr(proto, key) r.append(value) except Exception as e: - self.log.warning('Argument %s with value %s unknown inside %s (Exception: %s)', key, value, proto_name, e) + self.log.warning( + 'Argument %s with value %s unknown inside %s (Exception: %s)', + key, value, proto_classname, e) return proto.SerializeToString() - def _parse_main_response(self, response_raw, subrequests, use_dict = True): + def _parse_main_response(self, response_raw, subrequests, use_dict=True): self.log.debug('Parsing main RPC response...') if response_raw.status_code == 400: raise BadRequestException("400: Bad Request") if response_raw.status_code == 403: - raise NianticIPBannedException("Seems your IP Address is banned or something else went badly wrong...") + raise NianticIPBannedException( + "Seems your IP Address is banned or something else went badly wrong..." + ) elif response_raw.status_code in (502, 503, 504): - raise NianticOfflineException('{} Server Error'.format(response_raw.status_code)) + raise NianticOfflineException( + '{} Server Error'.format(response_raw.status_code)) elif response_raw.status_code != 200: - error = 'Unexpected HTTP server response - needs 200 got {}'.format(response_raw.status_code) + error = 'Unexpected HTTP server response - needs 200 got {}'.format( + response_raw.status_code) self.log.warning(error) - self.log.debug('HTTP output: \n%s', response_raw.content.decode('utf-8')) + self.log.debug('HTTP output: \n%s', + response_raw.content.decode('utf-8')) raise UnexpectedResponseException(error) if not response_raw.content: @@ -415,11 +483,15 @@ def _parse_main_response(self, response_raw, subrequests, use_dict = True): response_proto.ParseFromString(response_raw.content) except message.DecodeError as e: self.log.error('Could not parse response: %s', e) - raise MalformedNianticResponseException('Could not decode response.') + raise MalformedNianticResponseException( + 'Could not decode response.') - self.log.debug('Protobuf structure of rpc response:\n\r%s', response_proto) + self.log.debug('Protobuf structure of rpc response:\n\r%s', + response_proto) try: - self.log.debug('Decode raw over protoc (protoc has to be in your PATH):\n\r%s', self.decode_raw(response_raw.content).decode('utf-8')) + self.log.debug( + 'Decode raw over protoc (protoc has to be in your PATH):\n\r%s', + self.decode_raw(response_raw.content).decode('utf-8')) except Exception: self.log.debug('Error during protoc parsing - ignored.') @@ -431,17 +503,23 @@ def _parse_main_response(self, response_raw, subrequests, use_dict = True): response_proto_dict = {'envelope': response_proto} if not response_proto_dict: - raise MalformedNianticResponseException('Could not convert protobuf to dict.') - - response_proto_dict = self._parse_sub_responses(response_proto, subrequests, response_proto_dict, use_dict) - - #It can't be done before + raise MalformedNianticResponseException( + 'Could not convert protobuf to dict.') + + response_proto_dict = self._parse_sub_responses( + response_proto, subrequests, response_proto_dict, use_dict) + + # It can't be done before. if not use_dict: del response_proto_dict['envelope'].returns[:] return response_proto_dict - def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_dict, use_dict = True): + def _parse_sub_responses(self, + response_proto, + subrequests_list, + response_proto_dict, + use_dict=True): self.log.debug('Parsing sub RPC responses...') response_proto_dict['responses'] = {} @@ -462,9 +540,10 @@ def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_ subresponse_return = None try: subresponse_extension = self.get_class(proto_classname)() - except Exception as e: + except Exception: subresponse_extension = None - error = 'Protobuf definition for {} not found'.format(proto_classname) + error = 'Protobuf definition for {} not found'.format( + proto_classname) subresponse_return = error self.log.warning(error) @@ -473,11 +552,13 @@ def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_ subresponse_extension.ParseFromString(subresponse) if use_dict: - subresponse_return = protobuf_to_dict(subresponse_extension) + subresponse_return = protobuf_to_dict( + subresponse_extension) else: subresponse_return = subresponse_extension except Exception: - error = "Protobuf definition for {} seems not to match".format(proto_classname) + error = "Protobuf definition for {} seems not to match".format( + proto_classname) subresponse_return = error self.log.warning(error) @@ -485,3 +566,33 @@ def _parse_sub_responses(self, response_proto, subrequests_list, response_proto_ i += 1 return response_proto_dict + + +# Original by Noctem. +class RpcState: + def __init__(self): + self.session_hash = os.urandom(16) + self.mag_x_min = random.uniform(-80, 60) + self.mag_x_max = self.mag_x_min + 20 + self.mag_y_min = random.uniform(-120, 90) + self.mag_y_max = self.mag_y_min + 30 + self.mag_z_min = random.uniform(-70, 40) + self.mag_z_max = self.mag_y_min + 15 + self._course = random.uniform(0, 359.99) + + @property + def magnetic_field_x(self): + return random.uniform(self.mag_x_min, self.mag_x_max) + + @property + def magnetic_field_y(self): + return random.uniform(self.mag_y_min, self.mag_y_max) + + @property + def magnetic_field_z(self): + return random.uniform(self.mag_z_min, self.mag_z_max) + + @property + def course(self): + self._course = random.triangular(0, 359.99, self._course) + return self._course diff --git a/pgoapi/utilities.py b/pgoapi/utilities.py index 709aecdc..1181f0cc 100755 --- a/pgoapi/utilities.py +++ b/pgoapi/utilities.py @@ -21,13 +21,13 @@ import time import struct +import random import logging from json import JSONEncoder from binascii import unhexlify # other stuff -from google.protobuf.internal import encoder from geopy.geocoders import GoogleV3 from s2sphere import LatLng, Angle, Cap, RegionCoverer, math @@ -37,25 +37,26 @@ def f2i(float): - return struct.unpack(' 1500: radius = 1500 # radius = 1500 is max allowed by the server - region = Cap.from_axis_angle(LatLng.from_degrees(lat, long).to_point(), Angle.from_degrees(360*radius/(2*math.pi*EARTH_RADIUS))) + region = Cap.from_axis_angle( + LatLng.from_degrees(lat, long).to_point(), + Angle.from_degrees(360 * radius / (2 * math.pi * EARTH_RADIUS))) coverer = RegionCoverer() coverer.min_level = 15 coverer.max_level = 15 @@ -90,14 +94,14 @@ def get_cell_ids(lat, long, radius=500): return sorted([x.id() for x in cells]) -def get_time(ms = False): +def get_time(ms=False): if ms: return int(time.time() * 1000) else: return int(time.time()) -def get_format_time_diff(low, high, ms = True): +def get_format_time_diff(low, high, ms=True): diff = (high - low) if ms: m, s = divmod(diff / 1000, 60) @@ -113,3 +117,14 @@ def parse_api_endpoint(api_url): api_url = 'https://{}/rpc'.format(api_url) return api_url + + +def weighted_choice(choices): + total = sum(w for c, w in choices) + r = random.uniform(0, total) + upto = 0 + for c, w in choices: + if upto + w >= r: + return c + upto += w + assert False, "Shouldn't get here" diff --git a/scripts/accept-tos.py b/scripts/accept-tos.py index 489f491d..3dd4e502 100644 --- a/scripts/accept-tos.py +++ b/scripts/accept-tos.py @@ -1,6 +1,5 @@ #!/usr/bin/python # -*- coding: utf-8 -*- - """accept-tos.py: Example script to accept in-game Terms of Service""" from pgoapi import PGoApi @@ -11,16 +10,21 @@ import time import threading + def accept_tos(username, password, lat, lon, alt, auth='ptc'): - api = PGoApi() - api.set_position(lat, lon, alt) - api.login(auth, username, password) - time.sleep(2) - req = api.create_request() - req.mark_tutorial_complete(tutorials_completed = 0, send_marketing_emails = False, send_push_notifications = False) - response = req.call() - print('Accepted Terms of Service for {}'.format(username)) - #print('Response dictionary: \r\n{}'.format(pprint.PrettyPrinter(indent=4).pformat(response))) + api = PGoApi() + api.set_position(lat, lon, alt) + api.login(auth, username, password) + time.sleep(2) + req = api.create_request() + req.mark_tutorial_complete( + tutorials_completed=0, + send_marketing_emails=False, + send_push_notifications=False) + response = req.call() + print('Accepted Terms of Service for {}'.format(username)) + #print('Response dictionary: \r\n{}'.format(pprint.PrettyPrinter(indent=4).pformat(response))) + """auth service defaults to ptc if not given""" diff --git a/scripts/pokecli.py b/scripts/pokecli.py index 77fcfd60..07b2bcae 100755 --- a/scripts/pokecli.py +++ b/scripts/pokecli.py @@ -40,28 +40,38 @@ from pgoapi import pgoapi from pgoapi import utilities as util - log = logging.getLogger(__name__) + def init_config(): parser = argparse.ArgumentParser() config_file = "config.json" # If config file exists, load variables from json - load = {} + load = {} if os.path.isfile(config_file): with open(config_file) as data: load.update(json.load(data)) # Read passed in Arguments required = lambda x: not x in load - parser.add_argument("-a", "--auth_service", help="Auth Service ('ptc' or 'google')", + parser.add_argument( + "-a", + "--auth_service", + help="Auth Service ('ptc' or 'google')", required=required("auth_service")) - parser.add_argument("-u", "--username", help="Username", required=required("username")) + parser.add_argument( + "-u", "--username", help="Username", required=required("username")) parser.add_argument("-p", "--password", help="Password") - parser.add_argument("-l", "--location", help="Location", required=required("location")) - parser.add_argument("-d", "--debug", help="Debug Mode", action='store_true') - parser.add_argument("-t", "--test", help="Only parse the specified location", action='store_true') + parser.add_argument( + "-l", "--location", help="Location", required=required("location")) + parser.add_argument( + "-d", "--debug", help="Debug Mode", action='store_true') + parser.add_argument( + "-t", + "--test", + help="Only parse the specified location", + action='store_true') parser.add_argument("-px", "--proxy", help="Specify a socks5 proxy url") parser.set_defaults(DEBUG=False, TEST=False) config = parser.parse_args() @@ -72,12 +82,14 @@ def init_config(): config.__dict__[key] = str(load[key]) if config.__dict__["password"] is None: - log.info("Secure Password Input (if there is no password prompt, use --password ):") + log.info( + "Secure Password Input (if there is no password prompt, use --password ):" + ) config.__dict__["password"] = getpass.getpass() if config.auth_service not in ['ptc', 'google']: - log.error("Invalid Auth service specified! ('ptc' or 'google')") - return None + log.error("Invalid Auth service specified! ('ptc' or 'google')") + return None return config @@ -85,7 +97,9 @@ def init_config(): def main(): # log settings # log format - logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s [%(module)10s] [%(levelname)5s] %(message)s') # log level for http request class logging.getLogger("requests").setLevel(logging.WARNING) # log level for main pgoapi class @@ -102,7 +116,6 @@ def main(): logging.getLogger("pgoapi").setLevel(logging.DEBUG) logging.getLogger("rpc_api").setLevel(logging.DEBUG) - # instantiate pgoapi api = pgoapi.PGoApi() if config.proxy: @@ -121,15 +134,30 @@ def main(): # new authentication initialitation if config.proxy: - api.set_authentication(provider = config.auth_service, username = config.username, password = config.password, proxy_config = {'http': config.proxy, 'https': config.proxy}) + api.set_authentication( + provider=config.auth_service, + username=config.username, + password=config.password, + proxy_config={'http': config.proxy, + 'https': config.proxy}) else: - api.set_authentication(provider = config.auth_service, username = config.username, password = config.password) + api.set_authentication( + provider=config.auth_service, + username=config.username, + password=config.password) # print get maps object cell_ids = util.get_cell_ids(position[0], position[1]) - timestamps = [0,] * len(cell_ids) - response_dict = api.get_map_objects(latitude =position[0], longitude = position[1], since_timestamp_ms = timestamps, cell_id = cell_ids) - print('Response dictionary (get_player): \n\r{}'.format(pprint.PrettyPrinter(indent=4).pformat(response_dict))) + timestamps = [ + 0, + ] * len(cell_ids) + response_dict = api.get_map_objects( + latitude=position[0], + longitude=position[1], + since_timestamp_ms=timestamps, + cell_id=cell_ids) + print('Response dictionary (get_player): \n\r{}'.format( + pprint.PrettyPrinter(indent=4).pformat(response_dict))) if __name__ == '__main__': diff --git a/setup.py b/setup.py index 618685f6..d850e408 100755 --- a/setup.py +++ b/setup.py @@ -10,12 +10,12 @@ reqs = [str(ir.req) for ir in install_reqs] -setup(name='pgoapi', - author = 'tjado', - description = 'Pokemon Go API lib', - version = '1.2.0', - url = 'https://github.com/pogodevorg/pgoapi', - download_url = "https://github.com/pogodevorg/pgoapi/releases", - packages = find_packages(), - install_requires = reqs - ) +setup( + name='pgoapi', + author='tjado', + description='Pokemon Go API lib', + version='1.2.0', + url='https://github.com/sebastienvercammen/pgoapi', + download_url="https://github.com/sebastienvercammen/pgoapi/releases", + packages=find_packages(), + install_requires=reqs)