pogobuf/pogobuf.client.js
function Client(options) {
    if (!(this instanceof Client)) {
        return new Client(options);
    }
    const self = this;

    /*
     * PUBLIC METHODS
     */

     /**
      * Sets the specified client option to the given value.
      * Note that not all options support changes after client initialization.
      * @param {string} option - Option name
      * @param {any} value - Option value
      */
    this.setOption = function(option, value) {
        self.options[option] = value;
    };

    /**
     * Sets the player's latitude and longitude.
     * Note that this does not actually update the player location on the server, it only sets
     * the location to be used in following API calls. To update the location on the server you
     * need to make an API call.
     * @param {number|object} latitude - The player's latitude, or an object with parameters
     * @param {number} longitude - The player's longitude
     * @param {number} [accuracy=0] - The location accuracy in m
     * @param {number} [altitude=0] - The player's altitude
     */
    this.setPosition = function(latitude, longitude, accuracy, altitude) {
        if (typeof latitude === 'object') {
            const pos = latitude;
            latitude = pos.latitude;
            longitude = pos.longitude;
            accuracy = pos.accuracy;
            altitude = pos.altitude;
        }
        self.playerLatitude = latitude;
        self.playerLongitude = longitude;
        self.playerLocationAccuracy = accuracy || 0;
        self.playerAltitude = altitude || 0;
    };

    /**
     * Performs client initialization and downloads needed settings from the API and hashing server.
     * @param {boolean} [downloadSettings] - Deprecated, use downloadSettings option instead
     * @return {Promise} promise
     */
    this.init = function(downloadSettings) {
        // For backwards compatibility only
        if (typeof downloadSettings !== 'undefined') self.setOption('downloadSettings', downloadSettings);

        self.lastMapObjectsCall = 0;
        self.endpoint = INITIAL_ENDPOINT;

        // convert app version (5704) to client version (0.57.4)
        let signatureVersion = '0.' + ((+self.options.version) / 100).toFixed(0);
        signatureVersion += '.' + (+self.options.version % 100);

        Signature.signature.register(self, self.options.deviceId);

        self.signatureBuilder = new Signature.encryption.Builder({
            protos: POGOProtos,
            version: signatureVersion,
        });
        self.signatureBuilder.encryptAsync = Promise.promisify(self.signatureBuilder.encrypt,
                                                                { context: self.signatureBuilder });

        let promise = Promise.resolve(true);

        if (self.options.useHashingServer) {
            promise = promise.then(self.initializeHashingServer);
        }

        if (self.options.downloadSettings) {
            promise = promise.then(() => self.downloadSettings()).then(self.processSettingsResponse);
        }

        return promise;
    };

    /**
     * Sets batch mode. All further API requests will be held and executed in one RPC call when
     * {@link #batchCall} is called.
     * @return {Client} this
     */
    this.batchStart = function() {
        if (!self.batchRequests) {
            self.batchRequests = [];
        }
        return self;
    };

    /**
     * Clears the list of batched requests and aborts batch mode.
     */
    this.batchClear = function() {
        delete self.batchRequests;
    };

    /**
     * Executes any batched requests.
     * @return {Promise}
     */
    this.batchCall = function() {
        var p = self.callRPC(self.batchRequests || []);
        self.batchClear();
        return p;
    };

    /**
     * Gets rate limit info from the latest signature server request, if applicable.
     * @return {Object}
     */
    this.getSignatureRateInfo = function() {
        return self.signatureBuilder.rateInfos;
    };

    /*
     * API CALLS (in order of RequestType enum)
     */

    this.getPlayer = function(country, language, timezone) {
        return self.callOrChain({
            type: RequestType.GET_PLAYER,
            message: new RequestMessages.GetPlayerMessage({
                player_locale: {
                    country: country,
                    language: language,
                    timezone: timezone
                }
            }),
            responseType: Responses.GetPlayerResponse
        });
    };

    this.getInventory = function(lastTimestamp) {
        return self.callOrChain({
            type: RequestType.GET_INVENTORY,
            message: new RequestMessages.GetInventoryMessage({
                last_timestamp_ms: lastTimestamp
            }),
            responseType: Responses.GetInventoryResponse
        });
    };

    this.downloadSettings = function(hash) {
        return self.callOrChain({
            type: RequestType.DOWNLOAD_SETTINGS,
            message: new RequestMessages.DownloadSettingsMessage({
                hash: hash
            }),
            responseType: Responses.DownloadSettingsResponse
        });
    };

    this.downloadItemTemplates = function(paginate, pageOffset, pageTimestamp) {
        return self.callOrChain({
            type: RequestType.DOWNLOAD_ITEM_TEMPLATES,
            message: new RequestMessages.DownloadItemTemplatesMessage({
                paginate: paginate,
                page_offset: pageOffset,
                page_timestamp: pageTimestamp
            }),
            responseType: Responses.DownloadItemTemplatesResponse
        });
    };

    this.downloadRemoteConfigVersion = function(platform, deviceManufacturer, deviceModel, locale, appVersion) {
        return self.callOrChain({
            type: RequestType.DOWNLOAD_REMOTE_CONFIG_VERSION,
            message: new RequestMessages.DownloadRemoteConfigVersionMessage({
                platform: platform,
                device_manufacturer: deviceManufacturer,
                device_model: deviceModel,
                locale: locale,
                app_version: appVersion
            }),
            responseType: Responses.DownloadRemoteConfigVersionResponse
        });
    };

    this.registerBackgroundDevice = function(deviceType, deviceID) {
        return self.callOrChain({
            type: RequestType.REGISTER_BACKGROUND_DEVICE,
            message: new RequestMessages.RegisterBackgroundDeviceMessage({
                device_type: deviceType,
                device_id: deviceID
            }),
            responseType: Responses.RegisterBackgroundDeviceResponse
        });
    };

    this.fortSearch = function(fortID, fortLatitude, fortLongitude) {
        return self.callOrChain({
            type: RequestType.FORT_SEARCH,
            message: new RequestMessages.FortSearchMessage({
                fort_id: fortID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude,
                fort_latitude: fortLatitude,
                fort_longitude: fortLongitude
            }),
            responseType: Responses.FortSearchResponse
        });
    };

    this.encounter = function(encounterID, spawnPointID) {
        return self.callOrChain({
            type: RequestType.ENCOUNTER,
            message: new RequestMessages.EncounterMessage({
                encounter_id: encounterID,
                spawn_point_id: spawnPointID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.EncounterResponse
        });
    };

    this.catchPokemon = function(encounterID, pokeballItemID, normalizedReticleSize, spawnPointID, hitPokemon,
        spinModifier, normalizedHitPosition) {
        return self.callOrChain({
            type: RequestType.CATCH_POKEMON,
            message: new RequestMessages.CatchPokemonMessage({
                encounter_id: encounterID,
                pokeball: pokeballItemID,
                normalized_reticle_size: normalizedReticleSize,
                spawn_point_id: spawnPointID,
                hit_pokemon: hitPokemon,
                spin_modifier: spinModifier,
                normalized_hit_position: normalizedHitPosition
            }),
            responseType: Responses.CatchPokemonResponse
        });
    };

    this.fortDetails = function(fortID, fortLatitude, fortLongitude) {
        return self.callOrChain({
            type: RequestType.FORT_DETAILS,
            message: new RequestMessages.FortDetailsMessage({
                fort_id: fortID,
                latitude: fortLatitude,
                longitude: fortLongitude
            }),
            responseType: Responses.FortDetailsResponse
        });
    };

    this.getMapObjects = function(cellIDs, sinceTimestamps) {
        return self.callOrChain({
            type: RequestType.GET_MAP_OBJECTS,
            message: new RequestMessages.GetMapObjectsMessage({
                cell_id: cellIDs,
                since_timestamp_ms: sinceTimestamps,
                latitude: self.playerLatitude,
                longitude: self.playerLongitude
            }),
            responseType: Responses.GetMapObjectsResponse
        });
    };

    this.fortDeployPokemon = function(fortID, pokemonID) {
        return self.callOrChain({
            type: RequestType.FORT_DEPLOY_POKEMON,
            message: new RequestMessages.FortDeployPokemonMessage({
                fort_id: fortID,
                pokemon_id: pokemonID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.FortDeployPokemonResponse
        });
    };

    this.fortRecallPokemon = function(fortID, pokemonID) {
        return self.callOrChain({
            type: RequestType.FORT_RECALL_POKEMON,
            message: new RequestMessages.FortRecallPokemonMessage({
                fort_id: fortID,
                pokemon_id: pokemonID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.FortRecallPokemonResponse
        });
    };

    this.releasePokemon = function(pokemonIDs) {
        if (!Array.isArray(pokemonIDs)) pokemonIDs = [pokemonIDs];

        return self.callOrChain({
            type: RequestType.RELEASE_POKEMON,
            message: new RequestMessages.ReleasePokemonMessage({
                pokemon_id: pokemonIDs.length === 1 ? pokemonIDs[0] : undefined,
                pokemon_ids: pokemonIDs.length > 1 ? pokemonIDs : undefined
            }),
            responseType: Responses.ReleasePokemonResponse
        });
    };

    this.useItemPotion = function(itemID, pokemonID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_POTION,
            message: new RequestMessages.UseItemPotionMessage({
                item_id: itemID,
                pokemon_id: pokemonID
            }),
            responseType: Responses.UseItemPotionResponse
        });
    };

    this.useItemCapture = function(itemID, encounterID, spawnPointID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_CAPTURE,
            message: new RequestMessages.UseItemCaptureMessage({
                item_id: itemID,
                encounter_id: encounterID,
                spawn_point_id: spawnPointID
            }),
            responseType: Responses.UseItemCaptureResponse
        });
    };

    this.useItemRevive = function(itemID, pokemonID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_REVIVE,
            message: new RequestMessages.UseItemReviveMessage({
                item_id: itemID,
                pokemon_id: pokemonID
            }),
            responseType: Responses.UseItemReviveResponse
        });
    };

    this.getPlayerProfile = function(playerName) {
        return self.callOrChain({
            type: RequestType.GET_PLAYER_PROFILE,
            message: new RequestMessages.GetPlayerProfileMessage({
                player_name: playerName
            }),
            responseType: Responses.GetPlayerProfileResponse
        });
    };

    this.evolvePokemon = function(pokemonID, evolutionRequirementItemID) {
        return self.callOrChain({
            type: RequestType.EVOLVE_POKEMON,
            message: new RequestMessages.EvolvePokemonMessage({
                pokemon_id: pokemonID,
                evolution_item_requirement: evolutionRequirementItemID
            }),
            responseType: Responses.EvolvePokemonResponse
        });
    };

    this.getHatchedEggs = function() {
        return self.callOrChain({
            type: RequestType.GET_HATCHED_EGGS,
            responseType: Responses.GetHatchedEggsResponse
        });
    };

    this.encounterTutorialComplete = function(pokemonID) {
        return self.callOrChain({
            type: RequestType.ENCOUNTER_TUTORIAL_COMPLETE,
            message: new RequestMessages.EncounterTutorialCompleteMessage({
                pokemon_id: pokemonID
            }),
            responseType: Responses.EncounterTutorialCompleteResponse
        });
    };

    this.levelUpRewards = function(level) {
        return self.callOrChain({
            type: RequestType.LEVEL_UP_REWARDS,
            message: new RequestMessages.LevelUpRewardsMessage({
                level: level
            }),
            responseType: Responses.LevelUpRewardsResponse
        });
    };

    this.checkAwardedBadges = function() {
        return self.callOrChain({
            type: RequestType.CHECK_AWARDED_BADGES,
            responseType: Responses.CheckAwardedBadgesResponse
        });
    };

    this.useItemGym = function(itemID, gymID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_GYM,
            message: new RequestMessages.UseItemGymMessage({
                item_id: itemID,
                gym_id: gymID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.UseItemGymResponse
        });
    };

    this.getGymDetails = function(gymID, gymLatitude, gymLongitude, clientVersion) {
        return self.callOrChain({
            type: RequestType.GET_GYM_DETAILS,
            message: new RequestMessages.GetGymDetailsMessage({
                gym_id: gymID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude,
                gym_latitude: gymLatitude,
                gym_longitude: gymLongitude,
                client_version: clientVersion
            }),
            responseType: Responses.GetGymDetailsResponse
        });
    };

    this.startGymBattle = function(gymID, attackingPokemonIDs, defendingPokemonID) {
        return self.callOrChain({
            type: RequestType.START_GYM_BATTLE,
            message: new RequestMessages.StartGymBattleMessage({
                gym_id: gymID,
                attacking_pokemon_ids: attackingPokemonIDs,
                defending_pokemon_id: defendingPokemonID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.StartGymBattleResponse
        });
    };

    this.attackGym = function(gymID, battleID, attackActions, lastRetrievedAction) {
        return self.callOrChain({
            type: RequestType.ATTACK_GYM,
            message: new RequestMessages.AttackGymMessage({
                gym_id: gymID,
                battle_id: battleID,
                attack_actions: attackActions,
                last_retrieved_action: lastRetrievedAction,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.AttackGymResponse
        });
    };

    this.recycleInventoryItem = function(itemID, count) {
        return self.callOrChain({
            type: RequestType.RECYCLE_INVENTORY_ITEM,
            message: new RequestMessages.RecycleInventoryItemMessage({
                item_id: itemID,
                count: count
            }),
            responseType: Responses.RecycleInventoryItemResponse
        });
    };

    this.collectDailyBonus = function() {
        return self.callOrChain({
            type: RequestType.COLLECT_DAILY_BONUS,
            responseType: Responses.CollectDailyBonusResponse
        });
    };

    this.useItemXPBoost = function(itemID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_XP_BOOST,
            message: new RequestMessages.UseItemXpBoostMessage({
                item_id: itemID
            }),
            responseType: Responses.UseItemXpBoostResponse
        });
    };

    this.useItemEggIncubator = function(itemID, pokemonID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_EGG_INCUBATOR,
            message: new RequestMessages.UseItemEggIncubatorMessage({
                item_id: itemID,
                pokemon_id: pokemonID
            }),
            responseType: Responses.UseItemEggIncubatorResponse
        });
    };

    this.useIncense = function(itemID) {
        return self.callOrChain({
            type: RequestType.USE_INCENSE,
            message: new RequestMessages.UseIncenseMessage({
                incense_type: itemID
            }),
            responseType: Responses.UseIncenseResponse
        });
    };

    this.getIncensePokemon = function() {
        return self.callOrChain({
            type: RequestType.GET_INCENSE_POKEMON,
            message: new RequestMessages.GetIncensePokemonMessage({
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.GetIncensePokmeonResponse
        });
    };

    this.incenseEncounter = function(encounterID, encounterLocation) {
        return self.callOrChain({
            type: RequestType.INCENSE_ENCOUNTER,
            message: new RequestMessages.IncenseEncounterMessage({
                encounter_id: encounterID,
                encounter_location: encounterLocation
            }),
            responseType: Responses.IncenseEncounterResponse
        });
    };

    this.addFortModifier = function(modifierItemID, fortID) {
        return self.callOrChain({
            type: RequestType.ADD_FORT_MODIFIER,
            message: new RequestMessages.AddFortModifierMessage({
                modifier_type: modifierItemID,
                fort_id: fortID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            })
        });
    };

    this.diskEncounter = function(encounterID, fortID) {
        return self.callOrChain({
            type: RequestType.DISK_ENCOUNTER,
            message: new RequestMessages.DiskEncounterMessage({
                encounter_id: encounterID,
                fort_id: fortID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.DiskEncounterResponse
        });
    };

    this.collectDailyDefenderBonus = function() {
        return self.callOrChain({
            type: RequestType.COLLECT_DAILY_DEFENDER_BONUS,
            responseType: Responses.CollectDailyDefenderBonusResponse
        });
    };

    this.upgradePokemon = function(pokemonID) {
        return self.callOrChain({
            type: RequestType.UPGRADE_POKEMON,
            message: new RequestMessages.UpgradePokemonMessage({
                pokemon_id: pokemonID
            }),
            responseType: Responses.UpgradePokemonResponse
        });
    };

    this.setFavoritePokemon = function(pokemonID, isFavorite) {
        return self.callOrChain({
            type: RequestType.SET_FAVORITE_POKEMON,
            message: new RequestMessages.SetFavoritePokemonMessage({
                pokemon_id: pokemonID,
                is_favorite: isFavorite
            }),
            responseType: Responses.SetFavoritePokemonResponse
        });
    };

    this.nicknamePokemon = function(pokemonID, nickname) {
        return self.callOrChain({
            type: RequestType.NICKNAME_POKEMON,
            message: new RequestMessages.NicknamePokemonMessage({
                pokemon_id: pokemonID,
                nickname: nickname
            }),
            responseType: Responses.NicknamePokemonResponse
        });
    };

    this.equipBadge = function(badgeType) {
        return self.callOrChain({
            type: RequestType.EQUIP_BADGE,
            message: new RequestMessages.EquipBadgeMessage({
                badge_type: badgeType
            }),
            responseType: Responses.EquipBadgeResponse
        });
    };

    this.setContactSettings = function(sendMarketingEmails, sendPushNotifications) {
        return self.callOrChain({
            type: RequestType.SET_CONTACT_SETTINGS,
            message: new RequestMessages.SetContactSettingsMessage({
                contact_settings: {
                    send_marketing_emails: sendMarketingEmails,
                    send_push_notifications: sendPushNotifications
                }
            }),
            responseType: Responses.SetContactSettingsResponse
        });
    };

    this.setBuddyPokemon = function(pokemonID) {
        return self.callOrChain({
            type: RequestType.SET_BUDDY_POKEMON,
            message: new RequestMessages.SetBuddyPokemonMessage({
                pokemon_id: pokemonID
            }),
            responseType: Responses.SetBuddyPokemonResponse
        });
    };

    this.getBuddyWalked = function() {
        return self.callOrChain({
            type: RequestType.GET_BUDDY_WALKED,
            responseType: Responses.GetBuddyWalkedResponse
        });
    };

    this.useItemEncounter = function(itemID, encounterID, spawnPointGUID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_ENCOUNTER,
            message: new RequestMessages.UseItemEncounterMessage({
                item: itemID,
                encounter_id: encounterID,
                spawn_point_guid: spawnPointGUID
            }),
            responseType: Responses.UseItemEncounterResponse
        });
    };

    this.getAssetDigest = function(platform, deviceManufacturer, deviceModel, locale, appVersion) {
        return self.callOrChain({
            type: RequestType.GET_ASSET_DIGEST,
            message: new RequestMessages.GetAssetDigestMessage({
                platform: platform,
                device_manufacturer: deviceManufacturer,
                device_model: deviceModel,
                locale: locale,
                app_version: appVersion
            }),
            responseType: Responses.GetAssetDigestResponse
        });
    };

    this.getDownloadURLs = function(assetIDs) {
        return self.callOrChain({
            type: RequestType.GET_DOWNLOAD_URLS,
            message: new RequestMessages.GetDownloadUrlsMessage({
                asset_id: assetIDs
            }),
            responseType: Responses.GetDownloadUrlsResponse
        });
    };

    this.claimCodename = function(codename) {
        return self.callOrChain({
            type: RequestType.CLAIM_CODENAME,
            message: new RequestMessages.ClaimCodenameMessage({
                codename: codename
            }),
            responseType: Responses.ClaimCodenameResponse
        });
    };

    this.setAvatar = function(playerAvatar) {
        return self.callOrChain({
            type: RequestType.SET_AVATAR,
            message: new RequestMessages.SetAvatarMessage({
                player_avatar: playerAvatar
            }),
            responseType: Responses.SetAvatarResponse
        });
    };

    this.setPlayerTeam = function(teamColor) {
        return self.callOrChain({
            type: RequestType.SET_PLAYER_TEAM,
            message: new RequestMessages.SetPlayerTeamMessage({
                team: teamColor
            }),
            responseType: Responses.SetPlayerTeamResponse
        });
    };

    this.markTutorialComplete = function(tutorialsCompleted, sendMarketingEmails, sendPushNotifications) {
        return self.callOrChain({
            type: RequestType.MARK_TUTORIAL_COMPLETE,
            message: new RequestMessages.MarkTutorialCompleteMessage({
                tutorials_completed: tutorialsCompleted,
                send_marketing_emails: sendMarketingEmails,
                send_push_notifications: sendPushNotifications
            }),
            responseType: Responses.MarkTutorialCompleteResponse
        });
    };

    this.checkChallenge = function(isDebugRequest) {
        return self.callOrChain({
            type: RequestType.CHECK_CHALLENGE,
            message: new RequestMessages.CheckChallengeMessage({
                debug_request: isDebugRequest
            }),
            responseType: Responses.CheckChallengeResponse
        });
    };

    this.verifyChallenge = function(token) {
        return self.callOrChain({
            type: RequestType.VERIFY_CHALLENGE,
            message: new RequestMessages.VerifyChallengeMessage({
                token: token
            }),
            responseType: Responses.VerifyChallengeResponse
        });
    };

    this.echo = function() {
        return self.callOrChain({
            type: RequestType.ECHO,
            responseType: Responses.EchoResponse
        });
    };

    this.sfidaActionLog = function() {
        return self.callOrChain({
            type: RequestType.SFIDA_ACTION_LOG,
            responseType: Responses.SfidaActionLogResponse
        });
    };

    this.listAvatarCustomizations = function(avatarType, slots, filters, start, limit) {
        return self.callOrChain({
            type: RequestType.LIST_AVATAR_CUSTOMIZATIONS,
            message: new RequestMessages.ListAvatarCustomizationsMessage({
                avatar_type: avatarType,
                slot: slots,
                filters: filters,
                start: start,
                limit: limit
            }),
            responseType: Responses.ListAvatarCustomizationsResponse
        });
    };

    this.setAvatarItemAsViewed = function(avatarTemplateIDs) {
        return self.callOrChain({
            type: RequestType.SET_AVATAR_ITEM_AS_VIEWED,
            message: new RequestMessages.SetAvatarItemAsViewedMessage({
                avatar_template_id: avatarTemplateIDs
            }),
            responseType: Responses.SetAvatarItemAsViewdResponse
        });
    };

    /*
     * INTERNAL STUFF
     */

    this.request = request.defaults({
        headers: {
            'User-Agent': 'Niantic App',
            'Accept': '*/*',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        encoding: null
    });

    this.options = Object.assign({}, defaultOptions, options || {});
    this.authTicket = null;
    this.rpcId = 2;
    this.lastHashingKeyIndex = 0;
    this.firstGetMapObjects = true;
    this.lehmer = new Lehmer(16807);
    this.ptr8 = INITIAL_PTR8;

    /**
     * Executes a request and returns a Promise or, if we are in batch mode, adds it to the
     * list of batched requests and returns this (for chaining).
     * @private
     * @param {object} requestMessage - RPC request object
     * @return {Promise|Client}
     */
    this.callOrChain = function(requestMessage) {
        if (self.batchRequests) {
            self.batchRequests.push(requestMessage);
            return self;
        } else {
            return self.callRPC([requestMessage]);
        }
    };

    /**
     * Generates next rpc request id
     * @private
     * @return {Long}
     */
    this.getRequestID = function() {
        return new Long(self.rpcId++, this.lehmer.nextInt());
    };

    /**
     * Creates an RPC envelope with the given list of requests.
     * @private
     * @param {Object[]} requests - Array of requests to build
     * @return {POGOProtos.Networking.Envelopes.RequestEnvelope}
     */
    this.buildEnvelope = function(requests) {
        var envelopeData = {
            status_code: 2,
            request_id: self.getRequestID(),
            ms_since_last_locationfix: 100 + Math.floor(Math.random() * 900)
        };

        if (self.playerLatitude) envelopeData.latitude = self.playerLatitude;
        if (self.playerLongitude) envelopeData.longitude = self.playerLongitude;
        if (self.playerLocationAccuracy) {
            envelopeData.accuracy = self.playerLocationAccuracy;
        } else {
            const values = [5, 5, 5, 5, 10, 10, 10, 30, 30, 50, 65];
            values.unshift(Math.floor(Math.random() * (80 - 66)) + 66);
            envelopeData.accuracy = values[Math.floor(values.length * Math.random())];
        }

        if (self.authTicket) {
            envelopeData.auth_ticket = self.authTicket;
        } else if (!self.options.authType || !self.options.authToken) {
            throw Error('No auth info provided');
        } else {
            let unknown2 = 0;
            if (self.options.authType === 'ptc') {
                const values = [2, 8, 21, 21, 21, 28, 37, 56, 59, 59, 59];
                unknown2 = values[Math.floor(values.length * Math.random())];
            }
            envelopeData.auth_info = {
                provider: self.options.authType,
                token: {
                    contents: self.options.authToken,
                    unknown2: unknown2,
                }
            };
        }

        if (requests) {
            self.emit('request', {
                request_id: envelopeData.request_id.toString(),
                requests: requests.map(r => ({
                    name: Utils.getEnumKeyByValue(RequestType, r.type),
                    type: r.type,
                    data: r.message
                }))
            });

            envelopeData.requests = requests.map(r => {
                var requestData = {
                    request_type: r.type
                };

                if (r.message) {
                    requestData.request_message = r.message.encode();
                }

                return requestData;
            });
        }

        self.emit('raw-request', envelopeData);

        return new POGOProtos.Networking.Envelopes.RequestEnvelope(envelopeData);
    };

    /**
     * Constructs and adds a platform request to a request envelope.
     * @private
     * @param {RequestEnvelope} envelope - Request envelope
     * @param {PlatformRequestType} requestType - Type of the platform request to add
     * @param {Object} requestMessage - Pre-built but not encoded PlatformRequest protobuf message
     * @return {RequestEnvelope} The envelope (for convenience only)
     */
    this.addPlatformRequestToEnvelope = function(envelope, requestType, requestMessage) {
        envelope.platform_requests.push(
            new POGOProtos.Networking.Envelopes.RequestEnvelope.PlatformRequest({
                type: requestType,
                request_message: requestMessage.encode()
            })
        );

        return envelope;
    };

    /**
     * Determines whether the as of yet unknown platform request type 8 should be added
     * to the envelope based on the given type of requests.
     * @private
     * @param {Object[]} requests - Array of request data
     * @return {boolean}
     */
    this.needsPtr8 = function(requests) {
        // Single GET_PLAYER request always gets PTR8
        if (requests.length === 1 && requests[0].type === RequestType.GET_PLAYER) {
            return true;
        }

        // Any GET_MAP_OBJECTS requests get PTR8 except the first one in the session
        if (requests.some(r => r.type === RequestType.GET_MAP_OBJECTS)) {
            if (self.firstGetMapObjects) {
                self.firstGetMapObjects = false;
                return false;
            }

            return true;
        }

        return false;
    };

    /**
     * Creates an RPC envelope with the given list of requests and adds the encrypted signature,
     * or adds the signature to an existing envelope.
     * @private
     * @param {Object[]} requests - Array of requests to build
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to sign
     * @return {Promise} - A Promise that will be resolved with a RequestEnvelope instance
     */
    this.buildSignedEnvelope = function(requests, envelope) {
        if (!envelope) {
            try {
                envelope = self.buildEnvelope(requests);
            } catch (e) {
                throw new retry.StopError(e);
            }
        }

        if (self.needsPtr8(requests)) {
            self.addPlatformRequestToEnvelope(envelope, PlatformRequestType.UNKNOWN_PTR_8,
                new PlatformRequestMessages.UnknownPtr8Request({
                    message: self.ptr8,
                }));
        }

        let authTicket = envelope.auth_ticket;
        if (!authTicket) {
            authTicket = envelope.auth_info;
        }

        if (!authTicket) {
            // Can't sign before we have received an auth ticket
            return Promise.resolve(envelope);
        }

        if (self.options.useHashingServer) {
            let key = self.options.hashingKey;
            if (Array.isArray(key)) {
                key = key[self.lastHashingKeyIndex];
                self.lastHashingKeyIndex = (self.lastHashingKeyIndex + 1) % self.options.hashingKey.length;
            }

            self.signatureBuilder.useHashingServer(self.options.hashingServer + self.hashingVersion, key);
        }

        self.signatureBuilder.setAuthTicket(authTicket);

        if (typeof self.options.signatureInfo === 'function') {
            self.signatureBuilder.setFields(self.options.signatureInfo(envelope));
        } else if (self.options.signatureInfo) {
            self.signatureBuilder.setFields(self.options.signatureInfo);
        }

        self.signatureBuilder.setLocation(envelope.latitude, envelope.longitude, envelope.accuracy);

        return retry(() => self.signatureBuilder.encryptAsync(envelope.requests)
                        .catch(err => {
                            if (err.name === 'HashServerError' && err.message === 'Request limited') {
                                throw err;
                            } else {
                                throw new retry.StopError(err);
                            }
                        }),
            {
                interval: 1000,
                backoff: 2,
                max_tries: 10,
                args: envelope.requests,
            })
            .then(sigEncrypted =>
                self.addPlatformRequestToEnvelope(envelope, PlatformRequestType.SEND_ENCRYPTED_SIGNATURE,
                    new PlatformRequestMessages.SendEncryptedSignatureRequest({
                        encrypted_signature: sigEncrypted
                    })
                )
            );
    };

    /**
     * Executes an RPC call with the given list of requests, retrying if necessary.
     * @private
     * @param {Object[]} requests - Array of requests to send
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to use
     * @return {Promise} - A Promise that will be resolved with the (list of) response messages,
     *     or true if there aren't any
     */
    this.callRPC = function(requests, envelope) {
        // If the requests include a map objects request, make sure the minimum delay
        // since the last call has passed
        if (requests.some(r => r.type === RequestType.GET_MAP_OBJECTS)) {
            var now = new Date().getTime(),
                delayNeeded = self.lastMapObjectsCall + self.options.mapObjectsMinDelay - now;

            if (delayNeeded > 0 && self.options.mapObjectsThrottling) {
                return Promise.delay(delayNeeded).then(() => self.callRPC(requests, envelope));
            }

            self.lastMapObjectsCall = now;
        }

        if (self.options.maxTries <= 1) return self.tryCallRPC(requests, envelope);

        return retry(() => self.tryCallRPC(requests, envelope), {
            interval: 300,
            backoff: 2,
            max_tries: self.options.maxTries
        });
    };

    /**
     * Handle redirection to new API endpoint and resend last request to new endpoint.
     * @private
     * @param {Object[]} requests - Array of requests
     * @param {RequestEnvelope} signedEnvelope - Request envelope
     * @param {ResponseEnvelope} responseEnvelope - Result from API call
     * @return {Promise}
     */
    this.redirect = function(requests, signedEnvelope, responseEnvelope) {
        return new Promise((resolve, reject) => {
            if (!responseEnvelope.api_url) {
                reject(Error('Fetching RPC endpoint failed, none supplied in response'));
                return;
            }

            self.endpoint = 'https://' + responseEnvelope.api_url + '/rpc';

            self.emit('endpoint-response', {
                status_code: responseEnvelope.status_code,
                request_id: responseEnvelope.request_id.toString(),
                api_url: responseEnvelope.api_url
            });

            signedEnvelope.platform_requests = [];
            resolve(self.callRPC(requests, signedEnvelope));
        });
    };

    /**
     * Executes an RPC call with the given list of requests.
     * @private
     * @param {Object[]} requests - Array of requests to send
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to use
     * @return {Promise} - A Promise that will be resolved with the (list of) response messages,
     *     or true if there aren't any
     */
    this.tryCallRPC = function(requests, envelope) {
        return self.buildSignedEnvelope(requests, envelope)
            .then(signedEnvelope => new Promise((resolve, reject) => {
                self.request({
                    method: 'POST',
                    url: self.endpoint,
                    proxy: self.options.proxy,
                    body: signedEnvelope.toBuffer()
                }, (err, response, body) => {
                    if (err) {
                        reject(Error(err));
                        return;
                    }

                    if (response.statusCode !== 200) {
                        if (response.statusCode >= 400 && response.statusCode < 500) {
                            /* These are permanent errors so throw StopError */
                            reject(new retry.StopError(
                                `Status code ${response.statusCode} received from HTTPS request`
                            ));
                        } else {
                            /* Anything else might be recoverable so throw regular Error */
                            reject(Error(
                                `Status code ${response.statusCode} received from HTTPS request`
                            ));
                        }
                        return;
                    }

                    var responseEnvelope;
                    try {
                        responseEnvelope =
                            POGOProtos.Networking.Envelopes.ResponseEnvelope.decode(body);
                    } catch (e) {
                        self.emit('parse-envelope-error', body, e);
                        if (e.decoded) {
                            responseEnvelope = e.decoded;
                        } else {
                            reject(new retry.StopError(e));
                            return;
                        }
                    }

                    self.emit('raw-response', responseEnvelope);

                    if (responseEnvelope.error) {
                        reject(new retry.StopError(responseEnvelope.error));
                        return;
                    }

                    if (responseEnvelope.auth_ticket) self.authTicket = responseEnvelope.auth_ticket;

                    if (responseEnvelope.status_code === 53 ||
                        (responseEnvelope.status_code === 2 && self.endpoint === INITIAL_ENDPOINT)) {
                        resolve(self.redirect(requests, signedEnvelope, responseEnvelope));
                        return;
                    }

                    responseEnvelope.platform_returns.forEach(platformReturn => {
                        if (platformReturn.type === PlatformRequestType.UNKNOWN_PTR_8) {
                            const ptr8 = PlatformResponses.UnknownPtr8Response.decode(platformReturn.response);
                            if (ptr8) self.ptr8 = ptr8.message;
                        }
                    });

                    /* Throttling, retry same request later */
                    if (responseEnvelope.status_code === 52) {
                        signedEnvelope.platform_requests = [];
                        Promise.delay(2000).then(() => {
                            resolve(self.callRPC(requests, signedEnvelope));
                        });
                        return;
                    }

                    /* These codes indicate invalid input, no use in retrying so throw StopError */
                    if (responseEnvelope.status_code === 3 || responseEnvelope.status_code === 51 ||
                        responseEnvelope.status_code >= 100) {
                        reject(new retry.StopError(
                            `Status code ${responseEnvelope.status_code} received from RPC`
                        ));
                    }

                    /* These can be temporary so throw regular Error */
                    if (responseEnvelope.status_code !== 2 && responseEnvelope.status_code !== 1) {
                        reject(Error(
                            `Status code ${responseEnvelope.status_code} received from RPC`
                        ));
                        return;
                    }

                    var responses = [];

                    if (requests) {
                        if (requests.length !== responseEnvelope.returns.length) {
                            reject(Error('Request count does not match response count'));
                            return;
                        }

                        for (var i = 0; i < responseEnvelope.returns.length; i++) {
                            if (!requests[i].responseType) continue;

                            var responseMessage;
                            try {
                                responseMessage = requests[i].responseType.decode(
                                    responseEnvelope.returns[i]
                                );
                            } catch (e) {
                                self.emit('parse-response-error',
                                    responseEnvelope.returns[i].toBuffer(), e);
                                reject(new retry.StopError(e));
                                return;
                            }

                            if (self.options.includeRequestTypeInResponse) {
                                // eslint-disable-next-line no-underscore-dangle
                                responseMessage._requestType = requests[i].type;
                            }
                            responses.push(responseMessage);
                        }
                    }

                    self.emit('response', {
                        status_code: responseEnvelope.status_code,
                        request_id: responseEnvelope.request_id.toString(),
                        responses: responses.map((r, h) => ({
                            name: Utils.getEnumKeyByValue(
                                RequestType, requests[h].type
                            ),
                            type: requests[h].type,
                            data: r
                        }))
                    });

                    if (self.options.automaticLongConversion) {
                        responses = Utils.convertLongs(responses);
                    }

                    if (!responses.length) resolve(true);
                    else if (responses.length === 1) resolve(responses[0]);
                    else resolve(responses);
                });
            }));
    };

    /**
     * Processes the data received from the downloadSettings API call during init().
     * @private
     * @param {Object} settingsResponse - Response from API call
     * @return {Object} response - Unomdified response (to send back to Promise)
     */
    this.processSettingsResponse = function(settingsResponse) {
        // Extract the minimum delay of getMapObjects()
        if (settingsResponse &&
            !settingsResponse.error &&
            settingsResponse.settings &&
            settingsResponse.settings.map_settings &&
            settingsResponse.settings.map_settings.get_map_objects_min_refresh_seconds
        ) {
            self.setOption('mapObjectsMinDelay',
                settingsResponse.settings.map_settings.get_map_objects_min_refresh_seconds * 1000);
        }
        return settingsResponse;
    };

    /**
     * Makes an initial call to the hashing server to verify API version.
     * @private
     * @return {Promise}
     */
    this.initializeHashingServer = function() {
        if (!self.options.hashingServer) throw new Error('Hashing server enabled without host');
        if (!self.options.hashingKey) throw new Error('Hashing server enabled without key');

        if (self.options.hashingServer.slice(-1) !== '/') {
            self.setOption('hashingServer', self.options.hashingServer + '/');
        }

        return Signature.versions.getHashingEndpoint(self.options.hashingServer, self.options.version)
                .then(version => {
                    self.hashingVersion = version;
                });
    };

    /*
     * DEPRECATED METHODS
     */

    /**
     * Sets the authType and authToken options.
     * @deprecated Use options object or setOption() instead
     * @param {string} authType
     * @param {string} authToken
     */
    this.setAuthInfo = function(authType, authToken) {
        self.setOption('authType', authType);
        self.setOption('authToken', authToken);
    };

    /**
     * Sets the includeRequestTypeInResponse option.
     * @deprecated Use options object or setOption() instead
     * @param {bool} includeRequestTypeInResponse
     */
    this.setIncludeRequestTypeInResponse = function(includeRequestTypeInResponse) {
        self.setOption('includeRequestTypeInResponse', includeRequestTypeInResponse);
    };

    /**
     * Sets the maxTries option.
     * @deprecated Use options object or setOption() instead
     * @param {integer} maxTries
     */
    this.setMaxTries = function(maxTries) {
        self.setOption('maxTries', maxTries);
    };

    /**
     * Sets the proxy option.
     * @deprecated Use options object or setOption() instead
     * @param {string} proxy
     */
    this.setProxy = function(proxy) {
        self.setOption('proxy', proxy);
    };

    /**
     * Sets the mapObjectsThrottling option.
     * @deprecated Use options object or setOption() instead
     * @param {boolean} enable
     */
    this.setMapObjectsThrottlingEnabled = function(enable) {
        self.setOption('mapObjectsThrottling', enable);
    };

    /**
     * Sets a callback to be called for any envelope or request just before it is sent to
     * the server (mostly for debugging purposes).
     * @deprecated Use the raw-request event instead
     * @param {function} callback - function to call on requests
     */
    this.setRequestCallback = function(callback) {
        self.on('raw-request', callback);
    };

    /**
     * Sets a callback to be called for any envelope or response just after it has been
     * received from the server (mostly for debugging purposes).
     * @deprecated Use the raw-response event instead
     * @param {function} callback - function to call on responses
     */
    this.setResponseCallback = function(callback) {
        self.on('raw-response', callback);
    };

    /**
     * Sets the automaticLongConversion option.
     * @deprecated Use options object or setOption() instead
     * @param {boolean} enable
     */
    this.setAutomaticLongConversionEnabled = function(enable) {
        if (typeof enable !== 'boolean') return;
        self.setOption('automaticLongConversion', enable);
    };
}
function Client(options) {
    if (!(this instanceof Client)) {
        return new Client(options);
    }
    const self = this;

    /*
     * PUBLIC METHODS
     */

     /**
      * Sets the specified client option to the given value.
      * Note that not all options support changes after client initialization.
      * @param {string} option - Option name
      * @param {any} value - Option value
      */
    this.setOption = function(option, value) {
        self.options[option] = value;
    };

    /**
     * Sets the player's latitude and longitude.
     * Note that this does not actually update the player location on the server, it only sets
     * the location to be used in following API calls. To update the location on the server you
     * need to make an API call.
     * @param {number|object} latitude - The player's latitude, or an object with parameters
     * @param {number} longitude - The player's longitude
     * @param {number} [accuracy=0] - The location accuracy in m
     * @param {number} [altitude=0] - The player's altitude
     */
    this.setPosition = function(latitude, longitude, accuracy, altitude) {
        if (typeof latitude === 'object') {
            const pos = latitude;
            latitude = pos.latitude;
            longitude = pos.longitude;
            accuracy = pos.accuracy;
            altitude = pos.altitude;
        }
        self.playerLatitude = latitude;
        self.playerLongitude = longitude;
        self.playerLocationAccuracy = accuracy || 0;
        self.playerAltitude = altitude || 0;
    };

    /**
     * Performs client initialization and downloads needed settings from the API and hashing server.
     * @param {boolean} [downloadSettings] - Deprecated, use downloadSettings option instead
     * @return {Promise} promise
     */
    this.init = function(downloadSettings) {
        // For backwards compatibility only
        if (typeof downloadSettings !== 'undefined') self.setOption('downloadSettings', downloadSettings);

        self.lastMapObjectsCall = 0;
        self.endpoint = INITIAL_ENDPOINT;

        // convert app version (5704) to client version (0.57.4)
        let signatureVersion = '0.' + ((+self.options.version) / 100).toFixed(0);
        signatureVersion += '.' + (+self.options.version % 100);

        Signature.signature.register(self, self.options.deviceId);

        self.signatureBuilder = new Signature.encryption.Builder({
            protos: POGOProtos,
            version: signatureVersion,
        });
        self.signatureBuilder.encryptAsync = Promise.promisify(self.signatureBuilder.encrypt,
                                                                { context: self.signatureBuilder });

        let promise = Promise.resolve(true);

        if (self.options.useHashingServer) {
            promise = promise.then(self.initializeHashingServer);
        }

        if (self.options.downloadSettings) {
            promise = promise.then(() => self.downloadSettings()).then(self.processSettingsResponse);
        }

        return promise;
    };

    /**
     * Sets batch mode. All further API requests will be held and executed in one RPC call when
     * {@link #batchCall} is called.
     * @return {Client} this
     */
    this.batchStart = function() {
        if (!self.batchRequests) {
            self.batchRequests = [];
        }
        return self;
    };

    /**
     * Clears the list of batched requests and aborts batch mode.
     */
    this.batchClear = function() {
        delete self.batchRequests;
    };

    /**
     * Executes any batched requests.
     * @return {Promise}
     */
    this.batchCall = function() {
        var p = self.callRPC(self.batchRequests || []);
        self.batchClear();
        return p;
    };

    /**
     * Gets rate limit info from the latest signature server request, if applicable.
     * @return {Object}
     */
    this.getSignatureRateInfo = function() {
        return self.signatureBuilder.rateInfos;
    };

    /*
     * API CALLS (in order of RequestType enum)
     */

    this.getPlayer = function(country, language, timezone) {
        return self.callOrChain({
            type: RequestType.GET_PLAYER,
            message: new RequestMessages.GetPlayerMessage({
                player_locale: {
                    country: country,
                    language: language,
                    timezone: timezone
                }
            }),
            responseType: Responses.GetPlayerResponse
        });
    };

    this.getInventory = function(lastTimestamp) {
        return self.callOrChain({
            type: RequestType.GET_INVENTORY,
            message: new RequestMessages.GetInventoryMessage({
                last_timestamp_ms: lastTimestamp
            }),
            responseType: Responses.GetInventoryResponse
        });
    };

    this.downloadSettings = function(hash) {
        return self.callOrChain({
            type: RequestType.DOWNLOAD_SETTINGS,
            message: new RequestMessages.DownloadSettingsMessage({
                hash: hash
            }),
            responseType: Responses.DownloadSettingsResponse
        });
    };

    this.downloadItemTemplates = function(paginate, pageOffset, pageTimestamp) {
        return self.callOrChain({
            type: RequestType.DOWNLOAD_ITEM_TEMPLATES,
            message: new RequestMessages.DownloadItemTemplatesMessage({
                paginate: paginate,
                page_offset: pageOffset,
                page_timestamp: pageTimestamp
            }),
            responseType: Responses.DownloadItemTemplatesResponse
        });
    };

    this.downloadRemoteConfigVersion = function(platform, deviceManufacturer, deviceModel, locale, appVersion) {
        return self.callOrChain({
            type: RequestType.DOWNLOAD_REMOTE_CONFIG_VERSION,
            message: new RequestMessages.DownloadRemoteConfigVersionMessage({
                platform: platform,
                device_manufacturer: deviceManufacturer,
                device_model: deviceModel,
                locale: locale,
                app_version: appVersion
            }),
            responseType: Responses.DownloadRemoteConfigVersionResponse
        });
    };

    this.registerBackgroundDevice = function(deviceType, deviceID) {
        return self.callOrChain({
            type: RequestType.REGISTER_BACKGROUND_DEVICE,
            message: new RequestMessages.RegisterBackgroundDeviceMessage({
                device_type: deviceType,
                device_id: deviceID
            }),
            responseType: Responses.RegisterBackgroundDeviceResponse
        });
    };

    this.fortSearch = function(fortID, fortLatitude, fortLongitude) {
        return self.callOrChain({
            type: RequestType.FORT_SEARCH,
            message: new RequestMessages.FortSearchMessage({
                fort_id: fortID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude,
                fort_latitude: fortLatitude,
                fort_longitude: fortLongitude
            }),
            responseType: Responses.FortSearchResponse
        });
    };

    this.encounter = function(encounterID, spawnPointID) {
        return self.callOrChain({
            type: RequestType.ENCOUNTER,
            message: new RequestMessages.EncounterMessage({
                encounter_id: encounterID,
                spawn_point_id: spawnPointID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.EncounterResponse
        });
    };

    this.catchPokemon = function(encounterID, pokeballItemID, normalizedReticleSize, spawnPointID, hitPokemon,
        spinModifier, normalizedHitPosition) {
        return self.callOrChain({
            type: RequestType.CATCH_POKEMON,
            message: new RequestMessages.CatchPokemonMessage({
                encounter_id: encounterID,
                pokeball: pokeballItemID,
                normalized_reticle_size: normalizedReticleSize,
                spawn_point_id: spawnPointID,
                hit_pokemon: hitPokemon,
                spin_modifier: spinModifier,
                normalized_hit_position: normalizedHitPosition
            }),
            responseType: Responses.CatchPokemonResponse
        });
    };

    this.fortDetails = function(fortID, fortLatitude, fortLongitude) {
        return self.callOrChain({
            type: RequestType.FORT_DETAILS,
            message: new RequestMessages.FortDetailsMessage({
                fort_id: fortID,
                latitude: fortLatitude,
                longitude: fortLongitude
            }),
            responseType: Responses.FortDetailsResponse
        });
    };

    this.getMapObjects = function(cellIDs, sinceTimestamps) {
        return self.callOrChain({
            type: RequestType.GET_MAP_OBJECTS,
            message: new RequestMessages.GetMapObjectsMessage({
                cell_id: cellIDs,
                since_timestamp_ms: sinceTimestamps,
                latitude: self.playerLatitude,
                longitude: self.playerLongitude
            }),
            responseType: Responses.GetMapObjectsResponse
        });
    };

    this.fortDeployPokemon = function(fortID, pokemonID) {
        return self.callOrChain({
            type: RequestType.FORT_DEPLOY_POKEMON,
            message: new RequestMessages.FortDeployPokemonMessage({
                fort_id: fortID,
                pokemon_id: pokemonID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.FortDeployPokemonResponse
        });
    };

    this.fortRecallPokemon = function(fortID, pokemonID) {
        return self.callOrChain({
            type: RequestType.FORT_RECALL_POKEMON,
            message: new RequestMessages.FortRecallPokemonMessage({
                fort_id: fortID,
                pokemon_id: pokemonID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.FortRecallPokemonResponse
        });
    };

    this.releasePokemon = function(pokemonIDs) {
        if (!Array.isArray(pokemonIDs)) pokemonIDs = [pokemonIDs];

        return self.callOrChain({
            type: RequestType.RELEASE_POKEMON,
            message: new RequestMessages.ReleasePokemonMessage({
                pokemon_id: pokemonIDs.length === 1 ? pokemonIDs[0] : undefined,
                pokemon_ids: pokemonIDs.length > 1 ? pokemonIDs : undefined
            }),
            responseType: Responses.ReleasePokemonResponse
        });
    };

    this.useItemPotion = function(itemID, pokemonID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_POTION,
            message: new RequestMessages.UseItemPotionMessage({
                item_id: itemID,
                pokemon_id: pokemonID
            }),
            responseType: Responses.UseItemPotionResponse
        });
    };

    this.useItemCapture = function(itemID, encounterID, spawnPointID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_CAPTURE,
            message: new RequestMessages.UseItemCaptureMessage({
                item_id: itemID,
                encounter_id: encounterID,
                spawn_point_id: spawnPointID
            }),
            responseType: Responses.UseItemCaptureResponse
        });
    };

    this.useItemRevive = function(itemID, pokemonID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_REVIVE,
            message: new RequestMessages.UseItemReviveMessage({
                item_id: itemID,
                pokemon_id: pokemonID
            }),
            responseType: Responses.UseItemReviveResponse
        });
    };

    this.getPlayerProfile = function(playerName) {
        return self.callOrChain({
            type: RequestType.GET_PLAYER_PROFILE,
            message: new RequestMessages.GetPlayerProfileMessage({
                player_name: playerName
            }),
            responseType: Responses.GetPlayerProfileResponse
        });
    };

    this.evolvePokemon = function(pokemonID, evolutionRequirementItemID) {
        return self.callOrChain({
            type: RequestType.EVOLVE_POKEMON,
            message: new RequestMessages.EvolvePokemonMessage({
                pokemon_id: pokemonID,
                evolution_item_requirement: evolutionRequirementItemID
            }),
            responseType: Responses.EvolvePokemonResponse
        });
    };

    this.getHatchedEggs = function() {
        return self.callOrChain({
            type: RequestType.GET_HATCHED_EGGS,
            responseType: Responses.GetHatchedEggsResponse
        });
    };

    this.encounterTutorialComplete = function(pokemonID) {
        return self.callOrChain({
            type: RequestType.ENCOUNTER_TUTORIAL_COMPLETE,
            message: new RequestMessages.EncounterTutorialCompleteMessage({
                pokemon_id: pokemonID
            }),
            responseType: Responses.EncounterTutorialCompleteResponse
        });
    };

    this.levelUpRewards = function(level) {
        return self.callOrChain({
            type: RequestType.LEVEL_UP_REWARDS,
            message: new RequestMessages.LevelUpRewardsMessage({
                level: level
            }),
            responseType: Responses.LevelUpRewardsResponse
        });
    };

    this.checkAwardedBadges = function() {
        return self.callOrChain({
            type: RequestType.CHECK_AWARDED_BADGES,
            responseType: Responses.CheckAwardedBadgesResponse
        });
    };

    this.useItemGym = function(itemID, gymID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_GYM,
            message: new RequestMessages.UseItemGymMessage({
                item_id: itemID,
                gym_id: gymID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.UseItemGymResponse
        });
    };

    this.getGymDetails = function(gymID, gymLatitude, gymLongitude, clientVersion) {
        return self.callOrChain({
            type: RequestType.GET_GYM_DETAILS,
            message: new RequestMessages.GetGymDetailsMessage({
                gym_id: gymID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude,
                gym_latitude: gymLatitude,
                gym_longitude: gymLongitude,
                client_version: clientVersion
            }),
            responseType: Responses.GetGymDetailsResponse
        });
    };

    this.startGymBattle = function(gymID, attackingPokemonIDs, defendingPokemonID) {
        return self.callOrChain({
            type: RequestType.START_GYM_BATTLE,
            message: new RequestMessages.StartGymBattleMessage({
                gym_id: gymID,
                attacking_pokemon_ids: attackingPokemonIDs,
                defending_pokemon_id: defendingPokemonID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.StartGymBattleResponse
        });
    };

    this.attackGym = function(gymID, battleID, attackActions, lastRetrievedAction) {
        return self.callOrChain({
            type: RequestType.ATTACK_GYM,
            message: new RequestMessages.AttackGymMessage({
                gym_id: gymID,
                battle_id: battleID,
                attack_actions: attackActions,
                last_retrieved_action: lastRetrievedAction,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.AttackGymResponse
        });
    };

    this.recycleInventoryItem = function(itemID, count) {
        return self.callOrChain({
            type: RequestType.RECYCLE_INVENTORY_ITEM,
            message: new RequestMessages.RecycleInventoryItemMessage({
                item_id: itemID,
                count: count
            }),
            responseType: Responses.RecycleInventoryItemResponse
        });
    };

    this.collectDailyBonus = function() {
        return self.callOrChain({
            type: RequestType.COLLECT_DAILY_BONUS,
            responseType: Responses.CollectDailyBonusResponse
        });
    };

    this.useItemXPBoost = function(itemID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_XP_BOOST,
            message: new RequestMessages.UseItemXpBoostMessage({
                item_id: itemID
            }),
            responseType: Responses.UseItemXpBoostResponse
        });
    };

    this.useItemEggIncubator = function(itemID, pokemonID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_EGG_INCUBATOR,
            message: new RequestMessages.UseItemEggIncubatorMessage({
                item_id: itemID,
                pokemon_id: pokemonID
            }),
            responseType: Responses.UseItemEggIncubatorResponse
        });
    };

    this.useIncense = function(itemID) {
        return self.callOrChain({
            type: RequestType.USE_INCENSE,
            message: new RequestMessages.UseIncenseMessage({
                incense_type: itemID
            }),
            responseType: Responses.UseIncenseResponse
        });
    };

    this.getIncensePokemon = function() {
        return self.callOrChain({
            type: RequestType.GET_INCENSE_POKEMON,
            message: new RequestMessages.GetIncensePokemonMessage({
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.GetIncensePokmeonResponse
        });
    };

    this.incenseEncounter = function(encounterID, encounterLocation) {
        return self.callOrChain({
            type: RequestType.INCENSE_ENCOUNTER,
            message: new RequestMessages.IncenseEncounterMessage({
                encounter_id: encounterID,
                encounter_location: encounterLocation
            }),
            responseType: Responses.IncenseEncounterResponse
        });
    };

    this.addFortModifier = function(modifierItemID, fortID) {
        return self.callOrChain({
            type: RequestType.ADD_FORT_MODIFIER,
            message: new RequestMessages.AddFortModifierMessage({
                modifier_type: modifierItemID,
                fort_id: fortID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            })
        });
    };

    this.diskEncounter = function(encounterID, fortID) {
        return self.callOrChain({
            type: RequestType.DISK_ENCOUNTER,
            message: new RequestMessages.DiskEncounterMessage({
                encounter_id: encounterID,
                fort_id: fortID,
                player_latitude: self.playerLatitude,
                player_longitude: self.playerLongitude
            }),
            responseType: Responses.DiskEncounterResponse
        });
    };

    this.collectDailyDefenderBonus = function() {
        return self.callOrChain({
            type: RequestType.COLLECT_DAILY_DEFENDER_BONUS,
            responseType: Responses.CollectDailyDefenderBonusResponse
        });
    };

    this.upgradePokemon = function(pokemonID) {
        return self.callOrChain({
            type: RequestType.UPGRADE_POKEMON,
            message: new RequestMessages.UpgradePokemonMessage({
                pokemon_id: pokemonID
            }),
            responseType: Responses.UpgradePokemonResponse
        });
    };

    this.setFavoritePokemon = function(pokemonID, isFavorite) {
        return self.callOrChain({
            type: RequestType.SET_FAVORITE_POKEMON,
            message: new RequestMessages.SetFavoritePokemonMessage({
                pokemon_id: pokemonID,
                is_favorite: isFavorite
            }),
            responseType: Responses.SetFavoritePokemonResponse
        });
    };

    this.nicknamePokemon = function(pokemonID, nickname) {
        return self.callOrChain({
            type: RequestType.NICKNAME_POKEMON,
            message: new RequestMessages.NicknamePokemonMessage({
                pokemon_id: pokemonID,
                nickname: nickname
            }),
            responseType: Responses.NicknamePokemonResponse
        });
    };

    this.equipBadge = function(badgeType) {
        return self.callOrChain({
            type: RequestType.EQUIP_BADGE,
            message: new RequestMessages.EquipBadgeMessage({
                badge_type: badgeType
            }),
            responseType: Responses.EquipBadgeResponse
        });
    };

    this.setContactSettings = function(sendMarketingEmails, sendPushNotifications) {
        return self.callOrChain({
            type: RequestType.SET_CONTACT_SETTINGS,
            message: new RequestMessages.SetContactSettingsMessage({
                contact_settings: {
                    send_marketing_emails: sendMarketingEmails,
                    send_push_notifications: sendPushNotifications
                }
            }),
            responseType: Responses.SetContactSettingsResponse
        });
    };

    this.setBuddyPokemon = function(pokemonID) {
        return self.callOrChain({
            type: RequestType.SET_BUDDY_POKEMON,
            message: new RequestMessages.SetBuddyPokemonMessage({
                pokemon_id: pokemonID
            }),
            responseType: Responses.SetBuddyPokemonResponse
        });
    };

    this.getBuddyWalked = function() {
        return self.callOrChain({
            type: RequestType.GET_BUDDY_WALKED,
            responseType: Responses.GetBuddyWalkedResponse
        });
    };

    this.useItemEncounter = function(itemID, encounterID, spawnPointGUID) {
        return self.callOrChain({
            type: RequestType.USE_ITEM_ENCOUNTER,
            message: new RequestMessages.UseItemEncounterMessage({
                item: itemID,
                encounter_id: encounterID,
                spawn_point_guid: spawnPointGUID
            }),
            responseType: Responses.UseItemEncounterResponse
        });
    };

    this.getAssetDigest = function(platform, deviceManufacturer, deviceModel, locale, appVersion) {
        return self.callOrChain({
            type: RequestType.GET_ASSET_DIGEST,
            message: new RequestMessages.GetAssetDigestMessage({
                platform: platform,
                device_manufacturer: deviceManufacturer,
                device_model: deviceModel,
                locale: locale,
                app_version: appVersion
            }),
            responseType: Responses.GetAssetDigestResponse
        });
    };

    this.getDownloadURLs = function(assetIDs) {
        return self.callOrChain({
            type: RequestType.GET_DOWNLOAD_URLS,
            message: new RequestMessages.GetDownloadUrlsMessage({
                asset_id: assetIDs
            }),
            responseType: Responses.GetDownloadUrlsResponse
        });
    };

    this.claimCodename = function(codename) {
        return self.callOrChain({
            type: RequestType.CLAIM_CODENAME,
            message: new RequestMessages.ClaimCodenameMessage({
                codename: codename
            }),
            responseType: Responses.ClaimCodenameResponse
        });
    };

    this.setAvatar = function(playerAvatar) {
        return self.callOrChain({
            type: RequestType.SET_AVATAR,
            message: new RequestMessages.SetAvatarMessage({
                player_avatar: playerAvatar
            }),
            responseType: Responses.SetAvatarResponse
        });
    };

    this.setPlayerTeam = function(teamColor) {
        return self.callOrChain({
            type: RequestType.SET_PLAYER_TEAM,
            message: new RequestMessages.SetPlayerTeamMessage({
                team: teamColor
            }),
            responseType: Responses.SetPlayerTeamResponse
        });
    };

    this.markTutorialComplete = function(tutorialsCompleted, sendMarketingEmails, sendPushNotifications) {
        return self.callOrChain({
            type: RequestType.MARK_TUTORIAL_COMPLETE,
            message: new RequestMessages.MarkTutorialCompleteMessage({
                tutorials_completed: tutorialsCompleted,
                send_marketing_emails: sendMarketingEmails,
                send_push_notifications: sendPushNotifications
            }),
            responseType: Responses.MarkTutorialCompleteResponse
        });
    };

    this.checkChallenge = function(isDebugRequest) {
        return self.callOrChain({
            type: RequestType.CHECK_CHALLENGE,
            message: new RequestMessages.CheckChallengeMessage({
                debug_request: isDebugRequest
            }),
            responseType: Responses.CheckChallengeResponse
        });
    };

    this.verifyChallenge = function(token) {
        return self.callOrChain({
            type: RequestType.VERIFY_CHALLENGE,
            message: new RequestMessages.VerifyChallengeMessage({
                token: token
            }),
            responseType: Responses.VerifyChallengeResponse
        });
    };

    this.echo = function() {
        return self.callOrChain({
            type: RequestType.ECHO,
            responseType: Responses.EchoResponse
        });
    };

    this.sfidaActionLog = function() {
        return self.callOrChain({
            type: RequestType.SFIDA_ACTION_LOG,
            responseType: Responses.SfidaActionLogResponse
        });
    };

    this.listAvatarCustomizations = function(avatarType, slots, filters, start, limit) {
        return self.callOrChain({
            type: RequestType.LIST_AVATAR_CUSTOMIZATIONS,
            message: new RequestMessages.ListAvatarCustomizationsMessage({
                avatar_type: avatarType,
                slot: slots,
                filters: filters,
                start: start,
                limit: limit
            }),
            responseType: Responses.ListAvatarCustomizationsResponse
        });
    };

    this.setAvatarItemAsViewed = function(avatarTemplateIDs) {
        return self.callOrChain({
            type: RequestType.SET_AVATAR_ITEM_AS_VIEWED,
            message: new RequestMessages.SetAvatarItemAsViewedMessage({
                avatar_template_id: avatarTemplateIDs
            }),
            responseType: Responses.SetAvatarItemAsViewdResponse
        });
    };

    /*
     * INTERNAL STUFF
     */

    this.request = request.defaults({
        headers: {
            'User-Agent': 'Niantic App',
            'Accept': '*/*',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        encoding: null
    });

    this.options = Object.assign({}, defaultOptions, options || {});
    this.authTicket = null;
    this.rpcId = 2;
    this.lastHashingKeyIndex = 0;
    this.firstGetMapObjects = true;
    this.lehmer = new Lehmer(16807);
    this.ptr8 = INITIAL_PTR8;

    /**
     * Executes a request and returns a Promise or, if we are in batch mode, adds it to the
     * list of batched requests and returns this (for chaining).
     * @private
     * @param {object} requestMessage - RPC request object
     * @return {Promise|Client}
     */
    this.callOrChain = function(requestMessage) {
        if (self.batchRequests) {
            self.batchRequests.push(requestMessage);
            return self;
        } else {
            return self.callRPC([requestMessage]);
        }
    };

    /**
     * Generates next rpc request id
     * @private
     * @return {Long}
     */
    this.getRequestID = function() {
        return new Long(self.rpcId++, this.lehmer.nextInt());
    };

    /**
     * Creates an RPC envelope with the given list of requests.
     * @private
     * @param {Object[]} requests - Array of requests to build
     * @return {POGOProtos.Networking.Envelopes.RequestEnvelope}
     */
    this.buildEnvelope = function(requests) {
        var envelopeData = {
            status_code: 2,
            request_id: self.getRequestID(),
            ms_since_last_locationfix: 100 + Math.floor(Math.random() * 900)
        };

        if (self.playerLatitude) envelopeData.latitude = self.playerLatitude;
        if (self.playerLongitude) envelopeData.longitude = self.playerLongitude;
        if (self.playerLocationAccuracy) {
            envelopeData.accuracy = self.playerLocationAccuracy;
        } else {
            const values = [5, 5, 5, 5, 10, 10, 10, 30, 30, 50, 65];
            values.unshift(Math.floor(Math.random() * (80 - 66)) + 66);
            envelopeData.accuracy = values[Math.floor(values.length * Math.random())];
        }

        if (self.authTicket) {
            envelopeData.auth_ticket = self.authTicket;
        } else if (!self.options.authType || !self.options.authToken) {
            throw Error('No auth info provided');
        } else {
            let unknown2 = 0;
            if (self.options.authType === 'ptc') {
                const values = [2, 8, 21, 21, 21, 28, 37, 56, 59, 59, 59];
                unknown2 = values[Math.floor(values.length * Math.random())];
            }
            envelopeData.auth_info = {
                provider: self.options.authType,
                token: {
                    contents: self.options.authToken,
                    unknown2: unknown2,
                }
            };
        }

        if (requests) {
            self.emit('request', {
                request_id: envelopeData.request_id.toString(),
                requests: requests.map(r => ({
                    name: Utils.getEnumKeyByValue(RequestType, r.type),
                    type: r.type,
                    data: r.message
                }))
            });

            envelopeData.requests = requests.map(r => {
                var requestData = {
                    request_type: r.type
                };

                if (r.message) {
                    requestData.request_message = r.message.encode();
                }

                return requestData;
            });
        }

        self.emit('raw-request', envelopeData);

        return new POGOProtos.Networking.Envelopes.RequestEnvelope(envelopeData);
    };

    /**
     * Constructs and adds a platform request to a request envelope.
     * @private
     * @param {RequestEnvelope} envelope - Request envelope
     * @param {PlatformRequestType} requestType - Type of the platform request to add
     * @param {Object} requestMessage - Pre-built but not encoded PlatformRequest protobuf message
     * @return {RequestEnvelope} The envelope (for convenience only)
     */
    this.addPlatformRequestToEnvelope = function(envelope, requestType, requestMessage) {
        envelope.platform_requests.push(
            new POGOProtos.Networking.Envelopes.RequestEnvelope.PlatformRequest({
                type: requestType,
                request_message: requestMessage.encode()
            })
        );

        return envelope;
    };

    /**
     * Determines whether the as of yet unknown platform request type 8 should be added
     * to the envelope based on the given type of requests.
     * @private
     * @param {Object[]} requests - Array of request data
     * @return {boolean}
     */
    this.needsPtr8 = function(requests) {
        // Single GET_PLAYER request always gets PTR8
        if (requests.length === 1 && requests[0].type === RequestType.GET_PLAYER) {
            return true;
        }

        // Any GET_MAP_OBJECTS requests get PTR8 except the first one in the session
        if (requests.some(r => r.type === RequestType.GET_MAP_OBJECTS)) {
            if (self.firstGetMapObjects) {
                self.firstGetMapObjects = false;
                return false;
            }

            return true;
        }

        return false;
    };

    /**
     * Creates an RPC envelope with the given list of requests and adds the encrypted signature,
     * or adds the signature to an existing envelope.
     * @private
     * @param {Object[]} requests - Array of requests to build
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to sign
     * @return {Promise} - A Promise that will be resolved with a RequestEnvelope instance
     */
    this.buildSignedEnvelope = function(requests, envelope) {
        if (!envelope) {
            try {
                envelope = self.buildEnvelope(requests);
            } catch (e) {
                throw new retry.StopError(e);
            }
        }

        if (self.needsPtr8(requests)) {
            self.addPlatformRequestToEnvelope(envelope, PlatformRequestType.UNKNOWN_PTR_8,
                new PlatformRequestMessages.UnknownPtr8Request({
                    message: self.ptr8,
                }));
        }

        let authTicket = envelope.auth_ticket;
        if (!authTicket) {
            authTicket = envelope.auth_info;
        }

        if (!authTicket) {
            // Can't sign before we have received an auth ticket
            return Promise.resolve(envelope);
        }

        if (self.options.useHashingServer) {
            let key = self.options.hashingKey;
            if (Array.isArray(key)) {
                key = key[self.lastHashingKeyIndex];
                self.lastHashingKeyIndex = (self.lastHashingKeyIndex + 1) % self.options.hashingKey.length;
            }

            self.signatureBuilder.useHashingServer(self.options.hashingServer + self.hashingVersion, key);
        }

        self.signatureBuilder.setAuthTicket(authTicket);

        if (typeof self.options.signatureInfo === 'function') {
            self.signatureBuilder.setFields(self.options.signatureInfo(envelope));
        } else if (self.options.signatureInfo) {
            self.signatureBuilder.setFields(self.options.signatureInfo);
        }

        self.signatureBuilder.setLocation(envelope.latitude, envelope.longitude, envelope.accuracy);

        return retry(() => self.signatureBuilder.encryptAsync(envelope.requests)
                        .catch(err => {
                            if (err.name === 'HashServerError' && err.message === 'Request limited') {
                                throw err;
                            } else {
                                throw new retry.StopError(err);
                            }
                        }),
            {
                interval: 1000,
                backoff: 2,
                max_tries: 10,
                args: envelope.requests,
            })
            .then(sigEncrypted =>
                self.addPlatformRequestToEnvelope(envelope, PlatformRequestType.SEND_ENCRYPTED_SIGNATURE,
                    new PlatformRequestMessages.SendEncryptedSignatureRequest({
                        encrypted_signature: sigEncrypted
                    })
                )
            );
    };

    /**
     * Executes an RPC call with the given list of requests, retrying if necessary.
     * @private
     * @param {Object[]} requests - Array of requests to send
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to use
     * @return {Promise} - A Promise that will be resolved with the (list of) response messages,
     *     or true if there aren't any
     */
    this.callRPC = function(requests, envelope) {
        // If the requests include a map objects request, make sure the minimum delay
        // since the last call has passed
        if (requests.some(r => r.type === RequestType.GET_MAP_OBJECTS)) {
            var now = new Date().getTime(),
                delayNeeded = self.lastMapObjectsCall + self.options.mapObjectsMinDelay - now;

            if (delayNeeded > 0 && self.options.mapObjectsThrottling) {
                return Promise.delay(delayNeeded).then(() => self.callRPC(requests, envelope));
            }

            self.lastMapObjectsCall = now;
        }

        if (self.options.maxTries <= 1) return self.tryCallRPC(requests, envelope);

        return retry(() => self.tryCallRPC(requests, envelope), {
            interval: 300,
            backoff: 2,
            max_tries: self.options.maxTries
        });
    };

    /**
     * Handle redirection to new API endpoint and resend last request to new endpoint.
     * @private
     * @param {Object[]} requests - Array of requests
     * @param {RequestEnvelope} signedEnvelope - Request envelope
     * @param {ResponseEnvelope} responseEnvelope - Result from API call
     * @return {Promise}
     */
    this.redirect = function(requests, signedEnvelope, responseEnvelope) {
        return new Promise((resolve, reject) => {
            if (!responseEnvelope.api_url) {
                reject(Error('Fetching RPC endpoint failed, none supplied in response'));
                return;
            }

            self.endpoint = 'https://' + responseEnvelope.api_url + '/rpc';

            self.emit('endpoint-response', {
                status_code: responseEnvelope.status_code,
                request_id: responseEnvelope.request_id.toString(),
                api_url: responseEnvelope.api_url
            });

            signedEnvelope.platform_requests = [];
            resolve(self.callRPC(requests, signedEnvelope));
        });
    };

    /**
     * Executes an RPC call with the given list of requests.
     * @private
     * @param {Object[]} requests - Array of requests to send
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to use
     * @return {Promise} - A Promise that will be resolved with the (list of) response messages,
     *     or true if there aren't any
     */
    this.tryCallRPC = function(requests, envelope) {
        return self.buildSignedEnvelope(requests, envelope)
            .then(signedEnvelope => new Promise((resolve, reject) => {
                self.request({
                    method: 'POST',
                    url: self.endpoint,
                    proxy: self.options.proxy,
                    body: signedEnvelope.toBuffer()
                }, (err, response, body) => {
                    if (err) {
                        reject(Error(err));
                        return;
                    }

                    if (response.statusCode !== 200) {
                        if (response.statusCode >= 400 && response.statusCode < 500) {
                            /* These are permanent errors so throw StopError */
                            reject(new retry.StopError(
                                `Status code ${response.statusCode} received from HTTPS request`
                            ));
                        } else {
                            /* Anything else might be recoverable so throw regular Error */
                            reject(Error(
                                `Status code ${response.statusCode} received from HTTPS request`
                            ));
                        }
                        return;
                    }

                    var responseEnvelope;
                    try {
                        responseEnvelope =
                            POGOProtos.Networking.Envelopes.ResponseEnvelope.decode(body);
                    } catch (e) {
                        self.emit('parse-envelope-error', body, e);
                        if (e.decoded) {
                            responseEnvelope = e.decoded;
                        } else {
                            reject(new retry.StopError(e));
                            return;
                        }
                    }

                    self.emit('raw-response', responseEnvelope);

                    if (responseEnvelope.error) {
                        reject(new retry.StopError(responseEnvelope.error));
                        return;
                    }

                    if (responseEnvelope.auth_ticket) self.authTicket = responseEnvelope.auth_ticket;

                    if (responseEnvelope.status_code === 53 ||
                        (responseEnvelope.status_code === 2 && self.endpoint === INITIAL_ENDPOINT)) {
                        resolve(self.redirect(requests, signedEnvelope, responseEnvelope));
                        return;
                    }

                    responseEnvelope.platform_returns.forEach(platformReturn => {
                        if (platformReturn.type === PlatformRequestType.UNKNOWN_PTR_8) {
                            const ptr8 = PlatformResponses.UnknownPtr8Response.decode(platformReturn.response);
                            if (ptr8) self.ptr8 = ptr8.message;
                        }
                    });

                    /* Throttling, retry same request later */
                    if (responseEnvelope.status_code === 52) {
                        signedEnvelope.platform_requests = [];
                        Promise.delay(2000).then(() => {
                            resolve(self.callRPC(requests, signedEnvelope));
                        });
                        return;
                    }

                    /* These codes indicate invalid input, no use in retrying so throw StopError */
                    if (responseEnvelope.status_code === 3 || responseEnvelope.status_code === 51 ||
                        responseEnvelope.status_code >= 100) {
                        reject(new retry.StopError(
                            `Status code ${responseEnvelope.status_code} received from RPC`
                        ));
                    }

                    /* These can be temporary so throw regular Error */
                    if (responseEnvelope.status_code !== 2 && responseEnvelope.status_code !== 1) {
                        reject(Error(
                            `Status code ${responseEnvelope.status_code} received from RPC`
                        ));
                        return;
                    }

                    var responses = [];

                    if (requests) {
                        if (requests.length !== responseEnvelope.returns.length) {
                            reject(Error('Request count does not match response count'));
                            return;
                        }

                        for (var i = 0; i < responseEnvelope.returns.length; i++) {
                            if (!requests[i].responseType) continue;

                            var responseMessage;
                            try {
                                responseMessage = requests[i].responseType.decode(
                                    responseEnvelope.returns[i]
                                );
                            } catch (e) {
                                self.emit('parse-response-error',
                                    responseEnvelope.returns[i].toBuffer(), e);
                                reject(new retry.StopError(e));
                                return;
                            }

                            if (self.options.includeRequestTypeInResponse) {
                                // eslint-disable-next-line no-underscore-dangle
                                responseMessage._requestType = requests[i].type;
                            }
                            responses.push(responseMessage);
                        }
                    }

                    self.emit('response', {
                        status_code: responseEnvelope.status_code,
                        request_id: responseEnvelope.request_id.toString(),
                        responses: responses.map((r, h) => ({
                            name: Utils.getEnumKeyByValue(
                                RequestType, requests[h].type
                            ),
                            type: requests[h].type,
                            data: r
                        }))
                    });

                    if (self.options.automaticLongConversion) {
                        responses = Utils.convertLongs(responses);
                    }

                    if (!responses.length) resolve(true);
                    else if (responses.length === 1) resolve(responses[0]);
                    else resolve(responses);
                });
            }));
    };

    /**
     * Processes the data received from the downloadSettings API call during init().
     * @private
     * @param {Object} settingsResponse - Response from API call
     * @return {Object} response - Unomdified response (to send back to Promise)
     */
    this.processSettingsResponse = function(settingsResponse) {
        // Extract the minimum delay of getMapObjects()
        if (settingsResponse &&
            !settingsResponse.error &&
            settingsResponse.settings &&
            settingsResponse.settings.map_settings &&
            settingsResponse.settings.map_settings.get_map_objects_min_refresh_seconds
        ) {
            self.setOption('mapObjectsMinDelay',
                settingsResponse.settings.map_settings.get_map_objects_min_refresh_seconds * 1000);
        }
        return settingsResponse;
    };

    /**
     * Makes an initial call to the hashing server to verify API version.
     * @private
     * @return {Promise}
     */
    this.initializeHashingServer = function() {
        if (!self.options.hashingServer) throw new Error('Hashing server enabled without host');
        if (!self.options.hashingKey) throw new Error('Hashing server enabled without key');

        if (self.options.hashingServer.slice(-1) !== '/') {
            self.setOption('hashingServer', self.options.hashingServer + '/');
        }

        return Signature.versions.getHashingEndpoint(self.options.hashingServer, self.options.version)
                .then(version => {
                    self.hashingVersion = version;
                });
    };

    /*
     * DEPRECATED METHODS
     */

    /**
     * Sets the authType and authToken options.
     * @deprecated Use options object or setOption() instead
     * @param {string} authType
     * @param {string} authToken
     */
    this.setAuthInfo = function(authType, authToken) {
        self.setOption('authType', authType);
        self.setOption('authToken', authToken);
    };

    /**
     * Sets the includeRequestTypeInResponse option.
     * @deprecated Use options object or setOption() instead
     * @param {bool} includeRequestTypeInResponse
     */
    this.setIncludeRequestTypeInResponse = function(includeRequestTypeInResponse) {
        self.setOption('includeRequestTypeInResponse', includeRequestTypeInResponse);
    };

    /**
     * Sets the maxTries option.
     * @deprecated Use options object or setOption() instead
     * @param {integer} maxTries
     */
    this.setMaxTries = function(maxTries) {
        self.setOption('maxTries', maxTries);
    };

    /**
     * Sets the proxy option.
     * @deprecated Use options object or setOption() instead
     * @param {string} proxy
     */
    this.setProxy = function(proxy) {
        self.setOption('proxy', proxy);
    };

    /**
     * Sets the mapObjectsThrottling option.
     * @deprecated Use options object or setOption() instead
     * @param {boolean} enable
     */
    this.setMapObjectsThrottlingEnabled = function(enable) {
        self.setOption('mapObjectsThrottling', enable);
    };

    /**
     * Sets a callback to be called for any envelope or request just before it is sent to
     * the server (mostly for debugging purposes).
     * @deprecated Use the raw-request event instead
     * @param {function} callback - function to call on requests
     */
    this.setRequestCallback = function(callback) {
        self.on('raw-request', callback);
    };

    /**
     * Sets a callback to be called for any envelope or response just after it has been
     * received from the server (mostly for debugging purposes).
     * @deprecated Use the raw-response event instead
     * @param {function} callback - function to call on responses
     */
    this.setResponseCallback = function(callback) {
        self.on('raw-response', callback);
    };

    /**
     * Sets the automaticLongConversion option.
     * @deprecated Use options object or setOption() instead
     * @param {boolean} enable
     */
    this.setAutomaticLongConversionEnabled = function(enable) {
        if (typeof enable !== 'boolean') return;
        self.setOption('automaticLongConversion', enable);
    };
}
function Client ( options ) { if ( ! ( this instanceof Client ) ) { return new Client ( options ) ; } const self = this ; /*
     * PUBLIC METHODS
     */ /**
      * Sets the specified client option to the given value.
      * Note that not all options support changes after client initialization.
      * @param {string} option - Option name
      * @param {any} value - Option value
      */ this . setOption = function ( option , value ) { self . options [ option ] = value ; } ; /**
     * Sets the player's latitude and longitude.
     * Note that this does not actually update the player location on the server, it only sets
     * the location to be used in following API calls. To update the location on the server you
     * need to make an API call.
     * @param {number|object} latitude - The player's latitude, or an object with parameters
     * @param {number} longitude - The player's longitude
     * @param {number} [accuracy=0] - The location accuracy in m
     * @param {number} [altitude=0] - The player's altitude
     */ this . setPosition = function ( latitude , longitude , accuracy , altitude ) { if ( typeof latitude === 'object' ) { const pos = latitude ; latitude = pos . latitude ; longitude = pos . longitude ; accuracy = pos . accuracy ; altitude = pos . altitude ; } self . playerLatitude = latitude ; self . playerLongitude = longitude ; self . playerLocationAccuracy = accuracy || 0 ; self . playerAltitude = altitude || 0 ; } ; /**
     * Performs client initialization and downloads needed settings from the API and hashing server.
     * @param {boolean} [downloadSettings] - Deprecated, use downloadSettings option instead
     * @return {Promise} promise
     */ this . init = function ( downloadSettings ) { // For backwards compatibility only if ( typeof downloadSettings !== 'undefined' ) self . setOption ( 'downloadSettings' , downloadSettings ) ; self . lastMapObjectsCall = 0 ; self . endpoint = INITIAL_ENDPOINT ; // convert app version (5704) to client version (0.57.4) let signatureVersion = '0.' + ( ( + self . options . version ) / 100 ) . toFixed ( 0 ) ; signatureVersion += '.' + ( + self . options . version % 100 ) ; Signature . signature . register ( self , self . options . deviceId ) ; self . signatureBuilder = new Signature . encryption . Builder ( { protos : POGOProtos , version : signatureVersion , } ) ; self . signatureBuilder . encryptAsync = Promise . promisify ( self . signatureBuilder . encrypt , { context : self . signatureBuilder } ) ; let promise = Promise . resolve ( true ) ; if ( self . options . useHashingServer ) { promise = promise . then ( self . initializeHashingServer ) ; } if ( self . options . downloadSettings ) { promise = promise . then ( ( ) => self . downloadSettings ( ) ) . then ( self . processSettingsResponse ) ; } return promise ; } ; /**
     * Sets batch mode. All further API requests will be held and executed in one RPC call when
     * {@link #batchCall} is called.
     * @return {Client} this
     */ this . batchStart = function ( ) { if ( ! self . batchRequests ) { self . batchRequests = [ ] ; } return self ; } ; /**
     * Clears the list of batched requests and aborts batch mode.
     */ this . batchClear = function ( ) { delete self . batchRequests ; } ; /**
     * Executes any batched requests.
     * @return {Promise}
     */ this . batchCall = function ( ) { var p = self . callRPC ( self . batchRequests || [ ] ) ; self . batchClear ( ) ; return p ; } ; /**
     * Gets rate limit info from the latest signature server request, if applicable.
     * @return {Object}
     */ this . getSignatureRateInfo = function ( ) { return self . signatureBuilder . rateInfos ; } ; /*
     * API CALLS (in order of RequestType enum)
     */ this . getPlayer = function ( country , language , timezone ) { return self . callOrChain ( { type : RequestType . GET_PLAYER , message : new RequestMessages . GetPlayerMessage ( { player_locale : { country : country , language : language , timezone : timezone } } ) , responseType : Responses . GetPlayerResponse } ) ; } ; this . getInventory = function ( lastTimestamp ) { return self . callOrChain ( { type : RequestType . GET_INVENTORY , message : new RequestMessages . GetInventoryMessage ( { last_timestamp_ms : lastTimestamp } ) , responseType : Responses . GetInventoryResponse } ) ; } ; this . downloadSettings = function ( hash ) { return self . callOrChain ( { type : RequestType . DOWNLOAD_SETTINGS , message : new RequestMessages . DownloadSettingsMessage ( { hash : hash } ) , responseType : Responses . DownloadSettingsResponse } ) ; } ; this . downloadItemTemplates = function ( paginate , pageOffset , pageTimestamp ) { return self . callOrChain ( { type : RequestType . DOWNLOAD_ITEM_TEMPLATES , message : new RequestMessages . DownloadItemTemplatesMessage ( { paginate : paginate , page_offset : pageOffset , page_timestamp : pageTimestamp } ) , responseType : Responses . DownloadItemTemplatesResponse } ) ; } ; this . downloadRemoteConfigVersion = function ( platform , deviceManufacturer , deviceModel , locale , appVersion ) { return self . callOrChain ( { type : RequestType . DOWNLOAD_REMOTE_CONFIG_VERSION , message : new RequestMessages . DownloadRemoteConfigVersionMessage ( { platform : platform , device_manufacturer : deviceManufacturer , device_model : deviceModel , locale : locale , app_version : appVersion } ) , responseType : Responses . DownloadRemoteConfigVersionResponse } ) ; } ; this . registerBackgroundDevice = function ( deviceType , deviceID ) { return self . callOrChain ( { type : RequestType . REGISTER_BACKGROUND_DEVICE , message : new RequestMessages . RegisterBackgroundDeviceMessage ( { device_type : deviceType , device_id : deviceID } ) , responseType : Responses . RegisterBackgroundDeviceResponse } ) ; } ; this . fortSearch = function ( fortID , fortLatitude , fortLongitude ) { return self . callOrChain ( { type : RequestType . FORT_SEARCH , message : new RequestMessages . FortSearchMessage ( { fort_id : fortID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude , fort_latitude : fortLatitude , fort_longitude : fortLongitude } ) , responseType : Responses . FortSearchResponse } ) ; } ; this . encounter = function ( encounterID , spawnPointID ) { return self . callOrChain ( { type : RequestType . ENCOUNTER , message : new RequestMessages . EncounterMessage ( { encounter_id : encounterID , spawn_point_id : spawnPointID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) , responseType : Responses . EncounterResponse } ) ; } ; this . catchPokemon = function ( encounterID , pokeballItemID , normalizedReticleSize , spawnPointID , hitPokemon , spinModifier , normalizedHitPosition ) { return self . callOrChain ( { type : RequestType . CATCH_POKEMON , message : new RequestMessages . CatchPokemonMessage ( { encounter_id : encounterID , pokeball : pokeballItemID , normalized_reticle_size : normalizedReticleSize , spawn_point_id : spawnPointID , hit_pokemon : hitPokemon , spin_modifier : spinModifier , normalized_hit_position : normalizedHitPosition } ) , responseType : Responses . CatchPokemonResponse } ) ; } ; this . fortDetails = function ( fortID , fortLatitude , fortLongitude ) { return self . callOrChain ( { type : RequestType . FORT_DETAILS , message : new RequestMessages . FortDetailsMessage ( { fort_id : fortID , latitude : fortLatitude , longitude : fortLongitude } ) , responseType : Responses . FortDetailsResponse } ) ; } ; this . getMapObjects = function ( cellIDs , sinceTimestamps ) { return self . callOrChain ( { type : RequestType . GET_MAP_OBJECTS , message : new RequestMessages . GetMapObjectsMessage ( { cell_id : cellIDs , since_timestamp_ms : sinceTimestamps , latitude : self . playerLatitude , longitude : self . playerLongitude } ) , responseType : Responses . GetMapObjectsResponse } ) ; } ; this . fortDeployPokemon = function ( fortID , pokemonID ) { return self . callOrChain ( { type : RequestType . FORT_DEPLOY_POKEMON , message : new RequestMessages . FortDeployPokemonMessage ( { fort_id : fortID , pokemon_id : pokemonID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) , responseType : Responses . FortDeployPokemonResponse } ) ; } ; this . fortRecallPokemon = function ( fortID , pokemonID ) { return self . callOrChain ( { type : RequestType . FORT_RECALL_POKEMON , message : new RequestMessages . FortRecallPokemonMessage ( { fort_id : fortID , pokemon_id : pokemonID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) , responseType : Responses . FortRecallPokemonResponse } ) ; } ; this . releasePokemon = function ( pokemonIDs ) { if ( ! Array . isArray ( pokemonIDs ) ) pokemonIDs = [ pokemonIDs ] ; return self . callOrChain ( { type : RequestType . RELEASE_POKEMON , message : new RequestMessages . ReleasePokemonMessage ( { pokemon_id : pokemonIDs . length === 1 ? pokemonIDs [ 0 ] : undefined , pokemon_ids : pokemonIDs . length > 1 ? pokemonIDs : undefined } ) , responseType : Responses . ReleasePokemonResponse } ) ; } ; this . useItemPotion = function ( itemID , pokemonID ) { return self . callOrChain ( { type : RequestType . USE_ITEM_POTION , message : new RequestMessages . UseItemPotionMessage ( { item_id : itemID , pokemon_id : pokemonID } ) , responseType : Responses . UseItemPotionResponse } ) ; } ; this . useItemCapture = function ( itemID , encounterID , spawnPointID ) { return self . callOrChain ( { type : RequestType . USE_ITEM_CAPTURE , message : new RequestMessages . UseItemCaptureMessage ( { item_id : itemID , encounter_id : encounterID , spawn_point_id : spawnPointID } ) , responseType : Responses . UseItemCaptureResponse } ) ; } ; this . useItemRevive = function ( itemID , pokemonID ) { return self . callOrChain ( { type : RequestType . USE_ITEM_REVIVE , message : new RequestMessages . UseItemReviveMessage ( { item_id : itemID , pokemon_id : pokemonID } ) , responseType : Responses . UseItemReviveResponse } ) ; } ; this . getPlayerProfile = function ( playerName ) { return self . callOrChain ( { type : RequestType . GET_PLAYER_PROFILE , message : new RequestMessages . GetPlayerProfileMessage ( { player_name : playerName } ) , responseType : Responses . GetPlayerProfileResponse } ) ; } ; this . evolvePokemon = function ( pokemonID , evolutionRequirementItemID ) { return self . callOrChain ( { type : RequestType . EVOLVE_POKEMON , message : new RequestMessages . EvolvePokemonMessage ( { pokemon_id : pokemonID , evolution_item_requirement : evolutionRequirementItemID } ) , responseType : Responses . EvolvePokemonResponse } ) ; } ; this . getHatchedEggs = function ( ) { return self . callOrChain ( { type : RequestType . GET_HATCHED_EGGS , responseType : Responses . GetHatchedEggsResponse } ) ; } ; this . encounterTutorialComplete = function ( pokemonID ) { return self . callOrChain ( { type : RequestType . ENCOUNTER_TUTORIAL_COMPLETE , message : new RequestMessages . EncounterTutorialCompleteMessage ( { pokemon_id : pokemonID } ) , responseType : Responses . EncounterTutorialCompleteResponse } ) ; } ; this . levelUpRewards = function ( level ) { return self . callOrChain ( { type : RequestType . LEVEL_UP_REWARDS , message : new RequestMessages . LevelUpRewardsMessage ( { level : level } ) , responseType : Responses . LevelUpRewardsResponse } ) ; } ; this . checkAwardedBadges = function ( ) { return self . callOrChain ( { type : RequestType . CHECK_AWARDED_BADGES , responseType : Responses . CheckAwardedBadgesResponse } ) ; } ; this . useItemGym = function ( itemID , gymID ) { return self . callOrChain ( { type : RequestType . USE_ITEM_GYM , message : new RequestMessages . UseItemGymMessage ( { item_id : itemID , gym_id : gymID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) , responseType : Responses . UseItemGymResponse } ) ; } ; this . getGymDetails = function ( gymID , gymLatitude , gymLongitude , clientVersion ) { return self . callOrChain ( { type : RequestType . GET_GYM_DETAILS , message : new RequestMessages . GetGymDetailsMessage ( { gym_id : gymID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude , gym_latitude : gymLatitude , gym_longitude : gymLongitude , client_version : clientVersion } ) , responseType : Responses . GetGymDetailsResponse } ) ; } ; this . startGymBattle = function ( gymID , attackingPokemonIDs , defendingPokemonID ) { return self . callOrChain ( { type : RequestType . START_GYM_BATTLE , message : new RequestMessages . StartGymBattleMessage ( { gym_id : gymID , attacking_pokemon_ids : attackingPokemonIDs , defending_pokemon_id : defendingPokemonID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) , responseType : Responses . StartGymBattleResponse } ) ; } ; this . attackGym = function ( gymID , battleID , attackActions , lastRetrievedAction ) { return self . callOrChain ( { type : RequestType . ATTACK_GYM , message : new RequestMessages . AttackGymMessage ( { gym_id : gymID , battle_id : battleID , attack_actions : attackActions , last_retrieved_action : lastRetrievedAction , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) , responseType : Responses . AttackGymResponse } ) ; } ; this . recycleInventoryItem = function ( itemID , count ) { return self . callOrChain ( { type : RequestType . RECYCLE_INVENTORY_ITEM , message : new RequestMessages . RecycleInventoryItemMessage ( { item_id : itemID , count : count } ) , responseType : Responses . RecycleInventoryItemResponse } ) ; } ; this . collectDailyBonus = function ( ) { return self . callOrChain ( { type : RequestType . COLLECT_DAILY_BONUS , responseType : Responses . CollectDailyBonusResponse } ) ; } ; this . useItemXPBoost = function ( itemID ) { return self . callOrChain ( { type : RequestType . USE_ITEM_XP_BOOST , message : new RequestMessages . UseItemXpBoostMessage ( { item_id : itemID } ) , responseType : Responses . UseItemXpBoostResponse } ) ; } ; this . useItemEggIncubator = function ( itemID , pokemonID ) { return self . callOrChain ( { type : RequestType . USE_ITEM_EGG_INCUBATOR , message : new RequestMessages . UseItemEggIncubatorMessage ( { item_id : itemID , pokemon_id : pokemonID } ) , responseType : Responses . UseItemEggIncubatorResponse } ) ; } ; this . useIncense = function ( itemID ) { return self . callOrChain ( { type : RequestType . USE_INCENSE , message : new RequestMessages . UseIncenseMessage ( { incense_type : itemID } ) , responseType : Responses . UseIncenseResponse } ) ; } ; this . getIncensePokemon = function ( ) { return self . callOrChain ( { type : RequestType . GET_INCENSE_POKEMON , message : new RequestMessages . GetIncensePokemonMessage ( { player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) , responseType : Responses . GetIncensePokmeonResponse } ) ; } ; this . incenseEncounter = function ( encounterID , encounterLocation ) { return self . callOrChain ( { type : RequestType . INCENSE_ENCOUNTER , message : new RequestMessages . IncenseEncounterMessage ( { encounter_id : encounterID , encounter_location : encounterLocation } ) , responseType : Responses . IncenseEncounterResponse } ) ; } ; this . addFortModifier = function ( modifierItemID , fortID ) { return self . callOrChain ( { type : RequestType . ADD_FORT_MODIFIER , message : new RequestMessages . AddFortModifierMessage ( { modifier_type : modifierItemID , fort_id : fortID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) } ) ; } ; this . diskEncounter = function ( encounterID , fortID ) { return self . callOrChain ( { type : RequestType . DISK_ENCOUNTER , message : new RequestMessages . DiskEncounterMessage ( { encounter_id : encounterID , fort_id : fortID , player_latitude : self . playerLatitude , player_longitude : self . playerLongitude } ) , responseType : Responses . DiskEncounterResponse } ) ; } ; this . collectDailyDefenderBonus = function ( ) { return self . callOrChain ( { type : RequestType . COLLECT_DAILY_DEFENDER_BONUS , responseType : Responses . CollectDailyDefenderBonusResponse } ) ; } ; this . upgradePokemon = function ( pokemonID ) { return self . callOrChain ( { type : RequestType . UPGRADE_POKEMON , message : new RequestMessages . UpgradePokemonMessage ( { pokemon_id : pokemonID } ) , responseType : Responses . UpgradePokemonResponse } ) ; } ; this . setFavoritePokemon = function ( pokemonID , isFavorite ) { return self . callOrChain ( { type : RequestType . SET_FAVORITE_POKEMON , message : new RequestMessages . SetFavoritePokemonMessage ( { pokemon_id : pokemonID , is_favorite : isFavorite } ) , responseType : Responses . SetFavoritePokemonResponse } ) ; } ; this . nicknamePokemon = function ( pokemonID , nickname ) { return self . callOrChain ( { type : RequestType . NICKNAME_POKEMON , message : new RequestMessages . NicknamePokemonMessage ( { pokemon_id : pokemonID , nickname : nickname } ) , responseType : Responses . NicknamePokemonResponse } ) ; } ; this . equipBadge = function ( badgeType ) { return self . callOrChain ( { type : RequestType . EQUIP_BADGE , message : new RequestMessages . EquipBadgeMessage ( { badge_type : badgeType } ) , responseType : Responses . EquipBadgeResponse } ) ; } ; this . setContactSettings = function ( sendMarketingEmails , sendPushNotifications ) { return self . callOrChain ( { type : RequestType . SET_CONTACT_SETTINGS , message : new RequestMessages . SetContactSettingsMessage ( { contact_settings : { send_marketing_emails : sendMarketingEmails , send_push_notifications : sendPushNotifications } } ) , responseType : Responses . SetContactSettingsResponse } ) ; } ; this . setBuddyPokemon = function ( pokemonID ) { return self . callOrChain ( { type : RequestType . SET_BUDDY_POKEMON , message : new RequestMessages . SetBuddyPokemonMessage ( { pokemon_id : pokemonID } ) , responseType : Responses . SetBuddyPokemonResponse } ) ; } ; this . getBuddyWalked = function ( ) { return self . callOrChain ( { type : RequestType . GET_BUDDY_WALKED , responseType : Responses . GetBuddyWalkedResponse } ) ; } ; this . useItemEncounter = function ( itemID , encounterID , spawnPointGUID ) { return self . callOrChain ( { type : RequestType . USE_ITEM_ENCOUNTER , message : new RequestMessages . UseItemEncounterMessage ( { item : itemID , encounter_id : encounterID , spawn_point_guid : spawnPointGUID } ) , responseType : Responses . UseItemEncounterResponse } ) ; } ; this . getAssetDigest = function ( platform , deviceManufacturer , deviceModel , locale , appVersion ) { return self . callOrChain ( { type : RequestType . GET_ASSET_DIGEST , message : new RequestMessages . GetAssetDigestMessage ( { platform : platform , device_manufacturer : deviceManufacturer , device_model : deviceModel , locale : locale , app_version : appVersion } ) , responseType : Responses . GetAssetDigestResponse } ) ; } ; this . getDownloadURLs = function ( assetIDs ) { return self . callOrChain ( { type : RequestType . GET_DOWNLOAD_URLS , message : new RequestMessages . GetDownloadUrlsMessage ( { asset_id : assetIDs } ) , responseType : Responses . GetDownloadUrlsResponse } ) ; } ; this . claimCodename = function ( codename ) { return self . callOrChain ( { type : RequestType . CLAIM_CODENAME , message : new RequestMessages . ClaimCodenameMessage ( { codename : codename } ) , responseType : Responses . ClaimCodenameResponse } ) ; } ; this . setAvatar = function ( playerAvatar ) { return self . callOrChain ( { type : RequestType . SET_AVATAR , message : new RequestMessages . SetAvatarMessage ( { player_avatar : playerAvatar } ) , responseType : Responses . SetAvatarResponse } ) ; } ; this . setPlayerTeam = function ( teamColor ) { return self . callOrChain ( { type : RequestType . SET_PLAYER_TEAM , message : new RequestMessages . SetPlayerTeamMessage ( { team : teamColor } ) , responseType : Responses . SetPlayerTeamResponse } ) ; } ; this . markTutorialComplete = function ( tutorialsCompleted , sendMarketingEmails , sendPushNotifications ) { return self . callOrChain ( { type : RequestType . MARK_TUTORIAL_COMPLETE , message : new RequestMessages . MarkTutorialCompleteMessage ( { tutorials_completed : tutorialsCompleted , send_marketing_emails : sendMarketingEmails , send_push_notifications : sendPushNotifications } ) , responseType : Responses . MarkTutorialCompleteResponse } ) ; } ; this . checkChallenge = function ( isDebugRequest ) { return self . callOrChain ( { type : RequestType . CHECK_CHALLENGE , message : new RequestMessages . CheckChallengeMessage ( { debug_request : isDebugRequest } ) , responseType : Responses . CheckChallengeResponse } ) ; } ; this . verifyChallenge = function ( token ) { return self . callOrChain ( { type : RequestType . VERIFY_CHALLENGE , message : new RequestMessages . VerifyChallengeMessage ( { token : token } ) , responseType : Responses . VerifyChallengeResponse } ) ; } ; this . echo = function ( ) { return self . callOrChain ( { type : RequestType . ECHO , responseType : Responses . EchoResponse } ) ; } ; this . sfidaActionLog = function ( ) { return self . callOrChain ( { type : RequestType . SFIDA_ACTION_LOG , responseType : Responses . SfidaActionLogResponse } ) ; } ; this . listAvatarCustomizations = function ( avatarType , slots , filters , start , limit ) { return self . callOrChain ( { type : RequestType . LIST_AVATAR_CUSTOMIZATIONS , message : new RequestMessages . ListAvatarCustomizationsMessage ( { avatar_type : avatarType , slot : slots , filters : filters , start : start , limit : limit } ) , responseType : Responses . ListAvatarCustomizationsResponse } ) ; } ; this . setAvatarItemAsViewed = function ( avatarTemplateIDs ) { return self . callOrChain ( { type : RequestType . SET_AVATAR_ITEM_AS_VIEWED , message : new RequestMessages . SetAvatarItemAsViewedMessage ( { avatar_template_id : avatarTemplateIDs } ) , responseType : Responses . SetAvatarItemAsViewdResponse } ) ; } ; /*
     * INTERNAL STUFF
     */ this . request = request . defaults ( { headers : { 'User-Agent' : 'Niantic App' , 'Accept' : '*/*' , 'Content-Type' : 'application/x-www-form-urlencoded' } , encoding : null } ) ; this . options = Object . assign ( { } , defaultOptions , options || { } ) ; this . authTicket = null ; this . rpcId = 2 ; this . lastHashingKeyIndex = 0 ; this . firstGetMapObjects = true ; this . lehmer = new Lehmer ( 16807 ) ; this . ptr8 = INITIAL_PTR8 ; /**
     * Executes a request and returns a Promise or, if we are in batch mode, adds it to the
     * list of batched requests and returns this (for chaining).
     * @private
     * @param {object} requestMessage - RPC request object
     * @return {Promise|Client}
     */ this . callOrChain = function ( requestMessage ) { if ( self . batchRequests ) { self . batchRequests . push ( requestMessage ) ; return self ; } else { return self . callRPC ( [ requestMessage ] ) ; } } ; /**
     * Generates next rpc request id
     * @private
     * @return {Long}
     */ this . getRequestID = function ( ) { return new Long ( self . rpcId ++ , this . lehmer . nextInt ( ) ) ; } ; /**
     * Creates an RPC envelope with the given list of requests.
     * @private
     * @param {Object[]} requests - Array of requests to build
     * @return {POGOProtos.Networking.Envelopes.RequestEnvelope}
     */ this . buildEnvelope = function ( requests ) { var envelopeData = { status_code : 2 , request_id : self . getRequestID ( ) , ms_since_last_locationfix : 100 + Math . floor ( Math . random ( ) * 900 ) } ; if ( self . playerLatitude ) envelopeData . latitude = self . playerLatitude ; if ( self . playerLongitude ) envelopeData . longitude = self . playerLongitude ; if ( self . playerLocationAccuracy ) { envelopeData . accuracy = self . playerLocationAccuracy ; } else { const values = [ 5 , 5 , 5 , 5 , 10 , 10 , 10 , 30 , 30 , 50 , 65 ] ; values . unshift ( Math . floor ( Math . random ( ) * ( 80 - 66 ) ) + 66 ) ; envelopeData . accuracy = values [ Math . floor ( values . length * Math . random ( ) ) ] ; } if ( self . authTicket ) { envelopeData . auth_ticket = self . authTicket ; } else if ( ! self . options . authType || ! self . options . authToken ) { throw Error ( 'No auth info provided' ) ; } else { let unknown2 = 0 ; if ( self . options . authType === 'ptc' ) { const values = [ 2 , 8 , 21 , 21 , 21 , 28 , 37 , 56 , 59 , 59 , 59 ] ; unknown2 = values [ Math . floor ( values . length * Math . random ( ) ) ] ; } envelopeData . auth_info = { provider : self . options . authType , token : { contents : self . options . authToken , unknown2 : unknown2 , } } ; } if ( requests ) { self . emit ( 'request' , { request_id : envelopeData . request_id . toString ( ) , requests : requests . map ( r => ( { name : Utils . getEnumKeyByValue ( RequestType , r . type ) , type : r . type , data : r . message } ) ) } ) ; envelopeData . requests = requests . map ( r => { var requestData = { request_type : r . type } ; if ( r . message ) { requestData . request_message = r . message . encode ( ) ; } return requestData ; } ) ; } self . emit ( 'raw-request' , envelopeData ) ; return new POGOProtos . Networking . Envelopes . RequestEnvelope ( envelopeData ) ; } ; /**
     * Constructs and adds a platform request to a request envelope.
     * @private
     * @param {RequestEnvelope} envelope - Request envelope
     * @param {PlatformRequestType} requestType - Type of the platform request to add
     * @param {Object} requestMessage - Pre-built but not encoded PlatformRequest protobuf message
     * @return {RequestEnvelope} The envelope (for convenience only)
     */ this . addPlatformRequestToEnvelope = function ( envelope , requestType , requestMessage ) { envelope . platform_requests . push ( new POGOProtos . Networking . Envelopes . RequestEnvelope . PlatformRequest ( { type : requestType , request_message : requestMessage . encode ( ) } ) ) ; return envelope ; } ; /**
     * Determines whether the as of yet unknown platform request type 8 should be added
     * to the envelope based on the given type of requests.
     * @private
     * @param {Object[]} requests - Array of request data
     * @return {boolean}
     */ this . needsPtr8 = function ( requests ) { // Single GET_PLAYER request always gets PTR8 if ( requests . length === 1 && requests [ 0 ] . type === RequestType . GET_PLAYER ) { return true ; } // Any GET_MAP_OBJECTS requests get PTR8 except the first one in the session if ( requests . some ( r => r . type === RequestType . GET_MAP_OBJECTS ) ) { if ( self . firstGetMapObjects ) { self . firstGetMapObjects = false ; return false ; } return true ; } return false ; } ; /**
     * Creates an RPC envelope with the given list of requests and adds the encrypted signature,
     * or adds the signature to an existing envelope.
     * @private
     * @param {Object[]} requests - Array of requests to build
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to sign
     * @return {Promise} - A Promise that will be resolved with a RequestEnvelope instance
     */ this . buildSignedEnvelope = function ( requests , envelope ) { if ( ! envelope ) { try { envelope = self . buildEnvelope ( requests ) ; } catch ( e ) { throw new retry . StopError ( e ) ; } } if ( self . needsPtr8 ( requests ) ) { self . addPlatformRequestToEnvelope ( envelope , PlatformRequestType . UNKNOWN_PTR_8 , new PlatformRequestMessages . UnknownPtr8Request ( { message : self . ptr8 , } ) ) ; } let authTicket = envelope . auth_ticket ; if ( ! authTicket ) { authTicket = envelope . auth_info ; } if ( ! authTicket ) { // Can't sign before we have received an auth ticket return Promise . resolve ( envelope ) ; } if ( self . options . useHashingServer ) { let key = self . options . hashingKey ; if ( Array . isArray ( key ) ) { key = key [ self . lastHashingKeyIndex ] ; self . lastHashingKeyIndex = ( self . lastHashingKeyIndex + 1 ) % self . options . hashingKey . length ; } self . signatureBuilder . useHashingServer ( self . options . hashingServer + self . hashingVersion , key ) ; } self . signatureBuilder . setAuthTicket ( authTicket ) ; if ( typeof self . options . signatureInfo === 'function' ) { self . signatureBuilder . setFields ( self . options . signatureInfo ( envelope ) ) ; } else if ( self . options . signatureInfo ) { self . signatureBuilder . setFields ( self . options . signatureInfo ) ; } self . signatureBuilder . setLocation ( envelope . latitude , envelope . longitude , envelope . accuracy ) ; return retry ( ( ) => self . signatureBuilder . encryptAsync ( envelope . requests ) . catch ( err => { if ( err . name === 'HashServerError' && err . message === 'Request limited' ) { throw err ; } else { throw new retry . StopError ( err ) ; } } ) , { interval : 1000 , backoff : 2 , max_tries : 10 , args : envelope . requests , } ) . then ( sigEncrypted => self . addPlatformRequestToEnvelope ( envelope , PlatformRequestType . SEND_ENCRYPTED_SIGNATURE , new PlatformRequestMessages . SendEncryptedSignatureRequest ( { encrypted_signature : sigEncrypted } ) ) ) ; } ; /**
     * Executes an RPC call with the given list of requests, retrying if necessary.
     * @private
     * @param {Object[]} requests - Array of requests to send
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to use
     * @return {Promise} - A Promise that will be resolved with the (list of) response messages,
     *     or true if there aren't any
     */ this . callRPC = function ( requests , envelope ) { // If the requests include a map objects request, make sure the minimum delay // since the last call has passed if ( requests . some ( r => r . type === RequestType . GET_MAP_OBJECTS ) ) { var now = new Date ( ) . getTime ( ) , delayNeeded = self . lastMapObjectsCall + self . options . mapObjectsMinDelay - now ; if ( delayNeeded > 0 && self . options . mapObjectsThrottling ) { return Promise . delay ( delayNeeded ) . then ( ( ) => self . callRPC ( requests , envelope ) ) ; } self . lastMapObjectsCall = now ; } if ( self . options . maxTries <= 1 ) return self . tryCallRPC ( requests , envelope ) ; return retry ( ( ) => self . tryCallRPC ( requests , envelope ) , { interval : 300 , backoff : 2 , max_tries : self . options . maxTries } ) ; } ; /**
     * Handle redirection to new API endpoint and resend last request to new endpoint.
     * @private
     * @param {Object[]} requests - Array of requests
     * @param {RequestEnvelope} signedEnvelope - Request envelope
     * @param {ResponseEnvelope} responseEnvelope - Result from API call
     * @return {Promise}
     */ this . redirect = function ( requests , signedEnvelope , responseEnvelope ) { return new Promise ( ( resolve , reject ) => { if ( ! responseEnvelope . api_url ) { reject ( Error ( 'Fetching RPC endpoint failed, none supplied in response' ) ) ; return ; } self . endpoint = 'https://' + responseEnvelope . api_url + '/rpc' ; self . emit ( 'endpoint-response' , { status_code : responseEnvelope . status_code , request_id : responseEnvelope . request_id . toString ( ) , api_url : responseEnvelope . api_url } ) ; signedEnvelope . platform_requests = [ ] ; resolve ( self . callRPC ( requests , signedEnvelope ) ) ; } ) ; } ; /**
     * Executes an RPC call with the given list of requests.
     * @private
     * @param {Object[]} requests - Array of requests to send
     * @param {RequestEnvelope} [envelope] - Pre-built request envelope to use
     * @return {Promise} - A Promise that will be resolved with the (list of) response messages,
     *     or true if there aren't any
     */ this . tryCallRPC = function ( requests , envelope ) { return self . buildSignedEnvelope ( requests , envelope ) . then ( signedEnvelope => new Promise ( ( resolve , reject ) => { self . request ( { method : 'POST' , url : self . endpoint , proxy : self . options . proxy , body : signedEnvelope . toBuffer ( ) } , ( err , response , body ) => { if ( err ) { reject ( Error ( err ) ) ; return ; } if ( response . statusCode !== 200 ) { if ( response . statusCode >= 400 && response . statusCode < 500 ) { /* These are permanent errors so throw StopError */ reject ( new retry . StopError ( ` ${ response . statusCode } ` ) ) ; } else { /* Anything else might be recoverable so throw regular Error */ reject ( Error ( ` ${ response . statusCode } ` ) ) ; } return ; } var responseEnvelope ; try { responseEnvelope = POGOProtos . Networking . Envelopes . ResponseEnvelope . decode ( body ) ; } catch ( e ) { self . emit ( 'parse-envelope-error' , body , e ) ; if ( e . decoded ) { responseEnvelope = e . decoded ; } else { reject ( new retry . StopError ( e ) ) ; return ; } } self . emit ( 'raw-response' , responseEnvelope ) ; if ( responseEnvelope . error ) { reject ( new retry . StopError ( responseEnvelope . error ) ) ; return ; } if ( responseEnvelope . auth_ticket ) self . authTicket = responseEnvelope . auth_ticket ; if ( responseEnvelope . status_code === 53 || ( responseEnvelope . status_code === 2 && self . endpoint === INITIAL_ENDPOINT ) ) { resolve ( self . redirect ( requests , signedEnvelope , responseEnvelope ) ) ; return ; } responseEnvelope . platform_returns . forEach ( platformReturn => { if ( platformReturn . type === PlatformRequestType . UNKNOWN_PTR_8 ) { const ptr8 = PlatformResponses . UnknownPtr8Response . decode ( platformReturn . response ) ; if ( ptr8 ) self . ptr8 = ptr8 . message ; } } ) ; /* Throttling, retry same request later */ if ( responseEnvelope . status_code === 52 ) { signedEnvelope . platform_requests = [ ] ; Promise . delay ( 2000 ) . then ( ( ) => { resolve ( self . callRPC ( requests , signedEnvelope ) ) ; } ) ; return ; } /* These codes indicate invalid input, no use in retrying so throw StopError */ if ( responseEnvelope . status_code === 3 || responseEnvelope . status_code === 51 || responseEnvelope . status_code >= 100 ) { reject ( new retry . StopError ( ` ${ responseEnvelope . status_code } ` ) ) ; } /* These can be temporary so throw regular Error */ if ( responseEnvelope . status_code !== 2 && responseEnvelope . status_code !== 1 ) { reject ( Error ( ` ${ responseEnvelope . status_code } ` ) ) ; return ; } var responses = [ ] ; if ( requests ) { if ( requests . length !== responseEnvelope . returns . length ) { reject ( Error ( 'Request count does not match response count' ) ) ; return ; } for ( var i = 0 ; i < responseEnvelope . returns . length ; i ++ ) { if ( ! requests [ i ] . responseType ) continue ; var responseMessage ; try { responseMessage = requests [ i ] . responseType . decode ( responseEnvelope . returns [ i ] ) ; } catch ( e ) { self . emit ( 'parse-response-error' , responseEnvelope . returns [ i ] . toBuffer ( ) , e ) ; reject ( new retry . StopError ( e ) ) ; return ; } if ( self . options . includeRequestTypeInResponse ) { // eslint-disable-next-line no-underscore-dangle responseMessage . _requestType = requests [ i ] . type ; } responses . push ( responseMessage ) ; } } self . emit ( 'response' , { status_code : responseEnvelope . status_code , request_id : responseEnvelope . request_id . toString ( ) , responses : responses . map ( ( r , h ) => ( { name : Utils . getEnumKeyByValue ( RequestType , requests [ h ] . type ) , type : requests [ h ] . type , data : r } ) ) } ) ; if ( self . options . automaticLongConversion ) { responses = Utils . convertLongs ( responses ) ; } if ( ! responses . length ) resolve ( true ) ; else if ( responses . length === 1 ) resolve ( responses [ 0 ] ) ; else resolve ( responses ) ; } ) ; } ) ) ; } ; /**
     * Processes the data received from the downloadSettings API call during init().
     * @private
     * @param {Object} settingsResponse - Response from API call
     * @return {Object} response - Unomdified response (to send back to Promise)
     */ this . processSettingsResponse = function ( settingsResponse ) { // Extract the minimum delay of getMapObjects() if ( settingsResponse && ! settingsResponse . error && settingsResponse . settings && settingsResponse . settings . map_settings && settingsResponse . settings . map_settings . get_map_objects_min_refresh_seconds ) { self . setOption ( 'mapObjectsMinDelay' , settingsResponse . settings . map_settings . get_map_objects_min_refresh_seconds * 1000 ) ; } return settingsResponse ; } ; /**
     * Makes an initial call to the hashing server to verify API version.
     * @private
     * @return {Promise}
     */ this . initializeHashingServer = function ( ) { if ( ! self . options . hashingServer ) throw new Error ( 'Hashing server enabled without host' ) ; if ( ! self . options . hashingKey ) throw new Error ( 'Hashing server enabled without key' ) ; if ( self . options . hashingServer . slice ( - 1 ) !== '/' ) { self . setOption ( 'hashingServer' , self . options . hashingServer + '/' ) ; } return Signature . versions . getHashingEndpoint ( self . options . hashingServer , self . options . version ) . then ( version => { self . hashingVersion = version ; } ) ; } ; /*
     * DEPRECATED METHODS
     */ /**
     * Sets the authType and authToken options.
     * @deprecated Use options object or setOption() instead
     * @param {string} authType
     * @param {string} authToken
     */ this . setAuthInfo = function ( authType , authToken ) { self . setOption ( 'authType' , authType ) ; self . setOption ( 'authToken' , authToken ) ; } ; /**
     * Sets the includeRequestTypeInResponse option.
     * @deprecated Use options object or setOption() instead
     * @param {bool} includeRequestTypeInResponse
     */ this . setIncludeRequestTypeInResponse = function ( includeRequestTypeInResponse ) { self . setOption ( 'includeRequestTypeInResponse' , includeRequestTypeInResponse ) ; } ; /**
     * Sets the maxTries option.
     * @deprecated Use options object or setOption() instead
     * @param {integer} maxTries
     */ this . setMaxTries = function ( maxTries ) { self . setOption ( 'maxTries' , maxTries ) ; } ; /**
     * Sets the proxy option.
     * @deprecated Use options object or setOption() instead
     * @param {string} proxy
     */ this . setProxy = function ( proxy ) { self . setOption ( 'proxy' , proxy ) ; } ; /**
     * Sets the mapObjectsThrottling option.
     * @deprecated Use options object or setOption() instead
     * @param {boolean} enable
     */ this . setMapObjectsThrottlingEnabled = function ( enable ) { self . setOption ( 'mapObjectsThrottling' , enable ) ; } ; /**
     * Sets a callback to be called for any envelope or request just before it is sent to
     * the server (mostly for debugging purposes).
     * @deprecated Use the raw-request event instead
     * @param {function} callback - function to call on requests
     */ this . setRequestCallback = function ( callback ) { self . on ( 'raw-request' , callback ) ; } ; /**
     * Sets a callback to be called for any envelope or response just after it has been
     * received from the server (mostly for debugging purposes).
     * @deprecated Use the raw-response event instead
     * @param {function} callback - function to call on responses
     */ this . setResponseCallback = function ( callback ) { self . on ( 'raw-response' , callback ) ; } ; /**
     * Sets the automaticLongConversion option.
     * @deprecated Use options object or setOption() instead
     * @param {boolean} enable
     */ this . setAutomaticLongConversionEnabled = function ( enable ) { if ( typeof enable !== 'boolean' ) return ; self . setOption ( 'automaticLongConversion' , enable ) ; } ; }
Pokémon Go RPC client.
@class Client
@param {Object} [options] - Client options (see pogobuf wiki for documentation)
@memberof pogobuf
Pokémon Go RPC client .