From 70fb46cde6065c7069be205ab5a33bd0dda84052 Mon Sep 17 00:00:00 2001 From: Michael Krug Date: Fri, 22 Dec 2017 01:19:18 +0100 Subject: [PATCH 1/7] update auth class, update PTC login flow, integrate RPC_ID generation --- pgoapi/auth.py | 91 +++++------ pgoapi/auth_google.py | 33 ++-- pgoapi/auth_ptc.py | 226 +++++++++++++++------------ pgoapi/exceptions.py | 34 ++++- pgoapi/hash_engine.py | 5 +- pgoapi/hash_server.py | 60 +++++--- pgoapi/pgoapi.py | 172 +++++++++++++++------ pgoapi/rpc_api.py | 345 ++++++++++++++++++++++++++++-------------- pgoapi/utilities.py | 35 +++-- 9 files changed, 644 insertions(+), 357 deletions(-) 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" From 7a7ebc10850f9e799069630e14c5a65537377575 Mon Sep 17 00:00:00 2001 From: friscoMad Date: Tue, 19 Dec 2017 11:45:59 -0500 Subject: [PATCH 2/7] Remove examples and scripts as they are not working anymore Removed empty interface hash_engine Made Hash_server static no need to instantiate Removed unused requirements Removed dict usage Use single pool for all rpcs Store state in a single object and made hash_server and rpc_api static Improved logic Enable passing proxy and hash_key in the call Add support for disabling session reuse and changing hashing endpoint Don't store session in auth as it is not needed Reduce rpc pool size to 150 (that should be enough for 500-600 workers) Add adapter mounts for all protocols as proxies uses the pool by their own urls Remove unused function --- examples/pogo-optimizer/data/moves.json | 1 - examples/pogo-optimizer/data/pokemon.json | 1 - examples/pogo-optimizer/pogo-optimizer-cli.py | 157 -------- examples/spiral_poi_search.py | 228 ------------ pgoapi/auth.py | 18 + pgoapi/auth_google.py | 2 - pgoapi/auth_ptc.py | 34 +- pgoapi/hash_engine.py | 18 - pgoapi/hash_server.py | 57 ++- pgoapi/pgoapi.py | 109 +++--- pgoapi/rpc_api.py | 345 +++++++++--------- pgoapi/utilities.py | 7 - requirements.txt | 4 - scripts/accept-tos.py | 29 -- scripts/pokecli.py | 136 ------- 15 files changed, 272 insertions(+), 874 deletions(-) delete mode 100644 examples/pogo-optimizer/data/moves.json delete mode 100644 examples/pogo-optimizer/data/pokemon.json delete mode 100644 examples/pogo-optimizer/pogo-optimizer-cli.py delete mode 100755 examples/spiral_poi_search.py delete mode 100644 pgoapi/hash_engine.py delete mode 100644 scripts/accept-tos.py delete mode 100755 scripts/pokecli.py diff --git a/examples/pogo-optimizer/data/moves.json b/examples/pogo-optimizer/data/moves.json deleted file mode 100644 index 73a18c62..00000000 --- a/examples/pogo-optimizer/data/moves.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":13,"type":"Normal","name":"Wrap"},{"id":14,"type":"Normal","name":"Hyper Beam"},{"id":16,"type":"Dark","name":"Dark Pulse"},{"id":18,"type":"Poison","name":"Sludge"},{"id":20,"type":"Normal","name":"Vice Grip"},{"id":21,"type":"Fire","name":"Flame Wheel"},{"id":22,"type":"Bug","name":"Megahorn"},{"id":24,"type":"Fire","name":"Flamethrower"},{"id":26,"type":"Ground","name":"Dig"},{"id":28,"type":"Fighting","name":"Cross Chop"},{"id":30,"type":"Psychic","name":"Psybeam"},{"id":31,"type":"Ground","name":"Earthquake"},{"id":32,"type":"Rock","name":"Stone Edge"},{"id":33,"type":"Ice","name":"Ice Punch"},{"id":34,"type":"Psychic","name":"Heart Stamp"},{"id":35,"type":"Electric","name":"Discharge"},{"id":36,"type":"Steel","name":"Flash Cannon"},{"id":38,"type":"Flying","name":"Drill Peck"},{"id":39,"type":"Ice","name":"Ice Beam"},{"id":40,"type":"Ice","name":"Blizzard"},{"id":42,"type":"Fire","name":"Heat Wave"},{"id":45,"type":"Flying","name":"Aerial Ace"},{"id":46,"type":"Ground","name":"Drill Run"},{"id":47,"type":"Grass","name":"Petal Blizzard"},{"id":48,"type":"Grass","name":"Mega Drain"},{"id":49,"type":"Bug","name":"Bug Buzz"},{"id":50,"type":"Poison","name":"Poison Fang"},{"id":51,"type":"Dark","name":"Night Slash"},{"id":53,"type":"Water","name":"Bubble Beam"},{"id":54,"type":"Fighting","name":"Submission"},{"id":56,"type":"Fighting","name":"Low Sweep"},{"id":57,"type":"Water","name":"Aqua Jet"},{"id":58,"type":"Water","name":"Aqua Tail"},{"id":59,"type":"Grass","name":"Seed Bomb"},{"id":60,"type":"Psychic","name":"Psyshock"},{"id":62,"type":"Rock","name":"Ancient Power"},{"id":63,"type":"Rock","name":"Rock Tomb"},{"id":64,"type":"Rock","name":"Rock Slide"},{"id":65,"type":"Rock","name":"Power Gem"},{"id":66,"type":"Ghost","name":"Shadow Sneak"},{"id":67,"type":"Ghost","name":"Shadow Punch"},{"id":69,"type":"Ghost","name":"Ominous Wind"},{"id":70,"type":"Ghost","name":"Shadow Ball"},{"id":72,"type":"Steel","name":"Magnet Bomb"},{"id":74,"type":"Steel","name":"Iron Head"},{"id":75,"type":"Electric","name":"Parabolic Charge"},{"id":77,"type":"Electric","name":"Thunder Punch"},{"id":78,"type":"Electric","name":"Thunder"},{"id":79,"type":"Electric","name":"Thunderbolt"},{"id":80,"type":"Dragon","name":"Twister"},{"id":82,"type":"Dragon","name":"Dragon Pulse"},{"id":83,"type":"Dragon","name":"Dragon Claw"},{"id":84,"type":"Fairy","name":"Disarming Voice"},{"id":85,"type":"Fairy","name":"Draining Kiss"},{"id":86,"type":"Fairy","name":"Dazzling Gleam"},{"id":87,"type":"Fairy","name":"Moonblast"},{"id":88,"type":"Fairy","name":"Play Rough"},{"id":89,"type":"Poison","name":"Cross Poison"},{"id":90,"type":"Poison","name":"Sludge Bomb"},{"id":91,"type":"Poison","name":"Sludge Wave"},{"id":92,"type":"Poison","name":"Gunk Shot"},{"id":94,"type":"Ground","name":"Bone Club"},{"id":95,"type":"Ground","name":"Bulldoze"},{"id":96,"type":"Ground","name":"Mud Bomb"},{"id":99,"type":"Bug","name":"Signal Beam"},{"id":100,"type":"Bug","name":"X Scissor"},{"id":101,"type":"Fire","name":"Flame Charge"},{"id":102,"type":"Fire","name":"Flame Burst"},{"id":103,"type":"Fire","name":"Fire Blast"},{"id":104,"type":"Water","name":"Brine"},{"id":105,"type":"Water","name":"Water Pulse"},{"id":106,"type":"Water","name":"Scald"},{"id":107,"type":"Water","name":"Hydro Pump"},{"id":108,"type":"Psychic","name":"Psychic"},{"id":109,"type":"Psychic","name":"Psystrike"},{"id":111,"type":"Ice","name":"Icy Wind"},{"id":114,"type":"Grass","name":"Giga Drain"},{"id":115,"type":"Fire","name":"Fire Punch"},{"id":116,"type":"Grass","name":"Solar Beam"},{"id":117,"type":"Grass","name":"Leaf Blade"},{"id":118,"type":"Grass","name":"Power Whip"},{"id":121,"type":"Flying","name":"Air Cutter"},{"id":122,"type":"Flying","name":"Hurricane"},{"id":123,"type":"Fighting","name":"Brick Break"},{"id":125,"type":"Normal","name":"Swift"},{"id":126,"type":"Normal","name":"Horn Attack"},{"id":127,"type":"Normal","name":"Stomp"},{"id":129,"type":"Normal","name":"Hyper Fang"},{"id":131,"type":"Normal","name":"Body Slam"},{"id":132,"type":"Normal","name":"Rest"},{"id":133,"type":"Normal","name":"Struggle"},{"id":134,"type":"Water","name":"Scald Blastoise"},{"id":135,"type":"Water","name":"Hydro Pump Blastoise"},{"id":136,"type":"Normal","name":"Wrap Green"},{"id":137,"type":"Normal","name":"Wrap Pink"},{"id":200,"name":"Fury Cutter"},{"id":201,"name":"Bug Bite"},{"id":202,"name":"Bite"},{"id":203,"name":"Sucker Punch"},{"id":204,"name":"Dragon Breath"},{"id":205,"name":"Thunder Shock"},{"id":206,"name":"Spark"},{"id":207,"name":"Low Kick"},{"id":208,"name":"Karate Chop"},{"id":209,"name":"Ember"},{"id":210,"name":"Wing Attack"},{"id":211,"name":"Peck"},{"id":212,"name":"Lick"},{"id":213,"name":"Shadow Claw"},{"id":214,"name":"Vine Whip"},{"id":215,"name":"Razor Leaf"},{"id":216,"name":"Mud Shot"},{"id":217,"name":"Ice Shard"},{"id":218,"name":"Frost Breath"},{"id":219,"name":"Quick Attack"},{"id":220,"name":"Scratch"},{"id":221,"name":"Tackle"},{"id":222,"name":"Pound"},{"id":223,"name":"Cut"},{"id":224,"name":"Poison Jab"},{"id":225,"name":"Acid"},{"id":226,"name":"Psycho Cut"},{"id":227,"name":"Rock Throw"},{"id":228,"name":"Metal Claw"},{"id":229,"name":"Bullet Punch"},{"id":230,"name":"Water Gun"},{"id":231,"name":"Splash"},{"id":232,"name":"Water Gun Blastoise"},{"id":233,"name":"Mud Slap"},{"id":234,"name":"Zen Headbutt"},{"id":235,"name":"Confusion"},{"id":236,"name":"Poison Sting"},{"id":237,"name":"Bubble"},{"id":238,"name":"Feint Attack"},{"id":239,"name":"Steel Wing"},{"id":240,"name":"Fire Fang"},{"id":241,"name":"Rock Smash"}] diff --git a/examples/pogo-optimizer/data/pokemon.json b/examples/pogo-optimizer/data/pokemon.json deleted file mode 100644 index d5004ce2..00000000 --- a/examples/pogo-optimizer/data/pokemon.json +++ /dev/null @@ -1 +0,0 @@ -[{"Number":"001","Name":"Bulbasaur","Classification":"Seed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Tackle","Vine Whip"],"Weight":"6.9 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"002","Name":"Ivysaur"},{"Number":"003","Name":"Venusaur"}]},{"Number":"002","Name":"Ivysaur","Classification":"Seed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"13.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bulbasaur candies"},"Next evolution(s)":[{"Number":"003","Name":"Venusaur"}]},{"Number":"003","Name":"Venusaur","Classification":"Seed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Razor Leaf","Vine Whip"],"Weight":"100.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"001","Name":"Bulbasaur"},{"Number":"002","Name":"Ivysaur"}]},{"Number":"004","Name":"Charmander","AltName":"CHARMENDER","Classification":"Lizard Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Scratch"],"Weight":"8.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"005","Name":"Charmeleon"},{"Number":"006","Name":"Charizard"}]},{"Number":"005","Name":"Charmeleon","Classification":"Flame Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"19.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"}],"Next Evolution Requirements":{"Amount":100,"Name":"Charmander candies"},"Next evolution(s)":[{"Number":"006","Name":"Charizard"}]},{"Number":"006","Name":"Charizard","Classification":"Flame Pokèmon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Ember","Wing Attack"],"Weight":"90.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"004","Name":"Charmander"},{"Number":"005","Name":"Charmeleon"}]},{"Number":"007","Name":"Squirtle","Classification":"Tiny Turtle Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Tackle","Bubble"],"Weight":"9.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"008","Name":"Wartortle"},{"Number":"009","Name":"Blastoise"}]},{"Number":"008","Name":"Wartortle","Classification":"Turtle Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"22.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"}],"Next Evolution Requirements":{"Amount":100,"Name":"Squirtle candies"},"Next evolution(s)":[{"Number":"009","Name":"Blastoise"}]},{"Number":"009","Name":"Blastoise","Classification":"Shellfish Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bite","Water Gun"],"Weight":"85.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"007","Name":"Squirtle"},{"Number":"008","Name":"Wartortle"}]},{"Number":"010","Name":"Caterpie","Classification":"Worm Pokèmon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"2.9 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"011","Name":"Metapod"},{"Number":"012","Name":"Butterfree"}]},{"Number":"011","Name":"Metapod","Classification":"Cocoon Pokèmon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Tackle"],"Weight":"9.9 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"}],"Next Evolution Requirements":{"Amount":50,"Name":"Caterpie candies"},"Next evolution(s)":[{"Number":"012","Name":"Butterfree"}]},{"Number":"012","Name":"Butterfree","Classification":"Butterfly Pokèmon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"32.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"010","Name":"Caterpie"},{"Number":"011","Name":"Metapod"}]},{"Number":"013","Name":"Weedle","Classification":"Hairy Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Sting"],"Weight":"3.2 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"014","Name":"Kakuna"},{"Number":"015","Name":"Beedrill"}]},{"Number":"014","Name":"Kakuna","Classification":"Cocoon Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Posion Sting"],"Weight":"10.0 kg","Height":"0.6 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"}],"Next Evolution Requirements":{"Amount":50,"Name":"Weedle candies"},"Next evolution(s)":[{"Number":"015","Name":"Beedrill"}]},{"Number":"015","Name":"Beedrill","Classification":"Poison Bee Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Poison Jab"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"013","Name":"Weedle"},{"Number":"014","Name":"Kakuna"}]},{"Number":"016","Name":"Pidgey","Classification":"Tiny Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"1.8 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":12,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"017","Name":"Pidgeotto"},{"Number":"018","Name":"Pidgeot"}]},{"Number":"017","Name":"Pidgeotto","Classification":"Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Aerial Ace","Air Cutter","Twister"],"Weight":"30.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"}],"Next Evolution Requirements":{"Amount":50,"Name":"Pidgey candies"},"Next evolution(s)":[{"Number":"018","Name":"Pidgeot"}]},{"Number":"018","Name":"Pidgeot","Classification":"Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Steel Wing","Wing Attack"],"Special Attack(s)":["Hurricane"],"Weight":"39.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"016","Name":"Pidgey"},{"Number":"017","Name":"Pidgeotto"}]},{"Number":"019","Name":"Rattata","Classification":"Mouse Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Special Attack(s)":["Body Slam","Dig","Hyper Fang"],"Weight":"3.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Rattata candies"},"Next evolution(s)":[{"Number":"020","Name":"Raticate"}]},{"Number":"020","Name":"Raticate","Classification":"Mouse Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Quick Attack"],"Special Attack(s)":["Dig","Hyper Beam","Hyper Fang"],"Weight":"18.5 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"019","Name":"Rattata"}]},{"Number":"021","Name":"Spearow","Classification":"Tiny Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"2.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Spearow candies"},"Next evolution(s)":[{"Number":"022","Name":"Fearow"}]},{"Number":"022","Name":"Fearow","Classification":"Beak Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Steel Wing"],"Weight":"38.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"021","Name":"Spearow"}]},{"Number":"023","Name":"Ekans","Classification":"Snake Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Sting"],"Weight":"6.9 kg","Height":"2.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ekans candies"},"Next evolution(s)":[{"Number":"024","Name":"Arbok"}]},{"Number":"024","Name":"Arbok","Classification":"Cobra Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Bite"],"Weight":"65.0 kg","Height":"3.5 m","Previous evolution(s)":[{"Number":"023","Name":"Ekans"}]},{"Number":"025","Name":"Pikachu","Classification":"Mouse Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Quick Attack","Thunder Shock"],"Weight":"6.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Pikachu candies"},"Next evolution(s)":[{"Number":"026","Name":"Raichu"}]},{"Number":"026","Name":"Raichu","Classification":"Mouse Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock","Spark"],"Weight":"30.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"025","Name":"Pikachu"}]},{"Number":"027","Name":"Sandshrew","Classification":"Mouse Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"12.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Sandshrew candies"},"Next evolution(s)":[{"Number":"028","Name":"Sandslash"}]},{"Number":"028","Name":"Sandslash","Classification":"Mouse Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"027","Name":"Sandshrew"}]},{"Number":"029","Name":"Nidoran F","AltName":"NIDORAN_FEMALE","Classification":"Poison Pin Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"7.0 kg","Height":"0.4 m","Next evolution(s)":[{"Number":"030","Name":"Nidorina"},{"Number":"031","Name":"Nidoqueen"}]},{"Number":"030","Name":"Nidorina","Classification":"Poison Pin Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Sting"],"Weight":"20.0 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"}],"Next Evolution Requirements":{"Amount":100,"Name":"Nidoran F candies"},"Next evolution(s)":[{"Number":"031","Name":"Nidoqueen"}]},{"Number":"031","Name":"Nidoqueen","Classification":"Drill Pokèmon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"029","Name":"Nidoran F"},{"Number":"030","Name":"Nidorina"}]},{"Number":"032","Name":"Nidoran M","AltName":"NIDORAN_MALE","Classification":"Poison Pin Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Peck","Poison Sting"],"Weight":"9.0 kg","Height":"0.5 m","Next evolution(s)":[{"Number":"033","Name":"Nidorino"},{"Number":"034","Name":"Nidoking"}]},{"Number":"033","Name":"Nidorino","Classification":"Poison Pin Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Bite","Poison Jab"],"Weight":"19.5 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"}],"Next Evolution Requirements":{"Amount":100,"Name":"NidoranM candies"},"Next evolution(s)":[{"Number":"034","Name":"Nidoking"}]},{"Number":"034","Name":"Nidoking","Classification":"Drill Pokèmon","Type I":["Poison"],"Type II":["Ground"],"Weaknesses":["Water","Ice","Ground","Psychic"],"Fast Attack(s)":["Fury Cutter","Poison Jab"],"Weight":"62.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"032","Name":"Nidoran M"},{"Number":"033","Name":"Nidorino"}]},{"Number":"035","Name":"Clefairy","AltName":"CLEFARY","Classification":"Fairy Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"7.5 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Clefairy candies"},"Next evolution(s)":[{"Number":"036","Name":"Clefable"}]},{"Number":"036","Name":"Clefable","Classification":"Fairy Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"40.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"035","Name":"Clefairy"}]},{"Number":"037","Name":"Vulpix","Classification":"Fox Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"9.9 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Vulpi"},"Next evolution(s)":[{"Number":"038","Name":"Ninetales"}]},{"Number":"038","Name":"Ninetales","Classification":"Fox Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Quick Attack"],"Weight":"19.9 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"037","Name":"Vulpix"}]},{"Number":"039","Name":"Jigglypuff","Classification":"Balloon Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"5.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Jigglypuff candies"},"Next evolution(s)":[{"Number":"039","Name":"Jigglypuff"}]},{"Number":"040","Name":"Wigglytuff","Classification":"Balloon Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Pound"],"Weight":"12.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"040","Name":"Wigglytuff"}]},{"Number":"041","Name":"Zubat","Classification":"Bat Pokèmon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Quick Attack"],"Weight":"7.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Zubat candies"},"Next evolution(s)":[{"Number":"042","Name":"Golbat"}]},{"Number":"042","Name":"Golbat","Classification":"Bat Pokèmon","Type I":["Poison"],"Type II":["Flying"],"Weaknesses":["Electric","Ice","Psychic","Rock"],"Fast Attack(s)":["Bite","Wing Attack"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"041","Name":"Zubat"}]},{"Number":"043","Name":"Oddish","Classification":"Weed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"5.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":25,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"044","Name":"Gloom"},{"Number":"045","Name":"Vileplume"}]},{"Number":"044","Name":"Gloom","Classification":"Weed Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"8.6 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"}],"Next Evolution Requirements":{"Amount":100,"Name":"Oddish candies"},"Next evolution(s)":[{"Number":"045","Name":"Vileplume"}]},{"Number":"045","Name":"Vileplume","Classification":"Flower Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid",""],"Weight":"18.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"043","Name":"Oddish"},{"Number":"044","Name":"Gloom"}]},{"Number":"046","Name":"Paras","Classification":"Mushroom Pokèmon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Scratch"],"Weight":"5.4 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Paras candies"},"Next evolution(s)":[{"Number":"047","Name":"Parasect"}]},{"Number":"047","Name":"Parasect","Classification":"Mushroom Pokèmon","Type I":["Bug"],"Type II":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Rock"],"Fast Attack(s)":["Bug Bite","Fury Cutter"],"Weight":"29.5 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"046","Name":"Paras"}]},{"Number":"048","Name":"Venonat","Classification":"Insect Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Venonat candies"},"Next evolution(s)":[{"Number":"049","Name":"Venomoth"}]},{"Number":"049","Name":"Venomoth","Classification":"Poison Moth Pokèmon","Type I":["Bug"],"Type II":["Poison"],"Weaknesses":["Fire","Flying","Psychic","Rock"],"Fast Attack(s)":["Bug Bite","Confusion"],"Weight":"12.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"048","Name":"Venonat"}]},{"Number":"050","Name":"Diglett","Classification":"Mole Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"0.8 kg","Height":"0.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Diglett candies"},"Next evolution(s)":[{"Number":"051","Name":"Dugtrio"}]},{"Number":"051","Name":"Dugtrio","Classification":"Mole Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Shot","Sucker Punch"],"Weight":"33.3 kg","Height":"0.7 m","Previous evolution(s)":[{"Number":"050","Name":"Diglett"}]},{"Number":"052","Name":"Meowth","Classification":"Scratch Cat Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Bite","Scratch"],"Weight":"4.2 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Meowth candies"},"Next evolution(s)":[{"Number":"053","Name":"Persian"}]},{"Number":"053","Name":"Persian","Classification":"Classy Cat Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Feint Attack","Scratch"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"052","Name":"Meowth"}]},{"Number":"054","Name":"Psyduck","Classification":"Duck Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun","Zen Headbutt"],"Weight":"19.6 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Psyduck candies"},"Next evolution(s)":[{"Number":"055","Name":"Golduck"}]},{"Number":"055","Name":"Golduck","Classification":"Duck Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"76.6 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"054","Name":"Psyduck"}]},{"Number":"056","Name":"Mankey","Classification":"Pig Monkey Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Scratch"],"Weight":"28.0 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Mankey candies"},"Next evolution(s)":[{"Number":"057","Name":"Primeape"}]},{"Number":"057","Name":"Primeape","Classification":"Pig Monkey Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"32.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"056","Name":"Mankey"}]},{"Number":"058","Name":"Growlithe","Classification":"Puppy Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Ember"],"Weight":"19.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":50,"Name":"Growlithe candies"},"Next evolution(s)":[{"Number":"059","Name":"Arcanine"}]},{"Number":"059","Name":"Arcanine","Classification":"Legendary Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Bite","Fire Fang"],"Weight":"155.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"058","Name":"Growlithe"}]},{"Number":"060","Name":"Poliwag","Classification":"Tadpole Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"12.4 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":25,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"061","Name":"Poliwhirl"},{"Number":"062","Name":"Poliwrath"}]},{"Number":"061","Name":"Poliwhirl","Classification":"Tadpole Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"20.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"}],"Next Evolution Requirements":{"Amount":100,"Name":"Poliwag candies"},"Next evolution(s)":[{"Number":"062","Name":"Poliwrath"}]},{"Number":"062","Name":"Poliwrath","Classification":"Tadpole Pokèmon","Type I":["Water"],"Type II":["Fighting"],"Weaknesses":["Electric","Grass","Flying","Psychic","Fairy"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"54.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"060","Name":"Poliwag"},{"Number":"061","Name":"Poliwhirl"}]},{"Number":"063","Name":"Abra","Classification":"Psi Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Zen Headbutt",""],"Weight":"19.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":25,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"064","Name":"Kadabra"},{"Number":"065","Name":"Alakazam"}]},{"Number":"064","Name":"Kadabra","Classification":"Psi Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"56.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"}],"Next Evolution Requirements":{"Amount":100,"Name":"Abra candies"},"Next evolution(s)":[{"Number":"065","Name":"Alakazam"}]},{"Number":"065","Name":"Alakazam","AltName":"ALAKHAZAM","Classification":"Psi Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Psycho Cut"],"Weight":"48.0 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"063","Name":"Abra"},{"Number":"064","Name":"Kadabra"}]},{"Number":"066","Name":"Machop","Classification":"Superpower Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"19.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"067","Name":"Machoke"},{"Number":"068","Name":"Machamp"}]},{"Number":"067","Name":"Machoke","Classification":"Superpower Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Karate Chop","Low Kick"],"Weight":"70.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"}],"Next Evolution Requirements":{"Amount":100,"Name":"Machop candies"},"Next evolution(s)":[{"Number":"068","Name":"Machamp"}]},{"Number":"068","Name":"Machamp","Classification":"Superpower Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Karate Chop"],"Weight":"130.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"066","Name":"Machop"},{"Number":"067","Name":"Machoke"}]},{"Number":"069","Name":"Bellsprout","Classification":"Flower Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Vine Whip"],"Weight":"4.0 kg","Height":"0.7 m","Next Evolution Requirements":{"Amount":25,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"070","Name":"Weepinbell"},{"Number":"071","Name":"Victreebel"}]},{"Number":"070","Name":"Weepinbell","Classification":"Flycatcher Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"6.4 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"}],"Next Evolution Requirements":{"Amount":100,"Name":"Bellsprout candies"},"Next evolution(s)":[{"Number":"071","Name":"Victreebel"}]},{"Number":"071","Name":"Victreebel","Classification":"Flycatcher Pokèmon","Type I":["Grass"],"Type II":["Poison"],"Weaknesses":["Fire","Ice","Flying","Psychic"],"Fast Attack(s)":["Acid","Razor Leaf"],"Weight":"15.5 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"069","Name":"Bellsprout"},{"Number":"070","Name":"Weepinbell"}]},{"Number":"072","Name":"Tentacool","Classification":"Jellyfish Pokèmon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Bubble","Poison Sting"],"Weight":"45.5 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Tentacool candies"},"Next evolution(s)":[{"Number":"073","Name":"Tentacruel"}]},{"Number":"073","Name":"Tentacruel","Classification":"Jellyfish Pokèmon","Type I":["Water"],"Type II":["Poison"],"Weaknesses":["Electric","Ground","Psychic"],"Fast Attack(s)":["Acid","Poison Jab"],"Weight":"55.0 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"072","Name":"Tentacool"}]},{"Number":"074","Name":"Geodude","AltName":"GEODUGE","Classification":"Rock Pokèmon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"20.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":25,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"075","Name":"Graveler"},{"Number":"076","Name":"Golem"}]},{"Number":"075","Name":"Graveler","Classification":"Rock Pokèmon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"105.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"}],"Next Evolution Requirements":{"Amount":100,"Name":"Geodude candies"},"Next evolution(s)":[{"Number":"076","Name":"Golem"}]},{"Number":"076","Name":"Golem","Classification":"Megaton Pokèmon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Shot","Rock Throw"],"Weight":"300.0 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"074","Name":"Geodude"},{"Number":"075","Name":"Graveler"}]},{"Number":"077","Name":"Ponyta","Classification":"Fire Horse Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Tackle"],"Weight":"30.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Ponyta candies"},"Next evolution(s)":[{"Number":"078","Name":"Rapidash"}]},{"Number":"078","Name":"Rapidash","Classification":"Fire Horse Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Low Kick"],"Weight":"95.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"077","Name":"Ponyta"}]},{"Number":"079","Name":"Slowpoke","Classification":"Dopey Pokèmon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"36.0 kg","Height":"1.2 m","Next Evolution Requirements":{"Amount":50,"Name":"Slowpoke candies"},"Next evolution(s)":[{"Number":"080","Name":"Slowbro"}]},{"Number":"080","Name":"Slowbro","Classification":"Hermit Crab Pokèmon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Water Gun"],"Weight":"78.5 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"079","Name":"Slowpoke"}]},{"Number":"081","Name":"Magnemite","Classification":"Magnet Pokèmon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"6.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Magnemite candies"},"Next evolution(s)":[{"Number":"082","Name":"Magneton"}]},{"Number":"082","Name":"Magneton","Classification":"Magnet Pokèmon","Type I":["Electric"],"Type II":["Steel"],"Weaknesses":["Fire","Water","Ground"],"Fast Attack(s)":["Spark","Thunder Shock"],"Weight":"60.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"081","Name":"Magnemite"}]},{"Number":"083","Name":"Farfetch'd","Classification":"Wild Duck Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"15.0 kg","Height":"0.8 m"},{"Number":"084","Name":"Doduo","Classification":"Twin Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Peck","Quick Attack"],"Weight":"39.2 kg","Height":"1.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Doduo candies"},"Next evolution(s)":[{"Number":"085","Name":"Dodrio"}]},{"Number":"085","Name":"Dodrio","Classification":"Triple Bird Pokèmon","Type I":["Normal"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Feint Attack","Steel Wing"],"Weight":"85.2 kg","Height":"1.8 m","Previous evolution(s)":[{"Number":"084","Name":"Doduo"}]},{"Number":"086","Name":"Seel","Classification":"Sea Lion Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Water Gun"],"Weight":"90.0 kg","Height":"1.1 m","Next Evolution Requirements":{"Amount":50,"Name":"Seel candies"},"Next evolution(s)":[{"Number":"087","Name":"Dewgong"}]},{"Number":"087","Name":"Dewgong","Classification":"Sea Lion Pokèmon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"120.0 kg","Height":"1.7 m","Previous evolution(s)":[{"Number":"086","Name":"Seel"}]},{"Number":"088","Name":"Grimer","Classification":"Sludge Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Mud Slap"],"Weight":"30.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":50,"Name":"Grimer candies"},"Next evolution(s)":[{"Number":"089","Name":"Muk"}]},{"Number":"089","Name":"Muk","Classification":"Sludge Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Poison Jab",""],"Weight":"30.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"088","Name":"Grimer"}]},{"Number":"090","Name":"Shellder","Classification":"Bivalve Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Ice Shard","Tackle"],"Weight":"4.0 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":50,"Name":"Shellder candies"},"Next evolution(s)":[{"Number":"091","Name":"Cloyster"}]},{"Number":"091","Name":"Cloyster","Classification":"Bivalve Pokèmon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"132.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"090","Name":"Shellder"}]},{"Number":"092","Name":"Gastly","Classification":"Gas Pokèmon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Sucker Punch"],"Weight":"0.1 kg","Height":"1.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"093","Name":"Haunter"},{"Number":"094","Name":"Gengar"}]},{"Number":"093","Name":"Haunter","Classification":"Gas Pokèmon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Lick","Shadow Claw"],"Weight":"0.1 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"}],"Next Evolution Requirements":{"Amount":100,"Name":"Gastly candies"},"Next evolution(s)":[{"Number":"094","Name":"Gengar"}]},{"Number":"094","Name":"Gengar","Classification":"Shadow Pokèmon","Type I":["Ghost"],"Type II":["Poison"],"Weaknesses":["Ground","Psychic","Ghost","Dark"],"Fast Attack(s)":["Shadow Claw","Sucker Punch"],"Weight":"40.5 kg","Height":"1.5 m","Previous evolution(s)":[{"Number":"092","Name":"Gastly"},{"Number":"093","Name":"Haunter"}]},{"Number":"095","Name":"Onix","Classification":"Rock Snake Pokèmon","Type I":["Rock"],"Type II":["Ground"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Rock Throw","Tackle"],"Weight":"210.0 kg","Height":"8.8 m"},{"Number":"096","Name":"Drowzee","Classification":"Hypnosis Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Pound"],"Weight":"32.4 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Drowzee candies"},"Next evolution(s)":[{"Number":"097","Name":"Hypno"}]},{"Number":"097","Name":"Hypno","Classification":"Hypnosis Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"75.6 kg","Height":"1.6 m","Previous evolution(s)":[{"Number":"096","Name":"Drowzee"}]},{"Number":"098","Name":"Krabby","Classification":"River Crab Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Mud Shot"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Krabby candies"},"Next evolution(s)":[{"Number":"099","Name":"Kingler"}]},{"Number":"099","Name":"Kingler","Classification":"Pincer Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Metal Claw","Mud Shot"],"Weight":"60.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"098","Name":"Krabby"}]},{"Number":"100","Name":"Voltorb","Classification":"Ball Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark","Tackle"],"Weight":"10.4 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Voltorb candies"},"Next evolution(s)":[{"Number":"101","Name":"Electrode"}]},{"Number":"101","Name":"Electrode","Classification":"Ball Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Spark",""],"Weight":"66.6 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"100","Name":"Voltorb"}]},{"Number":"102","Name":"Exeggcute","Classification":"Egg Pokèmon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion",""],"Weight":"2.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"E"},"Next evolution(s)":[{"Number":"103","Name":"Exeggutor"}]},{"Number":"103","Name":"Exeggutor","Classification":"Coconut Pokèmon","Type I":["Grass"],"Type II":["Psychic"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"120.0 kg","Height":"2.0 m","Previous evolution(s)":[{"Number":"102","Name":"Exeggcute"}]},{"Number":"104","Name":"Cubone","Classification":"Lonely Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"6.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Cubone candies"},"Next evolution(s)":[{"Number":"105","Name":"Marowak"}]},{"Number":"105","Name":"Marowak","Classification":"Bone Keeper Pokèmon","Type I":["Ground"],"Weaknesses":["Water","Grass","Ice"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"45.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"104","Name":"Cubone"}]},{"Number":"106","Name":"Hitmonlee","Classification":"Kicking Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Low Kick","Rock Smash"],"Weight":"49.8 kg","Height":"1.5 m","Next evolution(s)":[{"Number":"107","Name":"Hitmonchan"}]},{"Number":"107","Name":"Hitmonchan","Classification":"Punching Pokèmon","Type I":["Fighting"],"Weaknesses":["Flying","Psychic","Fairy"],"Fast Attack(s)":["Bullet Punch","Rock Smash"],"Weight":"50.2 kg","Height":"1.4 m","Previous evolution(s)":[{"Number":"106","Name":"Hitmonlee"}]},{"Number":"108","Name":"Lickitung","Classification":"Licking Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"65.5 kg","Height":"1.2 m"},{"Number":"109","Name":"Koffing","Classification":"Poison Gas Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"1.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Koffing candies"},"Next evolution(s)":[{"Number":"110","Name":"Weezing"}]},{"Number":"110","Name":"Weezing","Classification":"Poison Gas Pokèmon","Type I":["Poison"],"Weaknesses":["Ground","Psychic"],"Fast Attack(s)":["Acid","Tackle"],"Weight":"9.5 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"109","Name":"Koffing"}]},{"Number":"111","Name":"Rhyhorn","Classification":"Spikes Pokèmon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"115.0 kg","Height":"1.0 m","Next Evolution Requirements":{"Amount":50,"Name":"Rhyhorn candies"},"Next evolution(s)":[{"Number":"112","Name":"Rhydon"}]},{"Number":"112","Name":"Rhydon","Classification":"Drill Pokèmon","Type I":["Ground"],"Type II":["Rock"],"Weaknesses":["Water","Grass","Ice","Fighting","Ground","Steel"],"Fast Attack(s)":["Mud Slap","Rock Smash"],"Weight":"120.0 kg","Height":"1.9 m","Previous evolution(s)":[{"Number":"111","Name":"Rhyhorn"}]},{"Number":"113","Name":"Chansey","Classification":"Egg Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Pound","Zen Headbutt"],"Weight":"34.6 kg","Height":"1.1 m"},{"Number":"114","Name":"Tangela","Classification":"Vine Pokèmon","Type I":["Grass"],"Weaknesses":["Fire","Ice","Poison","Flying","Bug"],"Fast Attack(s)":["Vine Whip",""],"Weight":"35.0 kg","Height":"1.0 m"},{"Number":"115","Name":"Kangaskhan","Classification":"Parent Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Low Kick",""],"Weight":"80.0 kg","Height":"2.2 m"},{"Number":"116","Name":"Horsea","Classification":"Dragon Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Bubble","Water Gun"],"Weight":"8.0 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Horsea candies"},"Next evolution(s)":[{"Number":"117","Name":"Seadra"}]},{"Number":"117","Name":"Seadra","Classification":"Dragon Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Dragon Breath","Water Gun"],"Weight":"25.0 kg","Height":"1.2 m","Previous evolution(s)":[{"Number":"116","Name":"Horsea"}]},{"Number":"118","Name":"Goldeen","Classification":"Goldfish Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Mud Shot"],"Weight":"15.0 kg","Height":"0.6 m","Next Evolution Requirements":{"Amount":50,"Name":"Goldeen candies"},"Next evolution(s)":[{"Number":"119","Name":"Seaking"}]},{"Number":"119","Name":"Seaking","Classification":"Goldfish Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Peck","Poison Jab"],"Weight":"39.0 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"118","Name":"Goldeen"}]},{"Number":"120","Name":"Staryu","Classification":"Starshape Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"34.5 kg","Height":"0.8 m","Next Evolution Requirements":{"Amount":50,"Name":"Staryu candies"},"Next evolution(s)":[{"Number":"120","Name":"Staryu"}]},{"Number":"121","Name":"Starmie","Classification":"Mysterious Pokèmon","Type I":["Water"],"Type II":["Psychic"],"Weaknesses":["Electric","Grass","Bug","Ghost","Dark"],"Fast Attack(s)":["Quick Attack","Water Gun"],"Weight":"80.0 kg","Height":"1.1 m","Previous evolution(s)":[{"Number":"121","Name":"Starmie"}]},{"Number":"122","Name":"Mr. Mime","Classification":"Barrier Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Confusion","Zen Headbutt"],"Weight":"54.5 kg","Height":"1.3 m"},{"Number":"123","Name":"Scyther","Classification":"Mantis Pokèmon","Type I":["Bug"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Ice","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Steel Wing"],"Weight":"56.0 kg","Height":"1.5 m"},{"Number":"124","Name":"Jynx","Classification":"Humanshape Pokèmon","Type I":["Ice"],"Type II":["Psychic"],"Weaknesses":["Fire","Bug","Rock","Ghost","Dark","Steel"],"Fast Attack(s)":["Frost Breath","Pound"],"Weight":"40.6 kg","Height":"1.4 m"},{"Number":"125","Name":"Electabuzz","Classification":"Electric Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Low Kick","Thunder Shock"],"Weight":"30.0 kg","Height":"1.1 m"},{"Number":"126","Name":"Magmar","Classification":"Spitfire Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember","Karate Chop"],"Weight":"44.5 kg","Height":"1.3 m"},{"Number":"127","Name":"Pinsir","Classification":"Stagbeetle Pokèmon","Type I":["Bug"],"Weaknesses":["Fire","Flying","Rock"],"Fast Attack(s)":["Fury Cutter","Rock Smash"],"Weight":"55.0 kg","Height":"1.5 m"},{"Number":"128","Name":"Tauros","Classification":"Wild Bull Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Tackle","Zen Headbutt"],"Weight":"88.4 kg","Height":"1.4 m"},{"Number":"129","Name":"Magikarp","Classification":"Fish Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Splash",""],"Weight":"10.0 kg","Height":"0.9 m","Next Evolution Requirements":{"Amount":400,"Name":"Magikarp candies"},"Next evolution(s)":[{"Number":"130","Name":"Gyarados"}]},{"Number":"130","Name":"Gyarados","Classification":"Atrocious Pokèmon","Type I":["Water"],"Type II":["Flying"],"Weaknesses":["Electric","Rock"],"Fast Attack(s)":["Bite","Dragon Breath"],"Weight":"235.0 kg","Height":"6.5 m","Previous evolution(s)":[{"Number":"129","Name":"Magikarp"}]},{"Number":"131","Name":"Lapras","Classification":"Transport Pokèmon","Type I":["Water"],"Type II":["Ice"],"Weaknesses":["Electric","Grass","Fighting","Rock"],"Fast Attack(s)":["Frost Breath","Ice Shard"],"Weight":"220.0 kg","Height":"2.5 m"},{"Number":"132","Name":"Ditto","Classification":"Transform Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.3 m"},{"Number":"133","Name":"Eevee","Classification":"Evolution Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"6.5 kg","Height":"0.3 m","Next Evolution Requirements":{"Amount":25,"Name":"Eevee candies"},"Next evolution(s)":[{"Number":"134","Name":"Vaporeon"},{"Number":"135","Name":"Jolteon"},{"Number":"136","Name":"Flareon"}]},{"Number":"134","Name":"Vaporeon","Classification":"Bubble Jet Pokèmon","Type I":["Water"],"Weaknesses":["Electric","Grass"],"Fast Attack(s)":["Water Gun",""],"Weight":"29.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"135","Name":"Jolteon","Classification":"Lightning Pokèmon","Type I":["Electric"],"Weaknesses":["Ground"],"Fast Attack(s)":["Thunder Shock",""],"Weight":"24.5 kg","Height":"0.8 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"136","Name":"Flareon","Classification":"Flame Pokèmon","Type I":["Fire"],"Weaknesses":["Water","Ground","Rock"],"Fast Attack(s)":["Ember",""],"Weight":"25.0 kg","Height":"0.9 m","Previous evolution(s)":[{"Number":"133","Name":"Eevee"}]},{"Number":"137","Name":"Porygon","Classification":"Virtual Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Quick Attack","Tackle"],"Weight":"36.5 kg","Height":"0.8 m"},{"Number":"138","Name":"Omanyte","Classification":"Spiral Pokèmon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Water Gun",""],"Weight":"7.5 kg","Height":"0.4 m","Next Evolution Requirements":{"Amount":50,"Name":"Omanyte candies"},"Next evolution(s)":[{"Number":"139","Name":"Omastar"}]},{"Number":"139","Name":"Omastar","Classification":"Spiral Pokèmon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Rock Throw","Water Gun"],"Weight":"35.0 kg","Height":"1.0 m","Previous evolution(s)":[{"Number":"138","Name":"Omanyte"}]},{"Number":"140","Name":"Kabuto","Classification":"Shellfish Pokèmon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Mud Shot","Scratch"],"Weight":"11.5 kg","Height":"0.5 m","Next Evolution Requirements":{"Amount":50,"Name":"Kabuto candies"},"Next evolution(s)":[{"Number":"141","Name":"Kabutops"}]},{"Number":"141","Name":"Kabutops","Classification":"Shellfish Pokèmon","Type I":["Rock"],"Type II":["Water"],"Weaknesses":["Electric","Grass","Fighting","Ground"],"Fast Attack(s)":["Fury Cutter","Mud Shot"],"Weight":"40.5 kg","Height":"1.3 m","Previous evolution(s)":[{"Number":"140","Name":"Kabuto"}]},{"Number":"142","Name":"Aerodactyl","Classification":"Fossil Pokèmon","Type I":["Rock"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Ice","Rock","Steel"],"Fast Attack(s)":["Bite","Steel Wing"],"Weight":"59.0 kg","Height":"1.8 m"},{"Number":"143","Name":"Snorlax","Classification":"Sleeping Pokèmon","Type I":["Normal"],"Weaknesses":["Fighting"],"Fast Attack(s)":["Lick","Zen Headbutt"],"Weight":"460.0 kg","Height":"2.1 m"},{"Number":"144","Name":"Articuno","Classification":"Freeze Pokèmon","Type I":["Ice"],"Type II":["Flying"],"Weaknesses":["Fire","Electric","Rock","Steel"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"55.4 kg","Height":"1.7 m"},{"Number":"145","Name":"Zapdos","Classification":"Electric Pokèmon","Type I":["Electric"],"Type II":["Flying"],"Weaknesses":["Ice","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"52.6 kg","Height":"1.6 m"},{"Number":"146","Name":"Moltres","Classification":"Flame Pokèmon","Type I":["Fire"],"Type II":["Flying"],"Weaknesses":["Water","Electric","Rock"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"60.0 kg","Height":"2.0 m"},{"Number":"147","Name":"Dratini","Classification":"Dragon Pokèmon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"3.3 kg","Height":"1.8 m","Next Evolution Requirements":{"Amount":25,"Name":"Dratini candies"}},{"Number":"148","Name":"Dragonair","Classification":"Dragon Pokèmon","Type I":["Dragon"],"Weaknesses":["Ice","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath",""],"Weight":"16.5 kg","Height":"4.0 m","Next Evolution Requirements":{"Amount":100,"Name":"Dratini candies"},"Next evolution(s)":[{"Number":"149","Name":"Dragonite"}]},{"Number":"149","Name":"Dragonite","Classification":"Dragon Pokèmon","Type I":["Dragon"],"Type II":["Flying"],"Weaknesses":["Ice","Rock","Dragon","Fairy"],"Fast Attack(s)":["Dragon Breath","Steel Wing"],"Weight":"210.0 kg","Height":"2.2 m","Previous evolution(s)":[{"Number":"148","Name":"Dragonair"}]},{"Number":"150","Name":"Mewtwo","Classification":"Genetic Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"122.0 kg","Height":"2.0 m"},{"Number":"151","Name":"Mew","Classification":"New Species Pokèmon","Type I":["Psychic"],"Weaknesses":["Bug","Ghost","Dark"],"Fast Attack(s)":["Unknown"],"Special Attack(s)":["Unknown"],"Weight":"4.0 kg","Height":"0.4 m"}] diff --git a/examples/pogo-optimizer/pogo-optimizer-cli.py b/examples/pogo-optimizer/pogo-optimizer-cli.py deleted file mode 100644 index 1e78d853..00000000 --- a/examples/pogo-optimizer/pogo-optimizer-cli.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python -""" -pgoapi - Pokemon Go API -Copyright (c) 2016 tjado - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. - -Author: tjado -""" - -import os -import re -import sys -import json -import time -import struct -import pprint -import logging -import requests -import argparse -import getpass - -# add directory of this file to PATH, so that the package will be found -sys.path.append(os.path.dirname(os.path.realpath(__file__))) - -# import Pokemon Go API lib -from pgoapi import pgoapi -from pgoapi import utilities as util - -# other stuff -from google.protobuf.internal import encoder -from tabulate import tabulate -from collections import defaultdict - -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 = {} - 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')", - required=required("auth_service")) - 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.set_defaults(DEBUG=False, TEST=False) - config = parser.parse_args() - - # Passed in arguments shoud trump - for key in config.__dict__: - if key in load and config.__dict__[key] == None: - 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 ):") - config.__dict__["password"] = getpass.getpass() - - if config.auth_service not in ['ptc', 'google']: - 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') - # log level for http request class - logging.getLogger("requests").setLevel(logging.WARNING) - # log level for main pgoapi class - logging.getLogger("pgoapi").setLevel(logging.INFO) - # log level for internal pgoapi class - logging.getLogger("rpc_api").setLevel(logging.INFO) - - config = init_config() - if not config: - return - - if config.debug: - logging.getLogger("requests").setLevel(logging.DEBUG) - logging.getLogger("pgoapi").setLevel(logging.DEBUG) - logging.getLogger("rpc_api").setLevel(logging.DEBUG) - - if config.test: - return - - # instantiate pgoapi - api = pgoapi.PGoApi() - - if not api.login(config.auth_service, config.username, config.password): - return - - # get inventory call - # ---------------------- - api.get_inventory() - - # execute the RPC call - response_dict = api.call() - - approot = os.path.dirname(os.path.realpath(__file__)) - - with open(os.path.join(approot, 'data/moves.json')) as data_file: - moves = json.load(data_file) - - with open(os.path.join(approot, 'data/pokemon.json')) as data_file: - pokemon = json.load(data_file) - - 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'] - 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 = list(map(format, all_pokemon)) - all_pokemon.sort(key=lambda x: x['power_quotient'], reverse=True) - - print(tabulate(all_pokemon, headers = "keys")) - -if __name__ == '__main__': - main() diff --git a/examples/spiral_poi_search.py b/examples/spiral_poi_search.py deleted file mode 100755 index bc5b2277..00000000 --- a/examples/spiral_poi_search.py +++ /dev/null @@ -1,228 +0,0 @@ -#!/usr/bin/env python -""" -pgoapi - Pokemon Go API -Copyright (c) 2016 tjado - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. - -Author: tjado -""" - -import os -import re -import sys -import json -import time -import struct -import random -import logging -import requests -import argparse -import pprint - -from pgoapi import PGoApi -from pgoapi.utilities import f2i, h2f -from pgoapi import utilities as util - -from google.protobuf.internal import encoder -from geopy.geocoders import GoogleV3 -from s2sphere import Cell, CellId, LatLng - -log = logging.getLogger(__name__) - -def get_pos_by_name(location_name): - geolocator = GoogleV3() - loc = geolocator.geocode(location_name) - if not loc: - 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) - - return (loc.latitude, loc.longitude, loc.altitude) - -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() - left = origin.prev() - - # Search around provided radius - for i in range(radius): - walk.append(right.id()) - walk.append(left.id()) - right = right.next() - left = left.prev() - - # 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 = {} - 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')", - 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.set_defaults(DEBUG=False, TEST=False) - config = parser.parse_args() - - # Passed in arguments shoud trump - for key in config.__dict__: - if key in load and config.__dict__[key] == None: - config.__dict__[key] = load[key] - - if config.auth_service not in ['ptc', 'google']: - 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') - # log level for http request class - logging.getLogger("requests").setLevel(logging.WARNING) - # log level for main pgoapi class - logging.getLogger("pgoapi").setLevel(logging.INFO) - # log level for internal pgoapi class - logging.getLogger("rpc_api").setLevel(logging.INFO) - - config = init_config() - if not config: - return - - if config.debug: - logging.getLogger("requests").setLevel(logging.DEBUG) - logging.getLogger("pgoapi").setLevel(logging.DEBUG) - logging.getLogger("rpc_api").setLevel(logging.DEBUG) - - position = get_pos_by_name(config.location) - if not position: - return - - if config.test: - return - - # instantiate pgoapi - api = PGoApi() - - # provide player position on the earth - api.set_position(*position) - - if not api.login(config.auth_service, config.username, config.password): - return - - # chain subrequests (methods) into one RPC call - - # get player profile call - # ---------------------- - 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 - # 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))) - find_poi(api, position[0], position[1]) - -def find_poi(api, lat, lng): - poi = {'pokemons': {}, 'forts': []} - step_size = 0.0015 - step_limit = 49 - coords = generate_spiral(lat, lng, step_size, step_limit) - for coord in coords: - lat = coord['lat'] - 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) - 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 '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 - 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))) - 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']) - -def print_gmaps_dbug(coords): - url_string = 'http://maps.googleapis.com/maps/api/staticmap?size=400x400&path=' - for coord in 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 - rlow = 0.0 - rhigh = 0.0005 - - while steps < step_limit: - while 2 * x * d < m and steps < step_limit: - x = x + d - steps += 1 - lat = x * step_size + starting_lat + random.uniform(rlow, rhigh) - lng = y * step_size + starting_lng + random.uniform(rlow, rhigh) - coords.append({'lat': lat, 'lng': lng}) - while 2 * y * d < m and steps < step_limit: - y = y + d - steps += 1 - lat = x * step_size + starting_lat + random.uniform(rlow, rhigh) - lng = y * step_size + starting_lng + random.uniform(rlow, rhigh) - coords.append({'lat': lat, 'lng': lng}) - - d = -1 * d - m = m + 1 - return coords - -if __name__ == '__main__': - main() diff --git a/pgoapi/auth.py b/pgoapi/auth.py index 7b8a74c8..1dd0103c 100755 --- a/pgoapi/auth.py +++ b/pgoapi/auth.py @@ -122,3 +122,21 @@ def check_access_token(self): self.log.info('Access Token expired!') return False + + def check_authentication(self, expire_timestamp_ms, start, end): + if self.is_new_ticket(expire_timestamp_ms): + + had_ticket = self.has_ticket() + self.set_ticket([expire_timestamp_ms, start, end]) + + now_ms = get_time(ms=True) + 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) + else: + self.log.debug( + 'Received Session Ticket valid for %02d:%02d:%02d hours (%s < %s)', + h, m, s, now_ms, expire_timestamp_ms) diff --git a/pgoapi/auth_google.py b/pgoapi/auth_google.py index 45d4b512..c90b4660 100755 --- a/pgoapi/auth_google.py +++ b/pgoapi/auth_google.py @@ -25,8 +25,6 @@ from __future__ import absolute_import -import logging - from pgoapi.auth import Auth from pgoapi.exceptions import AuthException, InvalidCredentialsException, AuthGoogleTwoFactorRequiredException from gpsoauth import perform_master_login, perform_oauth diff --git a/pgoapi/auth_ptc.py b/pgoapi/auth_ptc.py index 8c9240e4..d0960340 100755 --- a/pgoapi/auth_ptc.py +++ b/pgoapi/auth_ptc.py @@ -24,18 +24,13 @@ """ from __future__ import absolute_import -from future.standard_library import install_aliases -install_aliases() import requests - -from urllib.parse import parse_qs, urlsplit from six import string_types from pgoapi.auth import Auth from pgoapi.utilities import get_time from pgoapi.exceptions import AuthException, AuthTimeoutException, InvalidCredentialsException - from requests.exceptions import RequestException, Timeout, ProxyError, SSLError, ConnectionError @@ -56,8 +51,12 @@ def __init__(self, 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 = { + def set_proxy(self, proxy_config): + self.proxies = proxy_config + + def get_session(self): + session = requests.session() + session.headers = { 'Host': 'sso.pokemon.com', 'Accept': '*/*', 'Connection': 'keep-alive', @@ -66,9 +65,9 @@ def __init__(self, 'Accept-Encoding': 'br, gzip, deflate', 'X-Unity-Version': '2017.1.2f1' } - - def set_proxy(self, proxy_config): - self._session.proxies = proxy_config + if self.proxies: + session.proxies = self.proxies + return session def user_login(self, username=None, password=None): self._username = username or self._username @@ -79,7 +78,8 @@ def user_login(self, username=None, password=None): "Username/password not correctly specified") self.log.info('PTC User Login for: {}'.format(self._username)) - self._session.cookies.clear() + session = self.get_session() + session.cookies.clear() try: now = get_time() @@ -87,7 +87,7 @@ def user_login(self, username=None, password=None): logout_params = { 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize' } - r = self._session.get( + r = session.get( 'https://sso.pokemon.com/sso/logout', params=logout_params, timeout=self.timeout, @@ -98,7 +98,7 @@ def user_login(self, username=None, password=None): 'service': 'https://sso.pokemon.com/sso/oauth2.0/callbackAuthorize', 'locale': self.locale } - r = self._session.get( + r = session.get( 'https://sso.pokemon.com/sso/login', params=login_params_get, timeout=self.timeout) @@ -119,7 +119,7 @@ def user_login(self, username=None, password=None): login_headers_post = { 'Content-Type': 'application/x-www-form-urlencoded' } - r = self._session.post( + r = session.post( 'https://sso.pokemon.com/sso/login', params=login_params_post, headers=login_headers_post, @@ -128,7 +128,7 @@ def user_login(self, username=None, password=None): allow_redirects=False) try: - self._access_token = self._session.cookies['CASTGC'] + self._access_token = session.cookies['CASTGC'] except (AttributeError, KeyError, TypeError): try: j = r.json(encoding='utf-8') @@ -151,7 +151,7 @@ def user_login(self, username=None, password=None): token_headers = { 'Content-Type': 'application/x-www-form-urlencoded' } - r = self._session.post( + r = session.post( 'https://sso.pokemon.com/sso/oauth2.0/accessToken', headers=token_headers, data=token_data, @@ -166,7 +166,7 @@ def user_login(self, username=None, password=None): profile_headers = { 'Content-Type': 'application/x-www-form-urlencoded' } - r = self._session.post( + r = session.post( 'https://sso.pokemon.com/sso/oauth2.0/profile', headers=profile_headers, data=profile_data, diff --git a/pgoapi/hash_engine.py b/pgoapi/hash_engine.py deleted file mode 100644 index 88994d0f..00000000 --- a/pgoapi/hash_engine.py +++ /dev/null @@ -1,18 +0,0 @@ -class HashEngine: - def __init__(self): - self.location_hash = None - self.location_auth_hash = None - self.request_hashes = [] - - 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 dd4c91c1..5f5b5d4e 100644 --- a/pgoapi/hash_server.py +++ b/pgoapi/hash_server.py @@ -6,33 +6,33 @@ from struct import pack, unpack -from pgoapi.hash_engine import HashEngine from pgoapi.exceptions import BadHashRequestException, HashingOfflineException, HashingQuotaExceededException, HashingTimeoutException, MalformedHashResponseException, NoHashKeyException, TempHashingBanException, UnexpectedHashResponseException -class HashServer(HashEngine): +class HashServer: _session = requests.session() - _adapter = requests.adapters.HTTPAdapter(pool_maxsize=150, pool_block=True) - _session.mount('https://', _adapter) + _adapter = requests.adapters.HTTPAdapter(pool_maxsize=500, pool_block=True) + _session.mount('http://', _adapter) _session.verify = True - _session.headers.update({'User-Agent': 'Python pgoapi @pogodev'}) endpoint = 'https://pokehash.buddyauth.com/api/v157_5/hash' status = {} + _endpoint = 'http://hash.goman.io/api/v157_5/hash' + _headers = { + 'User-Agent': 'Python pgoapi @pogodev', + 'content-type': 'application/json', + 'Accept': 'application/json', + 'X-MaxRPMCount': '32000' + } - def __init__(self, auth_token): - if not auth_token: + @staticmethod + def hash(timestamp, latitude, longitude, accuracy, authticket, + sessiondata, requestslist, token): + + if not token: raise NoHashKeyException('Token not provided for hashing server.') - self.headers = { - 'content-type': 'application/json', - 'Accept': 'application/json', - 'X-AuthToken': auth_token - } - def hash(self, timestamp, latitude, longitude, accuracy, authticket, - sessiondata, requestslist): - self.location_hash = None - self.location_auth_hash = None - self.request_hashes = [] + headers = HashServer._headers.copy() + headers['X-AuthToken'] = token payload = { 'Timestamp': @@ -55,8 +55,8 @@ def hash(self, timestamp, latitude, longitude, accuracy, authticket, # request hashes from hashing server try: - response = self._session.post( - self.endpoint, json=payload, headers=self.headers, timeout=30) + response = HashServer._session.post( + HashServer._endpoint, json=payload, headers=headers, timeout=30) except requests.exceptions.Timeout: raise HashingTimeoutException('Hashing request timed out.') except requests.exceptions.ConnectionError as error: @@ -83,26 +83,19 @@ def hash(self, timestamp, latitude, longitude, accuracy, authticket, if not response.content: raise MalformedHashResponseException('Response was empty') - headers = response.headers - try: - self.status['period'] = int(headers['X-RatePeriodEnd']) - self.status['remaining'] = int(headers['X-RateRequestsRemaining']) - self.status['maximum'] = int(headers['X-MaxRequestCount']) - self.status['expiration'] = int(headers['X-AuthTokenExpiration']) - self.status['token'] = self.headers['X-AuthToken'] - except (KeyError, TypeError, ValueError): - pass - try: response_parsed = response.json() except ValueError: raise MalformedHashResponseException( 'Unable to parse JSON from hash server.') - self.location_auth_hash = ctypes.c_int32( + location_auth_hash = ctypes.c_int32( response_parsed['locationAuthHash']).value - self.location_hash = ctypes.c_int32( + location_hash = ctypes.c_int32( response_parsed['locationHash']).value + request_hashes = [] for request_hash in response_parsed['requestHashes']: - self.request_hashes.append(ctypes.c_int64(request_hash).value) + request_hashes.append(ctypes.c_uint64(request_hash).value) + + return (location_hash, location_auth_hash, request_hashes, response.headers) diff --git a/pgoapi/pgoapi.py b/pgoapi/pgoapi.py index 64da8f98..1a38ca78 100755 --- a/pgoapi/pgoapi.py +++ b/pgoapi/pgoapi.py @@ -26,16 +26,15 @@ from __future__ import absolute_import import time -import random import logging -import requests from . import __title__, __version__, __copyright__ from pgoapi.rpc_api import RpcApi, RpcState from pgoapi.auth_ptc import AuthPtc from pgoapi.auth_google import AuthGoogle -from pgoapi.utilities import parse_api_endpoint, get_time -from pgoapi.exceptions import AuthException, AuthTokenExpiredException, BadRequestException, BannedAccountException, InvalidCredentialsException, NoPlayerPositionSetException, NotLoggedInException, ServerApiEndpointRedirectException, ServerBusyOrOfflineException, UnexpectedResponseException +from pgoapi.hash_server import HashServer +from pgoapi.utilities import parse_api_endpoint +from pgoapi.exceptions import AuthException, AuthTokenExpiredException, BannedAccountException, InvalidCredentialsException, NoPlayerPositionSetException, NotLoggedInException, ServerApiEndpointRedirectException from . import protos from pogoprotos.networking.requests.request_type_pb2 import RequestType @@ -45,6 +44,8 @@ class PGoApi: + individual_session = False + def __init__(self, provider=None, oauth2_refresh_token=None, @@ -55,14 +56,10 @@ def __init__(self, position_alt=None, proxy_config=None, device_info=None): - self.RPC_ID_LOW = 1 - self.RPC_ID_HIGH = 1 - self.START_TIME = get_time(ms=True) - random.randint(6000, 10000) self.set_logger() self.log.info('%s v%s - %s', __title__, __version__, __copyright__) - self._auth_provider = None if provider is not None and ( (username is not None and password is not None) or (oauth2_refresh_token is not None)): @@ -75,25 +72,11 @@ def __init__(self, self._position_lng = position_lng self._position_alt = position_alt - self._hash_server_token = None - - self._session = requests.session() - - # requests' Session calls .default_headers() in init, which - # makes it set a bunch of default headers, including - # 'Connection': 'keep-alive', so we overwrite all of them. - self._session.headers = { - 'User-Agent': 'Niantic App', - 'Content-Type': 'application/binary', - 'Accept-Encoding': 'identity, gzip' - } - self._session.verify = True - - if proxy_config is not None: - self._session.proxies = proxy_config - - self.device_info = device_info - self.state = RpcState() + self.state = RpcState(device_info, None) + if PGoApi.individual_session: + self.state.session = RpcApi.create_session() + self.proxies = None + self.hash_key = None def set_logger(self, logger=None): self.log = logger or logging.getLogger(__name__) @@ -112,11 +95,11 @@ def set_authentication(self, timeout=None, locale=None): if provider == 'ptc': - self._auth_provider = AuthPtc(user_agent=user_agent, timeout=timeout, locale=locale) + self.state.auth_provider = AuthPtc(user_agent=user_agent, timeout=timeout, locale=locale) elif provider == 'google': - self._auth_provider = AuthGoogle() + self.state.auth_provider = AuthGoogle() elif provider is None: - self._auth_provider = None + self.state.auth_provider = None else: raise InvalidCredentialsException( "Invalid authentication provider - only ptc/google available.") @@ -124,12 +107,12 @@ def set_authentication(self, self.log.debug('Auth provider: {}'.format(provider)) if proxy_config: - self._auth_provider.set_proxy(proxy_config) + self.state.auth_provider.set_proxy(proxy_config) if oauth2_refresh_token is not None: - self._auth_provider.set_refresh_token(oauth2_refresh_token) + self.state.auth_provider.set_refresh_token(oauth2_refresh_token) elif username and password: - if not self._auth_provider.user_login(username, password): + if not self.state.auth_provider.user_login(username, password): raise AuthException("User login failed!") else: raise InvalidCredentialsException( @@ -147,7 +130,10 @@ def set_position(self, lat, lng, alt=None): self._position_alt = alt def set_proxy(self, proxy_config): - self._session.proxies = proxy_config + self.proxies = proxy_config + + def get_proxy(self): + return self.proxies def get_api_endpoint(self): return self._api_endpoint @@ -159,28 +145,25 @@ def set_api_endpoint(self, api_url): self._api_endpoint = parse_api_endpoint(api_url) def get_auth_provider(self): - return self._auth_provider + return self.state.auth_provider def create_request(self): request = PGoApiRequest(self, self._position_lat, self._position_lng, - self._position_alt, self.device_info) + self._position_alt) return request def activate_hash_server(self, hash_server_token): - self._hash_server_token = hash_server_token + self.hash_key = hash_server_token def get_hash_server_token(self): - return self._hash_server_token + return self.hash_key - def get_next_request_id(self): - self.RPC_ID_LOW += 1 - self.RPC_ID_HIGH = ((7**5) * self.RPC_ID_HIGH) % ((2**31) - 1) - reqid = (self.RPC_ID_HIGH << 32) | self.RPC_ID_LOW - self.log.debug('RPC Request ID: %s.', reqid) - return reqid + def set_hashing_endpoint(self, endpoint): + HashServer._endpoint = endpoint - def get_start_time(self): - return self.START_TIME + @staticmethod + def set_individual_session(value): + PGoApi.individual_session = value def __getattr__(self, func): def function(**kwargs): @@ -234,7 +217,6 @@ def app_simulation_login(self): """ The login function is not needed anymore but still in the code for backward compatibility" """ - def login(self, provider, username, @@ -278,15 +260,13 @@ def __init__(self, parent, position_lat, position_lng, - position_alt, - device_info=None): + position_alt): self.log = logging.getLogger(__name__) self.__parent__ = parent self.state = parent.state """ Inherit necessary parameters from parent """ self._api_endpoint = parent.get_api_endpoint() - self._auth_provider = parent.get_auth_provider() self._position_lat = position_lat self._position_lng = position_lng @@ -294,35 +274,31 @@ def __init__(self, self._req_method_list = [] self._req_platform_list = [] - self.device_info = device_info - def call(self, use_dict=True): + def call(self, hash_key=None, proxies=None): if (self._position_lat is None) or (self._position_lng is None): raise NoPlayerPositionSetException - if self._auth_provider is None or not self._auth_provider.is_login(): + if self.state.auth_provider is None or not self.state.auth_provider.is_login(): self.log.info('Not logged in') raise NotLoggedInException - api = self.__parent__ - request = RpcApi(self._auth_provider, self.device_info, self.state, - api.get_next_request_id(), api.get_start_time()) - request._session = api._session - - hash_server_token = api.get_hash_server_token() - request.activate_hash_server(hash_server_token) - response = None execute = True while execute: execute = False - + api = self.__parent__ + proxies = proxies or api.proxies + hash_key = hash_key or api.hash_key try: - response = request.request(self._api_endpoint, - self._req_method_list, - self._req_platform_list, - self.get_position(), use_dict) + response = RpcApi.request(self._api_endpoint, + self._req_method_list, + self._req_platform_list, + self.get_position(), + self.state, + hash_key, + proxies) except AuthTokenExpiredException as e: """ This exception only occures if the OAUTH service provider (google/ptc) didn't send any expiration date @@ -337,7 +313,6 @@ def call(self, use_dict=True): self.log.error(error) raise NotLoggedInException(error) - request.request_proto = None # reset request and rebuild execute = True # reexecute the call except ServerApiEndpointRedirectException as e: self.log.info('API Endpoint redirect... re-execution of call') diff --git a/pgoapi/rpc_api.py b/pgoapi/rpc_api.py index 4f9f6bdb..0d6f2601 100755 --- a/pgoapi/rpc_api.py +++ b/pgoapi/rpc_api.py @@ -30,16 +30,14 @@ import logging import requests import subprocess -import ctypes from importlib import import_module from google.protobuf import message -from protobuf_to_dict import protobuf_to_dict from pycrypt import pycrypt from pgoapi.exceptions import (AuthTokenExpiredException, BadRequestException, MalformedNianticResponseException, NianticIPBannedException, NianticOfflineException, NianticThrottlingException, NianticTimeoutException, NotLoggedInException, ServerApiEndpointRedirectException, UnexpectedResponseException) -from pgoapi.utilities import to_camel_case, get_time, get_format_time_diff, weighted_choice +from pgoapi.utilities import to_camel_case, get_time, weighted_choice from pgoapi.hash_server import HashServer from . import protos @@ -53,29 +51,39 @@ class RpcApi: - def __init__(self, auth_provider, device_info, state, request_id, start_time): - - self.log = logging.getLogger(__name__) - - self._auth_provider = auth_provider - self.request_id = request_id - self.start_time = start_time - - # mystical unknown6 - resolved by PokemonGoDev - self._hash_engine = None - self.request_proto = None - - # data fields for SignalAgglom - self.token2 = random.randint(1, 59) - self.course = random.uniform(0, 360) - - self.state = state - self.device_info = device_info - - def activate_hash_server(self, auth_token): - self._hash_engine = HashServer(auth_token) - - def decode_raw(self, raw): + log = logging.getLogger(__name__) + _session = None + + @staticmethod + def create_session(): + session = requests.session() + adapter = requests.adapters.HTTPAdapter(pool_maxsize=150, pool_block=True) + # proxies use the adapter by it's own url not endpoint so all 3 are needed + session.mount('http://', adapter) + session.mount('https://', adapter) + session.mount('socks5://', adapter) + + # requests' Session calls .default_headers() in init, which + # makes it set a bunch of default headers, including + # 'Connection': 'keep-alive', so we overwrite all of them. + session.headers = { + 'User-Agent': 'Niantic App', + 'Content-Type': 'application/binary', + 'Accept-Encoding': 'identity, gzip' + } + session.verify = True + return session + + @staticmethod + def get_session(state): + if state.session: + return state.session + if not RpcApi._session: + RpcApi._session = RpcApi.create_session() + return RpcApi._session + + @staticmethod + def decode_raw(raw): output = error = None try: process = subprocess.Popen( @@ -90,18 +98,23 @@ def decode_raw(self, raw): return output - def get_class(self, cls): + @staticmethod + def get_class(cls): module_, class_ = cls.rsplit('.', 1) class_ = getattr(import_module(module_), to_camel_case(class_)) return class_ - def _make_rpc(self, endpoint, request_proto_plain): - self.log.debug('Execution of RPC') + @staticmethod + def _make_rpc(endpoint, request_proto_plain, state, proxies): + RpcApi.log.debug('Execution of RPC') request_proto_serialized = request_proto_plain.SerializeToString() try: - http_response = self._session.post( - endpoint, data=request_proto_serialized, timeout=30) + # adapter = RpcApi.get_session(state).adapters['https://'] + # RpcApi.log.error(adapter.poolmanager.connection_from_url(endpoint).num_connections) + http_response = RpcApi.get_session(state).post( + endpoint, data=request_proto_serialized, timeout=30, + proxies=proxies) except requests.exceptions.Timeout: raise NianticTimeoutException('RPC request timed out.') except requests.exceptions.ConnectionError as e: @@ -109,80 +122,61 @@ def _make_rpc(self, endpoint, request_proto_plain): return http_response - def request(self, - endpoint, + @staticmethod + def request(endpoint, subrequests, platforms, player_position, - use_dict=True): + state, + hash_key, + proxies): - if not self._auth_provider or self._auth_provider.is_login() is False: + if not state.auth_provider or state.auth_provider.is_login() is False: raise NotLoggedInException() - self.request_proto = self.request_proto or self._build_main_request( - subrequests, platforms, player_position) - response = self._make_rpc(endpoint, self.request_proto) + (request, hash_headers) = RpcApi._build_main_request( + subrequests, platforms, player_position, state, hash_key) + response = RpcApi._make_rpc(endpoint, request, state, proxies) - response_dict = self._parse_main_response(response, subrequests, - use_dict) + response_dict = RpcApi._parse_main_response(response, subrequests) # 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']): - ticket = response_dict['auth_ticket'] - self.check_authentication(ticket['expire_timestamp_ms'], - ticket['start'], ticket['end']) + status_code = response_dict['envelope'].status_code + ticket = response_dict['envelope'].auth_ticket + if ticket: + state.auth_provider.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") + elif status_code == 53: + api_url = response_dict.get('api_url') + if api_url: + exception = ServerApiEndpointRedirectException() + exception.set_redirected_endpoint(api_url) + raise exception 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) - - if status_code == 102: - raise AuthTokenExpiredException - elif status_code == 52: - raise NianticThrottlingException( - "Request throttled by server... slow down man") - elif status_code == 53: - api_url = response_dict.get('api_url') - if api_url: - exception = ServerApiEndpointRedirectException() - exception.set_redirected_endpoint(api_url) - raise exception - else: - raise UnexpectedResponseException + raise UnexpectedResponseException + # add hash headers + response_dict['hash_headers'] = hash_headers return response_dict - def check_authentication(self, expire_timestamp_ms, start, end): - if self._auth_provider.is_new_ticket(expire_timestamp_ms): - - had_ticket = self._auth_provider.has_ticket() - self._auth_provider.set_ticket([expire_timestamp_ms, start, end]) - - now_ms = get_time(ms=True) - 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) - else: - 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): - self.log.debug('Generating main RPC request...') + @staticmethod + def _build_main_request(subrequests, + platforms, + player_position, + state, + hash_key): + RpcApi.log.debug('Generating main RPC request...') request = RequestEnvelope() request.status_code = 2 - request.request_id = self.request_id + request.request_id = state.get_next_request_id() + RpcApi.log.debug('RPC Request ID: %s.', request.request_id) # 5: 43%, 10: 30%, 30: 5%, 50: 4%, 65: 10%, 200: 1%, float: 7% request.accuracy = weighted_choice([ (5, 43), @@ -198,40 +192,37 @@ def _build_main_request(self, subrequests, platforms, request.latitude, request.longitude, altitude = player_position # generate sub requests before Signature generation - request = self._build_sub_requests(request, subrequests) - request = self._build_platform_requests(request, platforms) + request = RpcApi._build_sub_requests(request, subrequests) + request = RpcApi._build_platform_requests(request, platforms) - ticket = self._auth_provider.get_ticket() + ticket = state.auth_provider.get_ticket() if ticket: - self.log.debug( + RpcApi.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( + RpcApi.log.debug( 'No Session Ticket found - using OAUTH Access Token') - auth_provider = self._auth_provider + auth_provider = state.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 + request.auth_info.token.unknown2 = state.token2 # Sig uses this when no auth_ticket available. ticket_serialized = request.auth_info.SerializeToString() sig = Signature() - sig.session_hash = self.state.session_hash + sig.session_hash = state.session_hash sig.timestamp = get_time(ms=True) - sig.timestamp_since_start = get_time(ms=True) - self.start_time + sig.timestamp_since_start = get_time(ms=True) - state.start_time - 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(): - sig.request_hash.append(ctypes.c_uint64(req_hash).value) + (sig.location_hash2, sig.location_hash1, request_hash, headers) = HashServer.hash( + sig.timestamp, request.latitude, request.longitude, + request.accuracy, ticket_serialized, sig.session_hash, + request.requests, hash_key) + sig.request_hash.extend(request_hash) loc = sig.location_fix.add() sen = sig.sensor_info.add() @@ -250,7 +241,7 @@ def _build_main_request(self, subrequests, platforms, loc.course = -1 loc.speed = -1 else: - loc.course = self.state.course + loc.course = state.course loc.speed = random.triangular(0.25, 9.7, 8.2) loc.provider_status = 3 @@ -305,9 +296,9 @@ def _build_main_request(self, subrequests, platforms, 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.magnetic_field_x = state.magnetic_field_x + sen.magnetic_field_y = state.magnetic_field_y + sen.magnetic_field_z = 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) @@ -324,18 +315,11 @@ def _build_main_request(self, subrequests, platforms, sen.status = 3 sig.unknown25 = 4500779412463383546 - - if self.device_info: - for key in self.device_info: - setattr(sig.device_info, key, self.device_info[key]) - if self.device_info['device_brand'] == 'Apple': - sig.activity_status.stationary = True - else: - sig.activity_status.stationary = True + sig.activity_status.stationary = True signature_proto = sig.SerializeToString() - if self._needsPtr8(subrequests): + if RpcApi._needsPtr8(subrequests): plat_eight = UnknownPtr8Request() plat_eight.message = '15c79df0558009a4242518d2ab65de2a59e09499' plat8 = request.platform_requests.add() @@ -351,11 +335,12 @@ def _build_main_request(self, subrequests, platforms, request.ms_since_last_locationfix = sig.timestamp_since_start - loc.timestamp_snapshot - self.log.debug('Generated protobuf request: \n\r%s', request) + RpcApi.log.debug('Generated protobuf request: \n\r%s', request) - return request + return (request, headers) - def _needsPtr8(self, requests): + @staticmethod + def _needsPtr8(requests): if len(requests) == 0: return False randval = random.uniform(0, 1) @@ -368,14 +353,15 @@ def _needsPtr8(self, requests): return True return False - def _build_sub_requests(self, mainrequest, subrequest_list): - self.log.debug('Generating sub RPC requests...') + @staticmethod + def _build_sub_requests(mainrequest, subrequest_list): + RpcApi.log.debug('Generating sub RPC requests...') for entry_id, params in subrequest_list: if params: entry_name = RequestType.Name(entry_id) proto_name = entry_name.lower() + '_message' - bytes = self._get_proto_bytes( + bytes = RpcApi._get_proto_bytes( 'pogoprotos.networking.requests.messages.', proto_name, params) @@ -389,8 +375,9 @@ def _build_sub_requests(self, mainrequest, subrequest_list): return mainrequest - def _build_platform_requests(self, mainrequest, platform_list): - self.log.debug('Generating platform RPC requests...') + @staticmethod + def _build_platform_requests(mainrequest, platform_list): + RpcApi.log.debug('Generating platform RPC requests...') for entry_id, params in platform_list: if params: @@ -398,7 +385,7 @@ 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( + bytes = RpcApi._get_proto_bytes( 'pogoprotos.networking.platform.requests.', proto_name, params) @@ -412,22 +399,23 @@ def _build_platform_requests(self, mainrequest, platform_list): return mainrequest - def _get_proto_bytes(self, path, name, entry_content): + @staticmethod + def _get_proto_bytes(path, name, entry_content): proto_classname = path + name + '_pb2.' + name - proto = self.get_class(proto_classname)() + proto = RpcApi.get_class(proto_classname)() - self.log.debug("Subrequest class: %s", proto_classname) + RpcApi.log.debug("Subrequest class: %s", proto_classname) for key, value in entry_content.items(): if isinstance(value, list): - self.log.debug("Found list: %s - trying as repeated", key) + RpcApi.log.debug("Found list: %s - trying as repeated", key) for i in value: try: - self.log.debug("%s -> %s", key, i) + RpcApi.log.debug("%s -> %s", key, i) r = getattr(proto, key) r.append(i) except Exception as e: - self.log.warning( + RpcApi.log.warning( 'Argument %s with value %s unknown inside %s (Exception: %s)', key, i, proto_classname, e) elif isinstance(value, dict): @@ -436,7 +424,7 @@ def _get_proto_bytes(self, path, name, entry_content): r = getattr(proto, key) setattr(r, k, value[k]) except Exception as e: - self.log.warning( + RpcApi.log.warning( 'Argument %s with value %s unknown inside %s (Exception: %s)', key, str(value), proto_classname, e) else: @@ -444,18 +432,19 @@ def _get_proto_bytes(self, path, name, entry_content): setattr(proto, key, value) except Exception as e: try: - self.log.debug("%s -> %s", key, value) + RpcApi.log.debug("%s -> %s", key, value) r = getattr(proto, key) r.append(value) except Exception as e: - self.log.warning( + RpcApi.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): - self.log.debug('Parsing main RPC response...') + @staticmethod + def _parse_main_response(response_raw, subrequests): + RpcApi.log.debug('Parsing main RPC response...') if response_raw.status_code == 400: raise BadRequestException("400: Bad Request") @@ -469,58 +458,51 @@ def _parse_main_response(self, response_raw, subrequests, use_dict=True): elif response_raw.status_code != 200: 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')) + RpcApi.log.warning(error) + RpcApi.log.debug('HTTP output: \n%s', + response_raw.content.decode('utf-8')) raise UnexpectedResponseException(error) if not response_raw.content: - self.log.warning('Empty server response!') + RpcApi.log.warning('Empty server response!') raise MalformedNianticResponseException('Empty server response!') response_proto = ResponseEnvelope() try: response_proto.ParseFromString(response_raw.content) except message.DecodeError as e: - self.log.error('Could not parse response: %s', e) + RpcApi.log.error('Could not parse response: %s', e) raise MalformedNianticResponseException( 'Could not decode response.') - self.log.debug('Protobuf structure of rpc response:\n\r%s', - response_proto) + RpcApi.log.debug('Protobuf structure of rpc response:\n\r%s', + response_proto) try: - self.log.debug( + RpcApi.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')) + RpcApi.decode_raw(response_raw.content).decode('utf-8')) except Exception: - self.log.debug('Error during protoc parsing - ignored.') + RpcApi.log.debug('Error during protoc parsing - ignored.') - if use_dict: - response_proto_dict = protobuf_to_dict(response_proto) - if 'returns' in response_proto_dict: - del response_proto_dict['returns'] - else: - response_proto_dict = {'envelope': response_proto} + 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) + response_proto_dict = RpcApi._parse_sub_responses( + response_proto, subrequests, response_proto_dict) # It can't be done before. - if not use_dict: - del response_proto_dict['envelope'].returns[:] + del response_proto_dict['envelope'].returns[:] return response_proto_dict - def _parse_sub_responses(self, - response_proto, + @staticmethod + def _parse_sub_responses(response_proto, subrequests_list, - response_proto_dict, - use_dict=True): - self.log.debug('Parsing sub RPC responses...') + response_proto_dict): + RpcApi.log.debug('Parsing sub RPC responses...') response_proto_dict['responses'] = {} if response_proto.status_code == 53: @@ -535,32 +517,27 @@ def _parse_sub_responses(self, proto_name = entry_name.lower() + '_response' proto_classname = 'pogoprotos.networking.responses.' + proto_name + '_pb2.' + proto_name - self.log.debug("Parsing class: %s", proto_classname) + RpcApi.log.debug("Parsing class: %s", proto_classname) subresponse_return = None try: - subresponse_extension = self.get_class(proto_classname)() + subresponse_extension = RpcApi.get_class(proto_classname)() except Exception: subresponse_extension = None error = 'Protobuf definition for {} not found'.format( proto_classname) subresponse_return = error - self.log.warning(error) + RpcApi.log.warning(error) if subresponse_extension: try: subresponse_extension.ParseFromString(subresponse) - if use_dict: - - subresponse_return = protobuf_to_dict( - subresponse_extension) - else: - subresponse_return = subresponse_extension + subresponse_return = subresponse_extension except Exception: error = "Protobuf definition for {} seems not to match".format( proto_classname) subresponse_return = error - self.log.warning(error) + RpcApi.log.warning(error) response_proto_dict['responses'][entry_name] = subresponse_return i += 1 @@ -570,7 +547,7 @@ def _parse_sub_responses(self, # Original by Noctem. class RpcState: - def __init__(self): + def __init__(self, device_info, auth_provider): self.session_hash = os.urandom(16) self.mag_x_min = random.uniform(-80, 60) self.mag_x_max = self.mag_x_min + 20 @@ -579,6 +556,18 @@ def __init__(self): self.mag_z_min = random.uniform(-70, 40) self.mag_z_max = self.mag_y_min + 15 self._course = random.uniform(0, 359.99) + self.auth_provider = auth_provider + + # data fields for SignalAgglom + self.token2 = random.randint(1, 59) + self.course = random.uniform(0, 360) + + self.device_info = device_info + + self.RPC_ID_LOW = 1 + self.RPC_ID_HIGH = 1 + self.start_time = get_time(ms=True) - random.randint(6000, 10000) + self.session = None @property def magnetic_field_x(self): @@ -596,3 +585,9 @@ def magnetic_field_z(self): def course(self): self._course = random.triangular(0, 359.99, self._course) return self._course + + def get_next_request_id(self): + self.RPC_ID_LOW += 1 + self.RPC_ID_HIGH = ((7**5) * self.RPC_ID_HIGH) % ((2**31) - 1) + reqid = (self.RPC_ID_HIGH << 32) | self.RPC_ID_LOW + return reqid diff --git a/pgoapi/utilities.py b/pgoapi/utilities.py index 1181f0cc..d7bd7d19 100755 --- a/pgoapi/utilities.py +++ b/pgoapi/utilities.py @@ -24,7 +24,6 @@ import random import logging -from json import JSONEncoder from binascii import unhexlify # other stuff @@ -59,12 +58,6 @@ def to_camel_case(value): for word in value.split('_')) -# JSON Encoder to handle bytes -class JSONByteEncoder(JSONEncoder): - def default(self, o): - return o.decode('utf-8') - - def get_pos_by_name(location_name): geolocator = GoogleV3() loc = geolocator.geocode(location_name, timeout=10) diff --git a/requirements.txt b/requirements.txt index 0fd822f0..804560ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,5 @@ -geopy>=1.11.0 protobuf>=3.0.0 requests[socks]>=2.10.0 s2sphere>=0.2.4 gpsoauth>=0.4.0 -protobuf3-to-dict>=0.1.4 -future -six pycrypt>=0.7.1 diff --git a/scripts/accept-tos.py b/scripts/accept-tos.py deleted file mode 100644 index 489f491d..00000000 --- a/scripts/accept-tos.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -"""accept-tos.py: Example script to accept in-game Terms of Service""" - -from pgoapi import PGoApi -from pgoapi.utilities import f2i -from pgoapi import utilities as util -from pgoapi.exceptions import AuthException -import pprint -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))) - -"""auth service defaults to ptc if not given""" - -accept_tos('username', 'password', 40.7127837, -74.005941, 0.0) -accept_tos('username2', 'password', 5.612711763, -1.0632, 15.0, 'ptc') -accept_tos('username3', 'password', -22.2156, 35.1237, 3.45, 'google') diff --git a/scripts/pokecli.py b/scripts/pokecli.py deleted file mode 100755 index 77fcfd60..00000000 --- a/scripts/pokecli.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python -""" -pgoapi - Pokemon Go API -Copyright (c) 2016 tjado - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE -OR OTHER DEALINGS IN THE SOFTWARE. - -Author: tjado -""" - -import os -import sys -import json -import time -import pprint -import logging -import getpass -import argparse - -# add directory of this file to PATH, so that the package will be found -sys.path.append(os.path.dirname(os.path.realpath(__file__))) - -# import Pokemon Go API lib -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 = {} - 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')", - required=required("auth_service")) - 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("-px", "--proxy", help="Specify a socks5 proxy url") - parser.set_defaults(DEBUG=False, TEST=False) - config = parser.parse_args() - - # Passed in arguments shoud trump - for key in config.__dict__: - if key in load and config.__dict__[key] == None: - 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 ):") - config.__dict__["password"] = getpass.getpass() - - if config.auth_service not in ['ptc', 'google']: - 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') - # log level for http request class - logging.getLogger("requests").setLevel(logging.WARNING) - # log level for main pgoapi class - logging.getLogger("pgoapi").setLevel(logging.INFO) - # log level for internal pgoapi class - logging.getLogger("rpc_api").setLevel(logging.INFO) - - config = init_config() - if not config: - return - - if config.debug: - logging.getLogger("requests").setLevel(logging.DEBUG) - logging.getLogger("pgoapi").setLevel(logging.DEBUG) - logging.getLogger("rpc_api").setLevel(logging.DEBUG) - - - # instantiate pgoapi - api = pgoapi.PGoApi() - if config.proxy: - api.set_proxy({'http': config.proxy, 'https': config.proxy}) - - # parse position - position = util.get_pos_by_name(config.location) - if not position: - log.error('Your given location could not be found by name') - return - elif config.test: - return - - # set player position on the earth - api.set_position(*position) - - # 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}) - else: - 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))) - - -if __name__ == '__main__': - main() From 23339202f96fa2cebe0cf857b2be360e8706f7ee Mon Sep 17 00:00:00 2001 From: friscoMad Date: Tue, 26 Dec 2017 14:28:08 -0500 Subject: [PATCH 3/7] Fix bad merge --- pgoapi/auth_ptc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pgoapi/auth_ptc.py b/pgoapi/auth_ptc.py index d0960340..da4b4ff1 100755 --- a/pgoapi/auth_ptc.py +++ b/pgoapi/auth_ptc.py @@ -49,6 +49,7 @@ def __init__(self, self._password = password self.timeout = timeout or 10 self.locale = locale or 'en_US' + self.proxies = None self.user_agent = user_agent or 'pokemongo/0 CFNetwork/893.14.2 Darwin/17.3.0' def set_proxy(self, proxy_config): From 94f68d1b4488fe0fe32067e8b3797031c9fd4b18 Mon Sep 17 00:00:00 2001 From: friscoMad Date: Fri, 29 Dec 2017 16:30:45 -0500 Subject: [PATCH 4/7] Keep stack trace and fix acces to authprovider --- pgoapi/pgoapi.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pgoapi/pgoapi.py b/pgoapi/pgoapi.py index 1a38ca78..4f153bc9 100755 --- a/pgoapi/pgoapi.py +++ b/pgoapi/pgoapi.py @@ -27,6 +27,7 @@ import time import logging +import sys from . import __title__, __version__, __copyright__ from pgoapi.rpc_api import RpcApi, RpcState @@ -307,11 +308,11 @@ def call(self, hash_key=None, proxies=None): try: self.log.info( 'Access Token rejected! Requesting new one...') - self._auth_provider.get_access_token(force_refresh=True) - except Exception as e: - error = 'Reauthentication failed: {}'.format(e) - self.log.error(error) - raise NotLoggedInException(error) + self.state.auth_provider.get_access_token(force_refresh=True) + except Exception: + type, value, traceback = sys.exc_info() + raise NotLoggedInException, ('Reauthentication failed', + type, value), traceback execute = True # reexecute the call except ServerApiEndpointRedirectException as e: From 3858f3aacd973f3278d2cd4d19cba46332614d75 Mon Sep 17 00:00:00 2001 From: friscoMad Date: Fri, 29 Dec 2017 17:07:59 -0500 Subject: [PATCH 5/7] Fix endpoint merge errors --- pgoapi/hash_server.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pgoapi/hash_server.py b/pgoapi/hash_server.py index 5f5b5d4e..38a65d32 100644 --- a/pgoapi/hash_server.py +++ b/pgoapi/hash_server.py @@ -13,10 +13,9 @@ class HashServer: _session = requests.session() _adapter = requests.adapters.HTTPAdapter(pool_maxsize=500, pool_block=True) _session.mount('http://', _adapter) + _session.mount('https://', _adapter) _session.verify = True - endpoint = 'https://pokehash.buddyauth.com/api/v157_5/hash' - status = {} - _endpoint = 'http://hash.goman.io/api/v157_5/hash' + _endpoint = 'https://pokehash.buddyauth.com/api/v157_5/hash' _headers = { 'User-Agent': 'Python pgoapi @pogodev', 'content-type': 'application/json', From 527c7959a15bca955d324aa552ed94c4725c4894 Mon Sep 17 00:00:00 2001 From: friscoMad Date: Fri, 29 Dec 2017 18:41:22 -0500 Subject: [PATCH 6/7] Remove unneeded utility functions --- pgoapi/utilities.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/pgoapi/utilities.py b/pgoapi/utilities.py index d7bd7d19..3a57af36 100755 --- a/pgoapi/utilities.py +++ b/pgoapi/utilities.py @@ -20,14 +20,10 @@ """ import time -import struct import random import logging -from binascii import unhexlify - # other stuff -from geopy.geocoders import GoogleV3 from s2sphere import LatLng, Angle, Cap, RegionCoverer, math log = logging.getLogger(__name__) @@ -35,42 +31,11 @@ EARTH_RADIUS = 6371000 # radius of Earth in meters -def f2i(float): - return struct.unpack(' Date: Fri, 12 Jan 2018 11:26:56 -0500 Subject: [PATCH 7/7] Set device info in signature --- pgoapi/rpc_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pgoapi/rpc_api.py b/pgoapi/rpc_api.py index 0d6f2601..6ddc9be2 100755 --- a/pgoapi/rpc_api.py +++ b/pgoapi/rpc_api.py @@ -315,6 +315,8 @@ def _build_main_request(subrequests, sen.status = 3 sig.unknown25 = 4500779412463383546 + for key in state.device_info: + setattr(sig.device_info, key, state.device_info[key]) sig.activity_status.stationary = True signature_proto = sig.SerializeToString()