Beispiel #1
0
class POGOAccount(object):
    def __init__(self,
                 auth_service,
                 username,
                 password,
                 hash_key=None,
                 hash_key_provider=None,
                 proxy_url=None,
                 proxy_provider=None):

        self.auth_service = auth_service
        self.username = username
        self.password = password

        # Get myself a copy of the config
        self.cfg = _mr_mime_cfg.copy()

        # Initialize hash keys
        self._hash_key = None
        if hash_key_provider and not hash_key_provider.is_empty():
            self._hash_key_provider = hash_key_provider
        elif hash_key:
            self._hash_key_provider = CyclicResourceProvider(hash_key)
            self._hash_key = hash_key
        else:
            self._hash_key_provider = None

        # Initialize proxies
        self._proxy_url = None
        if proxy_provider and not proxy_provider.is_empty():
            self._proxy_provider = proxy_provider
        elif proxy_url:
            self._proxy_provider = CyclicResourceProvider(proxy_url)
            self._proxy_url = proxy_url
        else:
            self._proxy_provider = None

        # Captcha
        self.captcha_url = None

        # Inventory information
        self.inbox = {}
        self.inventory = None
        self.inventory_balls = 0
        self.inventory_lures = 0
        self.inventory_total = 0
        self.incubators = []
        self.pokemon = {}
        self.eggs = []

        # Current location
        self.latitude = None
        self.longitude = None
        self.altitude = None

        # Count number of rareless scans (to detect shadowbans)
        self.rareless_scans = None
        self.shadowbanned = None

        # Last log message (for GUI/console)
        self.last_msg = ""

        # --- private fields

        self._reset_api()

        # Will be set to true if a request returns a BAD_REQUEST response which equals a ban
        self._bad_request_ban = False

        # Tutorial state and warn/ban flags
        self._player_state = {}

        # Trainer statistics
        self._player_stats = None

        # PGPool
        self._pgpool_auto_update_enabled = mrmime_pgpool_enabled(
        ) and self.cfg['pgpool_auto_update']
        self._last_pgpool_update = time.time()

        self.callback_egg_hatched = None

    def _reset_api(self):
        self._api = PGoApi(device_info=self._generate_device_info())
        self._download_settings_hash = None
        self._asset_time = 0
        self._item_templates_time = 0

        # Timestamp when last API request was made
        self._last_request = 0

        # Timestamp of last get_map_objects request
        self._last_gmo = self._last_request

        # Timestamp for incremental inventory updates
        self._last_timestamp_ms = None

        # Timestamp when previous user action is completed
        self._last_action = 0

    @property
    def hash_key(self):
        return self._hash_key

    @hash_key.setter
    def hash_key(self, new_key):
        if self._hash_key_provider is None:
            self._hash_key_provider = CyclicResourceProvider(new_key)
        else:
            self._hash_key_provider.set_single_resource(new_key)
        self._hash_key = new_key

    @property
    def proxy_url(self):
        return self._proxy_url

    @proxy_url.setter
    def proxy_url(self, new_proxy_url):
        if self._proxy_provider is None:
            self._proxy_provider = CyclicResourceProvider(new_proxy_url)
        else:
            self._proxy_provider.set_single_resource(new_proxy_url)
        self._proxy_url = new_proxy_url

    def set_position(self, lat, lng, alt):
        """Sets the location and altitude of the account"""
        self._api.set_position(lat, lng, alt)
        self.latitude = lat
        self.longitude = lng
        self.altitude = alt

    def release(self, reason="No longer in use"):
        if mrmime_pgpool_enabled():
            self.update_pgpool(release=True, reason=reason)

        self._api._session.close()

        auth_provider = self._api.get_auth_provider()
        if isinstance(auth_provider, AuthPtc):
            auth_provider._session.close()

        del self._api

        # Maybe delete more stuff too?
        # del self._player_state
        # del self._player_stats
        # del self.inventory
        # del self.incubators
        # del self.pokemon
        # del self.eggs

    def perform_request(self,
                        add_main_request,
                        buddy_walked=True,
                        get_inbox=True,
                        action=None):
        failures = 0
        while True:
            try:
                request = self._api.create_request()

                # Add main request
                add_main_request(request)

                # Standard requests with every call
                request.check_challenge()
                request.get_hatched_eggs()

                # Check inventory with correct timestamp
                if self._last_timestamp_ms:
                    request.get_holo_inventory(
                        last_timestamp_ms=self._last_timestamp_ms)
                else:
                    request.get_holo_inventory()

                # Always check awarded badges
                request.check_awarded_badges()

                # Optional: download settings (with correct hash value)
                if self._download_settings_hash:
                    request.download_settings(
                        hash=self._download_settings_hash)
                else:
                    request.download_settings()

                # Optional: request buddy kilometers
                if buddy_walked:
                    request.get_buddy_walked()

                if get_inbox:
                    request.get_inbox(is_history=True)

                return self._call_request(request, action)
            except NotLoggedInException as e:
                failures += 1
                if failures < 3:
                    self.log_warning("{}: Trying to reset API".format(repr(e)))
                    time.sleep(3)
                    self._reset_api()
                    self.set_position(self.latitude, self.longitude,
                                      self.altitude)
                    self.check_login()
                    time.sleep(1)
                else:
                    self.log_error(
                        "Failed {} times to reset API and repeat request. Giving up."
                        .format(failures))
                    raise

    # Use API to check the login status, and retry the login if possible.
    def check_login(self):
        # Check auth ticket
        if self._api._auth_provider and self._api._auth_provider._access_token:
            remaining_time = self._api._auth_provider._access_token_expiry - time.time(
            )
            if remaining_time > 60:
                self.log_debug(
                    'Credentials remain valid for another {} seconds.'.format(
                        remaining_time))
                return True

        try:
            if not self.cfg['parallel_logins']:
                login_lock.acquire()

            # Set proxy if given.
            if self._proxy_provider:
                self._proxy_url = self._proxy_provider.next()
                self.log_debug("Using proxy {}".format(self._proxy_url))
                self._api.set_proxy({
                    'http': self._proxy_url,
                    'https': self._proxy_url
                })

            # Try to login. Repeat a few times, but don't get stuck here.
            num_tries = 0
            # One initial try + login_retries.
            while num_tries < self.cfg['login_retries']:
                try:
                    num_tries += 1
                    self.log_info("Login try {}.".format(num_tries))
                    if self._proxy_url:
                        self._api.set_authentication(
                            provider=self.auth_service,
                            username=self.username,
                            password=self.password,
                            proxy_config={
                                'http': self._proxy_url,
                                'https': self._proxy_url
                            })
                    else:
                        self._api.set_authentication(
                            provider=self.auth_service,
                            username=self.username,
                            password=self.password)
                    self.log_info(
                        "Login successful after {} tries.".format(num_tries))
                    break
                except AuthException as ex:
                    self.log_error(
                        'Failed to login. {} - Trying again in {} seconds.'.
                        format(repr(ex), self.cfg['login_delay']))
                    # Let the exception for the last try bubble up.
                    if num_tries >= self.cfg['login_retries']:
                        raise
                    time.sleep(self.cfg['login_delay'])

            if num_tries >= self.cfg['login_retries']:
                self.log_error(
                    'Failed to login in {} tries. Giving up.'.format(
                        num_tries))
                return False

            if self.cfg['full_login_flow'] is True:
                try:
                    return self._initial_login_request_flow()
                except BannedAccountException:
                    self.log_warning("Account most probably BANNED! :-(((")
                    return False
                except CaptchaException:
                    self.log_warning("Account got CAPTCHA'd! :-|")
                    return False
                except Exception as e:
                    self.log_error("Login failed: {}".format(repr(e)))
                    return False
            return True
        finally:
            if not self.cfg['parallel_logins']:
                login_lock.release()

    def rotate_proxy(self):
        if self._proxy_provider:
            old_proxy = self._proxy_url
            self._proxy_url = self._proxy_provider.next()
            if self._proxy_url != old_proxy:
                self.log_info("Rotating proxy. Old: {}  New: {}".format(
                    old_proxy, self._proxy_url))
                proxy_config = {
                    'http': self._proxy_url,
                    'https': self._proxy_url
                }
                self._api.set_proxy(proxy_config)
                self._api._auth_provider.set_proxy(proxy_config)

    def rotate_hash_key(self):
        # Set hash key for this request
        if not self._hash_key_provider:
            msg = "No hash key configured!"
            self.log_error(msg)
            raise NoHashKeyException()

        old_hash_key = self._hash_key
        self._hash_key = self._hash_key_provider.next()
        if self._hash_key != old_hash_key:
            self.log_debug("Using hash key {}".format(self._hash_key))
        self._api.activate_hash_server(self._hash_key)

    def is_logged_in(self):
        # Logged in? Enough time left? Cool!
        if self._api._auth_provider and self._api._auth_provider._access_token:
            remaining_time = self._api._auth_provider._access_token_expiry - time.time(
            )
            return remaining_time > 60
        return False

    def is_warned(self):
        return self._player_state.get('warn')

    def is_banned(self):
        return self._bad_request_ban or self._player_state.get('banned', False)

    def has_captcha(self):
        return None if not self.is_logged_in() else (
            self.captcha_url and len(self.captcha_url) > 1)

    def uses_proxy(self):
        return self._proxy_url is not None and len(self._proxy_url) > 0

    def get_stats(self, attr, default=None):
        return getattr(self._player_stats, attr,
                       default) if self._player_stats else default

    def get_state(self, key, default=None):
        return self._player_state.get(key, default)

    def needs_pgpool_update(self):
        return self._pgpool_auto_update_enabled and (
            time.time() - self._last_pgpool_update >=
            self.cfg['pgpool_update_interval'])

    def update_pgpool(self, release=False, reason=None):
        data = {
            'username': self.username,
            'password': self.password,
            'auth_service': self.auth_service,
            'system_id': self.cfg['pgpool_system_id'],
            'latitude': self.latitude,
            'longitude': self.longitude
        }
        # After login we know whether we've got a captcha
        if self.is_logged_in():
            data.update({'captcha': self.has_captcha()})
        if self.rareless_scans is not None:
            data['rareless_scans'] = self.rareless_scans
        if self.shadowbanned is not None:
            data['shadowbanned'] = self.shadowbanned
        if self._bad_request_ban:
            data['banned'] = True
        if self._player_state:
            data.update({
                'warn': self.is_warned(),
                'banned': self.is_banned(),
                'ban_flag': self.get_state('banned')
                #'tutorial_state': data.get('tutorial_state'),
            })
        if self._player_stats:
            data.update({
                'level': self.get_stats('level'),
                'xp': self.get_stats('experience'),
                'encounters': self.get_stats('pokemons_encountered'),
                'balls_thrown': self.get_stats('pokeballs_thrown'),
                'captures': self.get_stats('pokemons_captured'),
                'spins': self.get_stats('poke_stop_visits'),
                'walked': self.get_stats('km_walked')
            })
        if self.inventory:
            data.update({
                'balls': self.inventory_balls,
                'total_items': self.inventory_total,
                'pokemon': len(self.pokemon),
                'eggs': len(self.eggs),
                'incubators': len(self.incubators),
                'lures': self.inventory_lures
            })
        if self.inbox:
            data.update({
                'email': self.inbox.get('EMAIL'),
                'team': self.inbox.get('TEAM'),
                'coins': self.inbox.get('POKECOIN_BALANCE'),
                'stardust': self.inbox.get('STARDUST_BALANCE')
            })
        if release and reason:
            data['_release_reason'] = reason
        try:
            cmd = 'release' if release else 'update'
            url = '{}/account/{}'.format(self.cfg['pgpool_url'], cmd)
            r = requests.post(url, data=json.dumps(data))
            if r.status_code == 200:
                self.log_info("Successfully {}d PGPool account.".format(cmd))
            elif r.status_code == 503:
                self.log_warning(
                    "Could not update PGPool account: {} Try increasing 'pgpool_update_interval' in MrMime config."
                    .format(r.content))
            else:
                self.log_warning(
                    "Got status {} from PGPool while updating account: {}".
                    format(r.status_code, r.content))
        except Exception as e:
            self.log_error("Could not update PGPool account: {}".format(
                repr(e)))
        self._last_pgpool_update = time.time()

    def req_get_map_objects(self):
        """Scans current account location."""
        # Make sure that we don't hammer with GMO requests
        diff = self._last_gmo + self.cfg['scan_delay'] - time.time()
        if diff > 0:
            time.sleep(diff)

        # Jitter if wanted
        if self.cfg['jitter_gmo']:
            lat, lng = jitter_location(self.latitude, self.longitude)
        else:
            lat, lng = self.latitude, self.longitude

        cell_ids = get_cell_ids(lat, lng)
        timestamps = [
            0,
        ] * len(cell_ids)
        responses = self.perform_request(
            lambda req: req.get_map_objects(latitude=f2i(lat),
                                            longitude=f2i(lng),
                                            since_timestamp_ms=timestamps,
                                            cell_id=cell_ids),
            get_inbox=True)
        self._last_gmo = self._last_request

        return responses

    def req_encounter(self, encounter_id, spawn_point_id, latitude, longitude):
        return self.perform_request(
            lambda req: req.encounter(encounter_id=encounter_id,
                                      spawn_point_id=spawn_point_id,
                                      player_latitude=latitude,
                                      player_longitude=longitude),
            action=2.25)

    def req_catch_pokemon(self, encounter_id, spawn_point_id, ball,
                          normalized_reticle_size, spin_modifier):
        response = self.perform_request(lambda req: req.catch_pokemon(
            encounter_id=encounter_id,
            pokeball=ball,
            normalized_reticle_size=normalized_reticle_size,
            spawn_point_id=spawn_point_id,
            hit_pokemon=1,
            spin_modifier=spin_modifier,
            normalized_hit_position=1.0),
                                        action=6)

        if ('CATCH_POKEMON' in response):
            catch_pokemon = response['CATCH_POKEMON']
            catch_status = catch_pokemon.status
            capture_id = catch_pokemon.captured_pokemon_id

            # Determine caught Pokemon from inventory
            self.last_caught_pokemon = None
            if catch_status == 1:
                if capture_id in self.pokemon:
                    self.last_caught_pokemon = self.pokemon[capture_id]

        return response

    def req_release_pokemon(self, pokemon_id, pokemon_ids=None):
        return self.perform_request(lambda req: req.release_pokemon(
            pokemon_id=pokemon_id, pokemon_ids=pokemon_ids))

    def req_fort_details(self, fort_id, fort_lat, fort_lng):
        return self.perform_request(lambda req: req.fort_details(
            fort_id=fort_id, latitude=fort_lat, longitude=fort_lng),
                                    action=1.2)

    def req_fort_search(self, fort_id, fort_lat, fort_lng, player_lat,
                        player_lng):
        return self.perform_request(
            lambda req: req.fort_search(fort_id=fort_id,
                                        fort_latitude=fort_lat,
                                        fort_longitude=fort_lng,
                                        player_latitude=player_lat,
                                        player_longitude=player_lng),
            action=2)

    def req_add_fort_modifier(self, modifier_id, fort_id, player_lat,
                              player_lng):
        response = self.perform_request(
            lambda req: req.add_fort_modifier(modifier_type=modifier_id,
                                              fort_id=fort_id,
                                              player_latitude=player_lat,
                                              player_longitude=player_lng),
            action=1.2)
        return response

    def req_gym_get_info(self, gym_id, gym_lat, gym_lng, player_lat,
                         player_lng):
        return self.perform_request(
            lambda req: req.gym_get_info(gym_id=gym_id,
                                         player_lat_degrees=f2i(player_lat),
                                         player_lng_degrees=f2i(player_lng),
                                         gym_lat_degrees=gym_lat,
                                         gym_lng_degrees=gym_lng))

    def req_recycle_inventory_item(self, item_id, amount):
        return self.perform_request(lambda req: req.recycle_inventory_item(
            item_id=item_id, count=amount),
                                    action=2)

    def req_level_up_rewards(self, level):
        return self.perform_request(
            lambda req: req.level_up_rewards(level=level))

    def req_verify_challenge(self, captcha_token):
        responses = self.perform_request(
            lambda req: req.verify_challenge(token=captcha_token), action=4)
        if 'VERIFY_CHALLENGE' in responses:
            response = responses['VERIFY_CHALLENGE']
            if response.HasField('success'):
                self.captcha_url = None
                self.log_info("Successfully uncaptcha'd.")
                return True
            else:
                self.log_warning("Failed verifyChallenge")
                return False

    def req_use_item_egg_incubator(self, incubator_id, egg_id):
        return self.perform_request(lambda req: req.use_item_egg_incubator(
            item_id=incubator_id, pokemon_id=egg_id))

    def seq_spin_pokestop(self, fort_id, fort_lat, fort_lng, player_lat,
                          player_lng):
        # We first need to tap the Pokestop before we can spin it, so it's a sequence of actions
        self.req_fort_details(fort_id, fort_lat, fort_lng)
        return self.req_fort_search(fort_id, fort_lat, fort_lng, player_lat,
                                    player_lng)

    # =======================================================================

    def _generate_device_info(self):
        identifier = self.username + self.password
        md5 = hashlib.md5()
        md5.update(identifier.encode('utf-8'))
        pick_hash = int(md5.hexdigest(), 16)

        iphones = {
            'iPhone5,1': 'N41AP',
            'iPhone5,2': 'N42AP',
            'iPhone5,3': 'N48AP',
            'iPhone5,4': 'N49AP',
            'iPhone6,1': 'N51AP',
            'iPhone6,2': 'N53AP',
            'iPhone7,1': 'N56AP',
            'iPhone7,2': 'N61AP',
            'iPhone8,1': 'N71AP',
            'iPhone8,2': 'N66AP',
            'iPhone8,4': 'N69AP',
            'iPhone9,1': 'D10AP',
            'iPhone9,2': 'D11AP',
            'iPhone9,3': 'D101AP',
            'iPhone9,4': 'D111AP',
            'iPhone10,1': 'D20AP',
            'iPhone10,2': 'D21AP',
            'iPhone10,3': 'D22AP',
            'iPhone10,4': 'D201AP',
            'iPhone10,5': 'D211AP',
            'iPhone10,6': 'D221AP'
        }

        ios9 = ('9.0', '9.0.1', '9.0.2', '9.1', '9.2', '9.2.1', '9.3', '9.3.1',
                '9.3.2', '9.3.3', '9.3.4', '9.3.5')
        ios10 = ('10.0', '10.0.1', '10.0.2', '10.0.3', '10.1', '10.1.1')
        ios11 = ('11.0.1', '11.0.2', '11.0.3', '11.1', '11.1.1')

        device_info = {
            'device_brand': 'Apple',
            'device_model': 'iPhone',
            'hardware_manufacturer': 'Apple',
            'firmware_brand': 'iPhone OS'
        }

        devices = tuple(iphones.keys())
        device = devices[pick_hash % len(devices)]
        device_info['device_model_boot'] = device
        device_info['hardware_model'] = iphones[device]
        device_info['device_id'] = md5.hexdigest()

        if device.startswith('iPhone10'):
            ios_pool = ios11
        elif device.startswith('iPhone9'):
            ios_pool = ios10 + ios11
        elif device.startswith('iPhone8'):
            ios_pool = ios9 + ios10 + ios11
        else:
            ios_pool = ios9 + ios10
        device_info['firmware_type'] = ios_pool[pick_hash % len(ios_pool)]

        self.log_debug("Using an {} on iOS {} with device ID {}".format(
            device, device_info['firmware_type'], device_info['device_id']))

        return device_info

    def _call_request(self, request, action=None):
        # Wait until a previous user action gets completed
        if action:
            now = time.time()
            # wait for the time required, or at least a half-second
            if self._last_action > now + .5:
                time.sleep(self._last_action - now)
            else:
                time.sleep(0.5)

        req_method_list = copy.deepcopy(request._req_method_list)

        response = {}
        rotate_proxy = False
        while True:
            try:
                self.rotate_hash_key()
                if rotate_proxy:
                    self.rotate_proxy()
                    rotate_proxy = False

                response = request.call(use_dict=False)

                self._last_request = time.time()
                break
            except NianticIPBannedException as ex:
                if not self.uses_proxy():
                    # IP banned and not using proxies... we should quit
                    self.log_error(repr(ex))
                    sys.exit(ex)
            except NotLoggedInException:
                # We need to re-login and re-post the request but we do it one level up
                raise
            except PgoapiError as ex:
                defaultRetryDelay = float(self.cfg['request_retry_delay'])
                # Rotate proxy if it's part of the error
                if self.uses_proxy() and exception_caused_by_proxy_error(ex):
                    rotate_proxy = True
                    # Shorten retry delay according to number of available proxies
                    retryDelay = defaultRetryDelay / self._proxy_provider.len()
                else:
                    # Shorten retry delay according to number of available hash keys
                    retryDelay = defaultRetryDelay / self._hash_key_provider.len(
                    )
                self.log_warning("{}: Retrying in {:.1f}s.".format(
                    repr(ex), retryDelay))
                time.sleep(retryDelay)
            except Exception as ex:
                # No PgoapiError - this is serious!
                raise

        if not 'envelope' in response:
            msg = 'No response envelope. Something is wrong!'
            self.log_warning(msg)
            raise PgoapiError(msg)

        # status_code 3 means BAD_REQUEST, so probably banned
        status_code = response['envelope'].status_code
        if status_code == 3:
            log_suffix = ''
            if self.cfg['dump_bad_requests']:
                with open('BAD_REQUESTS.txt', 'a') as f:
                    f.write(repr(req_method_list))
                    f.close()
                log_suffix = ' Dumped request to BAD_REQUESTS.txt.'
            self.log_warning(
                "Got BAD_REQUEST response. Possible Ban!{}".format(log_suffix))
            self._bad_request_ban = True
            raise BannedAccountException

        # Clean up
        del response['envelope']

        if not 'responses' in response:
            self.log_error("Got no responses at all!")
            return {}

        # Set the timer when the user action will be completed
        if action:
            self._last_action = self._last_request + action

        # Return only the responses
        responses = response['responses']

        self._parse_responses(responses)

        if self.needs_pgpool_update():
            self.update_pgpool()

        return responses

    def _update_inventory_totals(self):
        ball_ids = [
            ITEM_POKE_BALL, ITEM_GREAT_BALL, ITEM_ULTRA_BALL, ITEM_MASTER_BALL
        ]
        lure_ids = [ITEM_TROY_DISK]
        balls = 0
        lures = 0
        total_items = 0
        for item_id in self.inventory:
            if item_id in ball_ids:
                balls += self.inventory[item_id]
            if item_id in lure_ids:
                lures += self.inventory[item_id]
            total_items += self.inventory[item_id]
        self.inventory_balls = balls
        self.inventory_lures = lures
        self.inventory_total = total_items

    def _parse_responses(self, responses):
        for response_type in responses.keys():
            response = responses[response_type]

            if response_type == 'GET_INBOX':
                self._parse_inbox_response(response)
                del responses[response_type]

            elif response_type == 'GET_HOLO_INVENTORY':
                api_inventory = response

                # Set an (empty) inventory if necessary
                if self.inventory is None:
                    self.inventory = {}

                # Update inventory (balls, items)
                self._parse_inventory_delta(api_inventory)
                self._update_inventory_totals()

                # Update last timestamp for inventory requests
                self._last_timestamp_ms = api_inventory.inventory_delta.new_timestamp_ms

                # Clean up
                del responses[response_type]

            # Get settings hash from response for future calls
            elif response_type == 'DOWNLOAD_SETTINGS':
                if response.hash:
                    self._download_settings_hash = response.hash
                # TODO: Check forced client version and exit program if different

                # Clean up
                del responses[response_type]

            elif response_type == 'GET_PLAYER':
                self._player_state = {
                    'tutorial_state': response.player_data.tutorial_state,
                    'buddy': response.player_data.buddy_pokemon.id,
                    'warn': response.warn,
                    'banned': response.banned
                }

                # Clean up
                del responses[response_type]

                if self._player_state['banned']:
                    self.log_warning("GET_PLAYER has the 'banned' flag set.")
                    raise BannedAccountException

            # Check for captcha
            elif response_type == 'CHECK_CHALLENGE':
                self.captcha_url = response.challenge_url

                # Clean up
                del responses[response_type]

                if self.has_captcha() and self.cfg['exception_on_captcha']:
                    raise CaptchaException

            elif response_type == 'GET_MAP_OBJECTS':
                if is_rareless_scan(response):
                    if self.rareless_scans is None:
                        self.rareless_scans = 1
                    else:
                        self.rareless_scans += 1
                else:
                    self.rareless_scans = 0

            elif response_type == 'GET_HATCHED_EGGS':
                if self.callback_egg_hatched and response.success and len(
                        response.hatched_pokemon) > 0:
                    for i in range(0, len(response.pokemon_id)):
                        hatched_egg = {
                            'experience_awarded':
                            response.experience_awarded[i],
                            'candy_awarded': response.candy_awarded[i],
                            'stardust_awarded': response.stardust_awarded[i],
                            'egg_km_walked': response.egg_km_walked[i],
                            'hatched_pokemon': response.hatched_pokemon[i]
                        }
                        self.callback_egg_hatched(self, hatched_egg)

    def _parse_inbox_response(self, response):
        vars = response.inbox.builtin_variables
        for v in vars:
            if v.name in ('POKECOIN_BALANCE', 'STARDUST_BALANCE'):
                self.inbox[v.name] = int(v.literal)
            elif v.name == 'EMAIL':
                self.inbox[v.name] = v.literal
            elif v.name == 'TEAM':
                self.inbox[v.name] = v.key

    def _parse_inventory_delta(self, inventory):
        for item in inventory.inventory_delta.inventory_items:
            item_data = item.inventory_item_data
            if item_data.HasField('player_stats'):
                self._player_stats = item_data.player_stats
            elif item_data.HasField('item'):
                item_id = item_data.item.item_id
                item_count = item_data.item.count
                self.inventory[item_id] = item_count
            elif item_data.HasField('egg_incubators'):
                incubators = item_data.egg_incubators.egg_incubator
                for incubator in incubators:
                    if incubator.pokemon_id == 0:
                        self.incubators.append({
                            'id':
                            incubator.id,
                            'item_id':
                            incubator.item_id,
                            'uses_remaining':
                            incubator.uses_remaining
                        })
            elif item_data.HasField('pokemon_data'):
                p_data = item_data.pokemon_data
                p_id = p_data.id
                if not p_data.is_egg:
                    self.pokemon[p_id] = {
                        'pokemon_id': p_data.pokemon_id,
                        'move_1': p_data.move_1,
                        'move_2': p_data.move_2,
                        'individual_attack': p_data.individual_attack,
                        'individual_defense': p_data.individual_defense,
                        'individual_stamina': p_data.individual_stamina,
                        'height': p_data.height_m,
                        'weight': p_data.weight_kg,
                        'costume': p_data.pokemon_display.costume,
                        'form': p_data.pokemon_display.form,
                        'gender': p_data.pokemon_display.gender,
                        'shiny': p_data.pokemon_display.shiny,
                        'cp': p_data.cp,
                        'cp_multiplier': p_data.cp_multiplier,
                        'is_bad': p_data.is_bad
                    }
                else:
                    # Incubating egg
                    if p_data.egg_incubator_id:
                        continue
                    # Egg
                    self.eggs.append({
                        'id': p_id,
                        'km_target': p_data.egg_km_walked_target
                    })

    def _initial_login_request_flow(self):
        self.log_info("Performing full login flow requests")

        # Empty request -----------------------------------------------------
        self.log_debug("Login Flow: Empty request")
        # ===== empty
        request = self._api.create_request()
        self._call_request(request)
        time.sleep(random.uniform(.43, .97))

        # Get player info ---------------------------------------------------
        self.log_debug("Login Flow: Get player state")
        # ===== GET_PLAYER (unchained)
        request = self._api.create_request()
        request.get_player(player_locale=self.cfg['player_locale'])
        self._call_request(request)
        time.sleep(random.uniform(.53, 1.1))

        # Download remote config --------------------------------------------
        self.log_debug("Login Flow: Downloading remote config")
        asset_time, template_time = self._download_remote_config_version()
        time.sleep(1)

        # Assets and item templates -----------------------------------------
        if self.cfg[
                'download_assets_and_items'] and asset_time > self._asset_time:
            self.log_debug("Login Flow: Download asset digest")
            self._get_asset_digest(asset_time)
        else:
            self.log_debug("Login Flow: Skipping asset digest download")

        if self.cfg[
                'download_assets_and_items'] and template_time > self._item_templates_time:
            self.log_debug("Login Flow: Download item templates")
            self._download_item_templates(template_time)
        else:
            self.log_debug("Login Flow: Skipping item template download")

        # TODO: Maybe download translation URLs from assets? Like pogonode?

        # Checking tutorial -------------------------------------------------
        if (self._player_state['tutorial_state'] is not None
                and not all(x in self._player_state['tutorial_state']
                            for x in (0, 1, 3, 4, 7))):
            self.log_info("Completing tutorial")
            self._complete_tutorial()
        else:
            # Get player profile
            self.log_debug("Login Flow: Get player profile")
            # ===== GET_PLAYER_PROFILE
            self.perform_request(lambda req: req.get_player_profile())
            time.sleep(random.uniform(.2, .3))

        # Level up rewards --------------------------------------------------
        self.log_debug("Login Flow: Get levelup rewards")
        # ===== LEVEL_UP_REWARDS
        self.perform_request(
            lambda req: req.level_up_rewards(level=self._player_stats.level))

        # Check store -------------------------------------------------------
        # TODO: There is currently no way to call the GET_STORE_ITEMS platform request.

        self.log_info('After-login procedure completed.')
        time.sleep(random.uniform(.5, 1.3))
        return True

    def _set_avatar(self, tutorial=False):
        player_avatar = avatar.new()
        # ===== LIST_AVATAR_CUSTOMIZATIONS
        self.perform_request(
            lambda req: req.list_avatar_customizations(
                avatar_type=player_avatar['avatar'],
                # slot=tuple(),
                filters=2),
            buddy_walked=not tutorial,
            action=5,
            get_inbox=False)
        time.sleep(random.uniform(7, 14))

        # ===== SET_AVATAR
        self.perform_request(
            lambda req: req.set_avatar(player_avatar=player_avatar),
            buddy_walked=not tutorial,
            action=2,
            get_inbox=False)

        if tutorial:
            time.sleep(random.uniform(.5, 4))

            # ===== MARK_TUTORIAL_COMPLETE
            self.perform_request(
                lambda req: req.mark_tutorial_complete(tutorials_completed=1),
                buddy_walked=False,
                get_inbox=False)

            time.sleep(random.uniform(.5, 1))

        self.perform_request(lambda req: req.get_player_profile(),
                             action=1,
                             get_inbox=False)

    def _complete_tutorial(self):
        tutorial_state = self._player_state['tutorial_state']
        if 0 not in tutorial_state:
            # legal screen
            self.log_debug("Tutorial #0: Legal screen")
            # ===== MARK_TUTORIAL_COMPLETE
            self.perform_request(
                lambda req: req.mark_tutorial_complete(tutorials_completed=0),
                buddy_walked=False,
                get_inbox=False)
            time.sleep(random.uniform(.35, .525))

            # ===== GET_PLAYER
            self.perform_request(lambda req: req.get_player(
                player_locale=self.cfg['player_locale']),
                                 buddy_walked=False,
                                 get_inbox=False)
            time.sleep(1)

        if 1 not in tutorial_state:
            # avatar selection
            self.log_debug("Tutorial #1: Avatar selection")
            self._set_avatar(tutorial=True)

        starter_id = None
        if 3 not in tutorial_state:
            # encounter tutorial
            self.log_debug("Tutorial #3: Catch starter Pokemon")
            time.sleep(random.uniform(.7, .9))
            # ===== GET_DOWNLOAD_URLS
            self.perform_request(lambda req: req.get_download_urls(asset_id=[
                '1a3c2816-65fa-4b97-90eb-0b301c064b7a/1487275569649000',
                'aa8f7687-a022-4773-b900-3a8c170e9aea/1487275581132582',
                'e89109b0-9a54-40fe-8431-12f7826c8194/1487275593635524'
            ]),
                                 get_inbox=False)

            time.sleep(random.uniform(7, 10.3))
            starter = random.choice((1, 4, 7))
            # ===== ENCOUNTER_TUTORIAL_COMPLETE
            self.perform_request(lambda req: req.encounter_tutorial_complete(
                pokemon_id=starter),
                                 action=1,
                                 get_inbox=False)

            time.sleep(random.uniform(.4, .5))
            # ===== GET_PLAYER
            responses = self.perform_request(lambda req: req.get_player(
                player_locale=self.cfg['player_locale']),
                                             get_inbox=False)

            try:
                inventory = responses[
                    'GET_HOLO_INVENTORY'].inventory_delta.inventory_items
                for item in inventory:
                    pokemon = item.inventory_item_data.pokemon_data
                    if pokemon.id:
                        starter_id = pokemon.id
                        break
            except (KeyError, TypeError):
                starter_id = None

        if 4 not in tutorial_state:
            # name selection
            self.log_debug("Tutorial #4: Set trainer name")
            time.sleep(random.uniform(12, 18))
            # ===== CLAIM_CODENAME
            self.perform_request(
                lambda req: req.claim_codename(codename=self.username),
                action=2,
                get_inbox=False)

            time.sleep(.7)
            # ===== GET_PLAYER
            self.perform_request(lambda req: req.get_player(
                player_locale=self.cfg['player_locale']),
                                 get_inbox=False)
            time.sleep(.13)

            # ===== MARK_TUTORIAL_COMPLETE
            self.perform_request(
                lambda req: req.mark_tutorial_complete(tutorials_completed=4),
                buddy_walked=False,
                get_inbox=False)

        if 7 not in tutorial_state:
            # first time experience
            self.log_debug("Tutorial #7: First time experience")
            time.sleep(random.uniform(3.9, 4.5))
            # ===== MARK_TUTORIAL_COMPLETE
            self.perform_request(
                lambda req: req.mark_tutorial_complete(tutorials_completed=7),
                get_inbox=False)

        # set starter as buddy
        if starter_id:
            self.log_debug("Setting buddy Pokemon")
            time.sleep(random.uniform(4, 5))
            # ===== SET_BUDDY_POKEMON
            self.perform_request(
                lambda req: req.set_buddy_pokemon(pokemon_id=starter_id),
                action=2,
                get_inbox=False)
            time.sleep(random.uniform(.8, 1.2))

        time.sleep(.2)
        return True

    def _download_remote_config_version(self):
        # ===== DOWNLOAD_REMOTE_CONFIG_VERSION
        responses = self.perform_request(
            lambda req: req.download_remote_config_version(
                platform=1, app_version=PGoApi.get_api_version()),
            buddy_walked=False,
            get_inbox=False)
        if 'DOWNLOAD_REMOTE_CONFIG_VERSION' not in responses:
            raise Exception("Call to download_remote_config_version did not"
                            " return proper response.")
        remote_config = responses['DOWNLOAD_REMOTE_CONFIG_VERSION']
        return remote_config.asset_digest_timestamp_ms / 1000000, \
               remote_config.item_templates_timestamp_ms / 1000

    def _get_asset_digest(self, asset_time):
        i = random.randint(0, 3)
        result = 2
        page_offset = 0
        page_timestamp = 0
        while result == 2:
            # ===== GET_ASSET_DIGEST
            responses = self.perform_request(lambda req: req.get_asset_digest(
                platform=1,
                app_version=PGoApi.get_api_version(),
                paginate=True,
                page_offset=page_offset,
                page_timestamp=page_timestamp),
                                             buddy_walked=False,
                                             get_inbox=False)
            if i > 2:
                time.sleep(1.45)
                i = 0
            else:
                i += 1
                time.sleep(.2)
            try:
                response = responses['GET_ASSET_DIGEST']
            except KeyError:
                break
            result = response.result
            page_offset = response.page_offset
            page_timestamp = response.timestamp_ms
        self._asset_time = asset_time

    def _download_item_templates(self, template_time):
        i = random.randint(0, 3)
        result = 2
        page_offset = 0
        page_timestamp = 0
        while result == 2:
            # ===== DOWNLOAD_ITEM_TEMPLATES
            responses = self.perform_request(
                lambda req: req.download_item_templates(
                    paginate=True,
                    page_offset=page_offset,
                    page_timestamp=page_timestamp),
                buddy_walked=False,
                get_inbox=False)
            if i > 2:
                time.sleep(1.5)
                i = 0
            else:
                i += 1
                time.sleep(.25)
            try:
                response = responses['DOWNLOAD_ITEM_TEMPLATES']
            except KeyError:
                break
            result = response.result
            page_offset = response.page_offset
            page_timestamp = response.timestamp_ms
        self._item_templates_time = template_time

    def log_info(self, msg):
        self.last_msg = msg
        log.info(u"[{}] {}".format(self.username, msg))

    def log_debug(self, msg):
        self.last_msg = msg
        log.debug(u"[{}] {}".format(self.username, msg))

    def log_warning(self, msg):
        self.last_msg = msg
        log.warning(u"[{}] {}".format(self.username, msg))

    def log_error(self, msg):
        self.last_msg = msg
        log.error(u"[{}] {}".format(self.username, msg))
Beispiel #2
0
class POGOAccount(object):

    def __init__(self, auth_service, username, password, hash_key=None,
                 hash_key_provider=None, proxy_url=None, proxy_provider=None):
        self.auth_service = auth_service
        self.username = username
        self.password = password

        # Initialize hash keys
        self._hash_key = None
        if hash_key_provider and not hash_key_provider.is_empty():
            self._hash_key_provider = hash_key_provider
        elif hash_key:
            self._hash_key_provider = CyclicResourceProvider(hash_key)
            self._hash_key = hash_key
        else:
            self._hash_key_provider = None

        # Initialize proxies
        self._proxy_url = None
        if proxy_provider and not proxy_provider.is_empty():
            self._proxy_provider = proxy_provider
        elif proxy_url:
            self._proxy_provider = CyclicResourceProvider(proxy_url)
            self._proxy_url = proxy_url
        else:
            self._proxy_provider = None

        self.cfg = _mr_mime_cfg.copy()

        # Tutorial state and warn/ban flags
        self.player_state = {}

        # Trainer statistics
        self.player_stats = {}

        self.captcha_url = None

        # Inventory information
        self.inventory = None
        self.inventory_balls = 0
        self.inventory_total = 0

        # Location
        self.latitude = None
        self.longitude = None
        self.altitude = None

        # Last log message (for GUI/console)
        self.last_msg = ""

        # --- private fields

        self._api = PGoApi(device_info=self._generate_device_info())
        self._download_settings_hash = None
        self._asset_time = 0
        self._item_templates_time = 0

        # Timestamp when last API request was made
        self._last_request = 0

        # Timestamp of last get_map_objects request
        self._last_gmo = self._last_request

        # Timestamp for incremental inventory updates
        self._last_timestamp_ms = None

        # Timestamp when previous user action is completed
        self._last_action = 0

        self._map_cells = []

    @property
    def hash_key(self):
        return self._hash_key

    @hash_key.setter
    def hash_key(self, new_key):
        if self._hash_key_provider is None:
            self._hash_key_provider = CyclicResourceProvider(new_key)
        else:
            self._hash_key_provider.set_single_resource(new_key)
        self._hash_key = new_key

    @property
    def proxy_url(self):
        return self._proxy_url

    @proxy_url.setter
    def proxy_url(self, new_proxy_url):
        if self._proxy_provider is None:
            self._proxy_provider = CyclicResourceProvider(new_proxy_url)
        else:
            self._proxy_provider.set_single_resource(new_proxy_url)
        self._proxy_url = new_proxy_url

    def set_position(self, lat, lng, alt):
        """Sets the location and altitude of the account"""
        self._api.set_position(lat, lng, alt)
        self.latitude = lat
        self.longitude = lng
        self.altitude = alt

    def perform_request(self, add_main_request, download_settings=False,
                        buddy_walked=True, get_inbox=True, action=None, jitter=True):
        request = self._api.create_request()

        # Add main request
        add_main_request(request)

        # Standard requests with every call
        request.check_challenge()
        request.get_hatched_eggs()

        # Check inventory with correct timestamp
        if self._last_timestamp_ms:
            request.get_inventory(last_timestamp_ms=self._last_timestamp_ms)
        else:
            request.get_inventory()

        # Always check awarded badges
        request.check_awarded_badges()

        # Optional: download settings (with correct hash value)
        if download_settings:
            if self._download_settings_hash:
                request.download_settings(hash=self._download_settings_hash)
            else:
                request.download_settings()

        # Optional: request buddy kilometers
        if buddy_walked:
            request.get_buddy_walked()

        if get_inbox:
            request.get_inbox(is_history=True)

        return self._call_request(request, action, jitter)

    # Use API to check the login status, and retry the login if possible.
    def check_login(self):
        # Check auth ticket
        if self._api.get_auth_provider() and self._api.get_auth_provider().check_ticket():
            return True

        try:
            if not self.cfg['parallel_logins']:
                login_lock.acquire()

            # Set proxy if given.
            if self._proxy_provider:
                self._proxy_url = self._proxy_provider.next()
                self.log_debug("Using proxy {}".format(self._proxy_url))
                self._api.set_proxy({
                    'http': self._proxy_url,
                    'https': self._proxy_url
                })

            # Try to login. Repeat a few times, but don't get stuck here.
            num_tries = 0
            # One initial try + login_retries.
            while num_tries < self.cfg['login_retries']:
                try:
                    num_tries += 1
                    self.log_info("Login try {}.".format(num_tries))
                    if self._proxy_url:
                        self._api.set_authentication(
                            provider=self.auth_service,
                            username=self.username,
                            password=self.password,
                            proxy_config={
                                'http': self._proxy_url,
                                'https': self._proxy_url
                            })
                    else:
                        self._api.set_authentication(
                            provider=self.auth_service,
                            username=self.username,
                            password=self.password)
                    self.log_info("Login successful after {} tries.".format(num_tries))
                    break
                except AuthException:
                    self.log_error(
                        'Failed to login. Trying again in {} seconds.'.format(
                            self.cfg['login_delay']))
                    time.sleep(self.cfg['login_delay'])

            if num_tries >= self.cfg['login_retries']:
                self.log_error(
                    'Failed to login in {} tries. Giving up.'.format(num_tries))
                return False

            if self.cfg['full_login_flow'] is True:
                try:
                    return self._initial_login_request_flow()
                except BannedAccountException:
                    self.log_warning("Account most probably BANNED! :-(((")
                    self.player_state['banned'] = True
                    return False
                except CaptchaException:
                    self.log_warning("Account got CAPTCHA'd! :-|")
                    return False
                except Exception as e:
                    self.log_error("Login failed: {}".format(repr(e)))
                    return False
            return True
        finally:
            if not self.cfg['parallel_logins']:
                login_lock.release()


    def is_logged_in(self):
        # Logged in? Enough time left? Cool!
        if self._api.get_auth_provider() and self._api.get_auth_provider().has_ticket():
            remaining_time = self._api.get_auth_provider()._ticket_expire / 1000 - time.time()
            return remaining_time > 60
        return False

    def is_warned(self):
        return self.player_state.get('warn')

    def is_banned(self):
        return self.player_state.get('banned')

    def has_captcha(self):
        return None if not self.is_logged_in() else (
            self.captcha_url and len(self.captcha_url) > 1)

    def uses_proxy(self):
        return self._proxy_url is not None and len(self._proxy_url) > 0

    def req_get_map_objects(self):
        """Scans current account location."""
        # Make sure that we don't hammer with GMO requests
        diff = self._last_gmo + self.cfg['scan_delay'] - time.time()
        if diff > 0:
            time.sleep(diff)

        # We jitter here because we need the jittered location NOW
        lat, lng = jitter_location(self.latitude, self.longitude)
        self._api.set_position(lat, lng, self.altitude)

        cell_ids = get_cell_ids(lat, lng)
        timestamps = [0, ] * len(cell_ids)
        responses = self.perform_request(
            lambda req: req.get_map_objects(latitude=f2i(lat),
                                            longitude=f2i(lng),
                                            since_timestamp_ms=timestamps,
                                            cell_id=cell_ids),
            get_inbox=True,
            jitter=False # we already jittered
        )
        self._last_gmo = self._last_request
        self._map_cells = responses['GET_MAP_OBJECTS']['map_cells']
        return responses

    def req_encounter(self, encounter_id, spawn_point_id, latitude, longitude):
        return self.perform_request(lambda req: req.encounter(
            encounter_id=encounter_id,
            spawn_point_id=spawn_point_id,
            player_latitude=latitude,
            player_longitude=longitude), get_inbox=True)

    def req_catch_pokemon(self, encounter_id, spawn_point_id, ball,
                          normalized_reticle_size, spin_modifier):
        return self.perform_request(lambda req: req.catch_pokemon(
                encounter_id=encounter_id,
                pokeball=ball,
                normalized_reticle_size=normalized_reticle_size,
                spawn_point_id=spawn_point_id,
                hit_pokemon=1,
                spin_modifier=spin_modifier,
                normalized_hit_position=1.0), get_inbox=True)

    def req_release_pokemon(self, pokemon_id):
        return self.perform_request(
            lambda req: req.release_pokemon(pokemon_id=pokemon_id), get_inbox=True)

    def req_fort_search(self, fort_id, fort_lat, fort_lng, player_lat,
                        player_lng):
        return self.perform_request(lambda req: req.fort_search(
            fort_id=fort_id,
            fort_latitude=fort_lat,
            fort_longitude=fort_lng,
            player_latitude=player_lat,
            player_longitude=player_lng), get_inbox=True)

    def req_get_gym_details(self, gym_id, gym_lat, gym_lng, player_lat, player_lng):
        return self.perform_request(
            lambda req: req.get_gym_details(gym_id=gym_id,
                                            player_latitude=f2i(player_lat),
                                            player_longitude=f2i(player_lng),
                                            gym_latitude=gym_lat,
                                            gym_longitude=gym_lng,
                                            client_version=API_VERSION), get_inbox=True)

    def req_recycle_inventory_item(self, item_id, amount):
        return self.perform_request(lambda req: req.recycle_inventory_item(
            item_id=item_id,
            count=amount), get_inbox=True)

    def req_level_up_rewards(self, level):
        return self.perform_request(
            lambda req: req.level_up_rewards(level=level), get_inbox=True)

    def req_verify_challenge(self, captcha_token):
        req = self._api.create_request()
        req.verify_challenge(token=captcha_token)
        responses = self._call_request(req)
        if 'VERIFY_CHALLENGE' in responses:
            response = responses['VERIFY_CHALLENGE']
            if 'success' in response:
                self.captcha_url = None
                self.log_info("Successfully uncaptcha'd.")
                return True
            else:
                self.log_warning("Failed verifyChallenge")
                return False

    def get_forts(self):
        forts = []
        for cell in self._map_cells:
            for fort in enumerate(cell['forts']):
                forts.append(fort)

        return forts

    def get_pokemon(self):
        pokemons = []
        for cell in self._map_cells:
            for pokemon in enumerate(cell['catchable_pokemons']):
                pokemons.append(pokemon)

        return pokemons

    def get_spawn_points(self):
        spawns = []
        for cell in self._map_cells:
            for spawnpoint in enumerate(cell['spawn_points']):
                spawns.append(spawnpoint)

        return spawns

    # =======================================================================

    def _generate_device_info(self):
        identifier = self.username + self.password
        md5 = hashlib.md5()
        md5.update(identifier)
        pick_hash = int(md5.hexdigest(), 16)

        iphones = {
            'iPhone5,1': 'N41AP',
            'iPhone5,2': 'N42AP',
            'iPhone5,3': 'N48AP',
            'iPhone5,4': 'N49AP',
            'iPhone6,1': 'N51AP',
            'iPhone6,2': 'N53AP',
            'iPhone7,1': 'N56AP',
            'iPhone7,2': 'N61AP',
            'iPhone8,1': 'N71AP',
            'iPhone8,2': 'N66AP',
            'iPhone8,4': 'N69AP',
            'iPhone9,1': 'D10AP',
            'iPhone9,2': 'D11AP',
            'iPhone9,3': 'D101AP',
            'iPhone9,4': 'D111AP'
        }

        ios8 = ('8.0', '8.0.1', '8.0.2', '8.1', '8.1.1',
                '8.1.2', '8.1.3', '8.2', '8.3', '8.4', '8.4.1')
        ios9 = ('9.0', '9.0.1', '9.0.2', '9.1', '9.2', '9.2.1',
                '9.3', '9.3.1', '9.3.2', '9.3.3', '9.3.4', '9.3.5')
        ios10 = ('10.0', '10.0.1', '10.0.2', '10.0.3', '10.1', '10.1.1')

        device_info = {
            'device_brand': 'Apple',
            'device_model': 'iPhone',
            'hardware_manufacturer': 'Apple',
            'firmware_brand': 'iPhone OS'
        }

        devices = tuple(iphones.keys())
        device = devices[pick_hash % len(devices)]
        device_info['device_model_boot'] = device
        device_info['hardware_model'] = iphones[device]
        device_info['device_id'] = md5.hexdigest()

        if device.startswith('iPhone9'):
            ios_pool = ios10
        elif device.startswith('iPhone8'):
            ios_pool = ios9 + ios10
        else:
            ios_pool = ios8 + ios9 + ios10
        device_info['firmware_type'] = ios_pool[pick_hash % len(ios_pool)]

        self.log_info("Using an {} on iOS {} with device ID {}".format(device,
            device_info['firmware_type'], device_info['device_id']))

        return device_info

    def _call_request(self, request, action=None, jitter=True):
        # Wait until a previous user action gets completed
        if action:
            now = time.time()
            # wait for the time required, or at least a half-second
            if self._last_action > now + .5:
                time.sleep(self._last_action - now)
            else:
                time.sleep(0.5)

        if jitter:
            lat, lng = jitter_location(self.latitude, self.longitude)
            self._api.set_position(lat, lng, self.altitude)

        success = False
        while not success:
            try:
                # Set hash key for this request
                old_hash_key = self._hash_key
                self._hash_key = self._hash_key_provider.next()
                if self._hash_key != old_hash_key:
                    self.log_debug("Using hash key {}".format(self._hash_key))
                self._api.activate_hash_server(self._hash_key)

                response = request.call()
                self._last_request = time.time()
                success = True
            except HashingQuotaExceededException as e:
                if self.cfg['retry_on_hash_quota_exceeded'] or self.cfg['retry_on_hashing_error']:
                    self.log_warning("{}: Retrying in 5s.".format(repr(e)))
                    time.sleep(5)
                else:
                    raise
            except (HashingOfflineException, HashingTimeoutException) as e:
                if self.cfg['retry_on_hashing_error']:
                    self.log_warning("{}: Retrying in 5s.".format(repr(e)))
                    time.sleep(5)
                else:
                    raise

        # status_code 3 means BAD_REQUEST, so probably banned
        if 'status_code' in response and response['status_code'] == 3:
            self.log_warning("Got BAD_REQUEST response.")
            raise BannedAccountException

        if not 'responses' in response:
            self.log_error("Got no responses at all!")
            return {}

        # Set the timer when the user action will be completed
        if action:
            self._last_action = self._last_request + action

        # Return only the responses
        responses = response['responses']

        self._parse_responses(responses)

        return responses

    def _update_inventory_totals(self):
        ball_ids = [
            ITEM_POKE_BALL,
            ITEM_GREAT_BALL,
            ITEM_ULTRA_BALL,
            ITEM_MASTER_BALL
        ]
        balls = 0
        total_items = 0
        for item_id in self.inventory:
            if item_id in ball_ids:
                balls += self.inventory[item_id]
            total_items += self.inventory[item_id]
        self.inventory_balls = balls
        self.inventory_total = total_items

    def _parse_responses(self, responses):
        for response_type in responses.keys():
            response = responses[response_type]
            if response_type == 'GET_INVENTORY':
                api_inventory = response

                # Set an (empty) inventory if necessary
                if self.inventory is None:
                    self.inventory = {}

                # Update inventory (balls, items)
                inventory_delta = parse_inventory_delta(api_inventory)
                self.inventory.update(inventory_delta)
                self._update_inventory_totals()

                # Update stats (level, xp, encounters, captures, km walked, etc.)
                self.player_stats.update(parse_player_stats(api_inventory))

                # Update last timestamp for inventory requests
                self._last_timestamp_ms = api_inventory[
                    'inventory_delta'].get('new_timestamp_ms', 0)

                # Cleanup
                del responses[response_type]

            # Get settings hash from response for future calls
            elif response_type == 'DOWNLOAD_SETTINGS':
                if 'hash' in response:
                    self._download_settings_hash = response['hash']
                # TODO: Check forced client version and exit program if different

            elif response_type == 'GET_PLAYER':
                self.player_state = {
                    'tutorial_state': response.get('player_data', {}).get('tutorial_state', []),
                    'warn': response.get('warn', False),
                    'banned': response.get('banned', False)
                }
                if self.player_state.get('banned', False):
                    self.log_warning("GET_PLAYER has the 'banned' flag set.")
                    raise BannedAccountException

            # Check for captcha
            elif response_type == 'CHECK_CHALLENGE':
                self.captcha_url = response.get('challenge_url')
                if self.has_captcha():
                    raise CaptchaException

    def _initial_login_request_flow(self):
        self.log_info("Performing full login flow requests")

        # Empty request -----------------------------------------------------
        self.log_debug("Login Flow: Empty request")
        # ===== empty
        request = self._api.create_request()
        self._call_request(request)
        time.sleep(random.uniform(.43, .97))

        # Get player info ---------------------------------------------------
        self.log_debug("Login Flow: Get player state")
        # ===== GET_PLAYER (unchained)
        request = self._api.create_request()
        request.get_player(
            player_locale=self.cfg['player_locale'])
        self._call_request(request)
        time.sleep(random.uniform(.53, 1.1))

        # Download remote config --------------------------------------------
        self.log_debug("Login Flow: Downloading remote config")
        asset_time, template_time = self._download_remote_config_version()
        time.sleep(1)

        # Assets and item templates -----------------------------------------
        if self.cfg['download_assets_and_items'] and asset_time > self._asset_time:
            self.log_debug("Login Flow: Download asset digest")
            self._get_asset_digest(asset_time)
        else:
            self.log_debug("Login Flow: Skipping asset digest download")

        if self.cfg['download_assets_and_items'] and template_time > self._item_templates_time:
            self.log_debug("Login Flow: Download item templates")
            self._download_item_templates(template_time)
        else:
            self.log_debug("Login Flow: Skipping item template download")

        # TODO: Maybe download translation URLs from assets? Like pogonode?

        # Checking tutorial -------------------------------------------------
        if (self.player_state['tutorial_state'] is not None and
                not all(x in self.player_state['tutorial_state'] for x in
                        (0, 1, 3, 4, 7))):
            self.log_debug("Login Flow: Completing tutorial")
            self._complete_tutorial()
        else:
            # Get player profile
            self.log_debug("Login Flow: Get player profile")
            # ===== GET_PLAYER_PROFILE
            self.perform_request(lambda req: req.get_player_profile(),
                                 download_settings=True)
            time.sleep(random.uniform(.2, .3))

        # Level up rewards --------------------------------------------------
        self.log_debug("Login Flow: Get levelup rewards")
        # ===== LEVEL_UP_REWARDS
        self.perform_request(
            lambda req: req.level_up_rewards(level=self.player_stats['level']),
            download_settings=True)

        # Check store -------------------------------------------------------
        # TODO: There is currently no way to call the GET_STORE_ITEMS platform request.

        self.log_info('After-login procedure completed.')
        time.sleep(random.uniform(.5, 1.3))
        return True

    def _set_avatar(self, tutorial=False):
        player_avatar = avatar.new()
        # ===== LIST_AVATAR_CUSTOMIZATIONS
        self.perform_request(lambda req: req.list_avatar_customizations(
            avatar_type=player_avatar['avatar'],
#            slot=tuple(),
            filters=2), buddy_walked=not tutorial, action=5, get_inbox=False)
        time.sleep(random.uniform(7, 14))

        # ===== SET_AVATAR
        self.perform_request(
            lambda req: req.set_avatar(player_avatar=player_avatar),
            buddy_walked=not tutorial, action=2, get_inbox=False)

        if tutorial:
            time.sleep(random.uniform(.5, 4))

            # ===== MARK_TUTORIAL_COMPLETE
            self.perform_request(
                lambda req: req.mark_tutorial_complete(
                    tutorials_completed=1), buddy_walked=False, get_inbox=False)

            time.sleep(random.uniform(.5, 1))

        self.perform_request(
            lambda req: req.get_player_profile(), action=1, get_inbox=False)

    def _complete_tutorial(self):
        tutorial_state = self.player_state['tutorial_state']
        if 0 not in tutorial_state:
            # legal screen
            self.log_debug("Tutorial #0: Legal screen")
            # ===== MARK_TUTORIAL_COMPLETE
            self.perform_request(lambda req: req.mark_tutorial_complete(
                tutorials_completed=0), buddy_walked=False, get_inbox=False)
            time.sleep(random.uniform(.35, .525))

            # ===== GET_PLAYER
            self.perform_request(
                lambda req: req.get_player(
                    player_locale=self.cfg['player_locale']),
                buddy_walked=False, get_inbox=False)
            time.sleep(1)

        if 1 not in tutorial_state:
            # avatar selection
            self.log_debug("Tutorial #1: Avatar selection")
            self._set_avatar(tutorial=True)

        starter_id = None
        if 3 not in tutorial_state:
            # encounter tutorial
            self.log_debug("Tutorial #3: Catch starter Pokemon")
            time.sleep(random.uniform(.7, .9))
            # ===== GET_DOWNLOAD_URLS
            self.perform_request(lambda req: req.get_download_urls(asset_id=
                ['1a3c2816-65fa-4b97-90eb-0b301c064b7a/1487275569649000',
                'aa8f7687-a022-4773-b900-3a8c170e9aea/1487275581132582',
                 'e89109b0-9a54-40fe-8431-12f7826c8194/1487275593635524']), get_inbox=False)

            time.sleep(random.uniform(7, 10.3))
            starter = random.choice((1, 4, 7))
            # ===== ENCOUNTER_TUTORIAL_COMPLETE
            self.perform_request(lambda req: req.encounter_tutorial_complete(
                pokemon_id=starter), action=1, get_inbox=False)

            time.sleep(random.uniform(.4, .5))
            # ===== GET_PLAYER
            responses = self.perform_request(
                lambda req: req.get_player(player_locale=self.cfg['player_locale']), get_inbox=False)

            try:
                inventory = responses[
                    'GET_INVENTORY'].inventory_delta.inventory_items
                for item in inventory:
                    pokemon = item.inventory_item_data.pokemon_data
                    if pokemon.id:
                        starter_id = pokemon.id
                        break
            except (KeyError, TypeError):
                starter_id = None

        if 4 not in tutorial_state:
            # name selection
            self.log_debug("Tutorial #4: Set trainer name")
            time.sleep(random.uniform(12, 18))
            # ===== CLAIM_CODENAME
            self.perform_request(
                lambda req: req.claim_codename(codename=self.username),
                action=2, get_inbox=False)

            time.sleep(.7)
            # ===== GET_PLAYER
            self.perform_request(
                lambda req: req.get_player(player_locale=self.cfg['player_locale']), get_inbox=False)
            time.sleep(.13)

            # ===== MARK_TUTORIAL_COMPLETE
            self.perform_request(lambda req: req.mark_tutorial_complete(
                tutorials_completed=4), buddy_walked=False, get_inbox=False)

        if 7 not in tutorial_state:
            # first time experience
            self.log_debug("Tutorial #7: First time experience")
            time.sleep(random.uniform(3.9, 4.5))
            # ===== MARK_TUTORIAL_COMPLETE
            self.perform_request(lambda req: req.mark_tutorial_complete(
                tutorials_completed=7), get_inbox=False)

        # set starter as buddy
        if starter_id:
            self.log_debug("Setting buddy Pokemon")
            time.sleep(random.uniform(4, 5))
            # ===== SET_BUDDY_POKEMON
            self.perform_request(
                lambda req: req.set_buddy_pokemon(pokemon_id=starter_id),
                action=2, get_inbox=False)
            time.sleep(random.uniform(.8, 1.2))

        time.sleep(.2)
        return True

    def _download_remote_config_version(self):
        # ===== DOWNLOAD_REMOTE_CONFIG_VERSION
        responses = self.perform_request(
            lambda req: req.download_remote_config_version(platform=1,
                                                           app_version=APP_VERSION),
            download_settings=True, buddy_walked=False, get_inbox=False)
        if 'DOWNLOAD_REMOTE_CONFIG_VERSION' not in responses:
            raise Exception("Call to download_remote_config_version did not"
                            " return proper response.")
        remote_config = responses['DOWNLOAD_REMOTE_CONFIG_VERSION']
        return remote_config['asset_digest_timestamp_ms'] / 1000000, \
               remote_config['item_templates_timestamp_ms'] / 1000

    def _get_asset_digest(self, asset_time):
        i = random.randint(0, 3)
        result = 2
        page_offset = 0
        page_timestamp = 0
        while result == 2:
            # ===== GET_ASSET_DIGEST
            responses = self.perform_request(lambda req: req.get_asset_digest(
                platform=1,
                app_version=APP_VERSION,
                paginate=True,
                page_offset=page_offset,
                page_timestamp=page_timestamp), download_settings=True, buddy_walked=False, get_inbox=False)
            if i > 2:
                time.sleep(1.45)
                i = 0
            else:
                i += 1
                time.sleep(.2)
            try:
                response = responses['GET_ASSET_DIGEST']
            except KeyError:
                break
            result = response['result']
            page_offset = response.get('page_offset')
            page_timestamp = response['timestamp_ms']
        self._asset_time = asset_time

    def _download_item_templates(self, template_time):
        i = random.randint(0, 3)
        result = 2
        page_offset = 0
        page_timestamp = 0
        while result == 2:
            # ===== DOWNLOAD_ITEM_TEMPLATES
            responses = self.perform_request(lambda req: req.download_item_templates(
                paginate=True,
                page_offset=page_offset,
                page_timestamp=page_timestamp), download_settings=True, buddy_walked=False, get_inbox=False)
            if i > 2:
                time.sleep(1.5)
                i = 0
            else:
                i += 1
                time.sleep(.25)
            try:
                response = responses['DOWNLOAD_ITEM_TEMPLATES']
            except KeyError:
                break
            result = response['result']
            page_offset = response.get('page_offset')
            page_timestamp = response['timestamp_ms']
        self._item_templates_time = template_time

    def log_info(self, msg):
        self.last_msg = msg
        log.info("[{}] {}".format(self.username, msg))

    def log_debug(self, msg):
        self.last_msg = msg
        log.debug("[{}] {}".format(self.username, msg))

    def log_warning(self, msg):
        self.last_msg = msg
        log.warning("[{}] {}".format(self.username, msg))

    def log_error(self, msg):
        self.last_msg = msg
        log.error("[{}] {}".format(self.username, msg))