UNPKG

fnbr

Version:

A library to interact with Epic Games' Fortnite HTTP and XMPP services

1,043 lines 44.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); /* eslint-disable no-restricted-syntax */ const events_1 = require("events"); const Enums_1 = tslib_1.__importDefault(require("../enums/Enums")); const Util_1 = require("./util/Util"); const Auth_1 = tslib_1.__importDefault(require("./auth/Auth")); const HTTP_1 = tslib_1.__importDefault(require("./http/HTTP")); const AsyncLock_1 = tslib_1.__importDefault(require("./util/AsyncLock")); const Endpoints_1 = tslib_1.__importDefault(require("../resources/Endpoints")); const XMPP_1 = tslib_1.__importDefault(require("./xmpp/XMPP")); const Friend_1 = tslib_1.__importDefault(require("./structures/friend/Friend")); const UserNotFoundError_1 = tslib_1.__importDefault(require("./exceptions/UserNotFoundError")); const StatsPrivacyError_1 = tslib_1.__importDefault(require("./exceptions/StatsPrivacyError")); const CreatorCode_1 = tslib_1.__importDefault(require("./structures/CreatorCode")); const CreatorCodeNotFoundError_1 = tslib_1.__importDefault(require("./exceptions/CreatorCodeNotFoundError")); const FriendNotFoundError_1 = tslib_1.__importDefault(require("./exceptions/FriendNotFoundError")); const IncomingPendingFriend_1 = tslib_1.__importDefault(require("./structures/friend/IncomingPendingFriend")); const OutgoingPendingFriend_1 = tslib_1.__importDefault(require("./structures/friend/OutgoingPendingFriend")); const BlockedUser_1 = tslib_1.__importDefault(require("./structures/user/BlockedUser")); const ClientParty_1 = tslib_1.__importDefault(require("./structures/party/ClientParty")); const Party_1 = tslib_1.__importDefault(require("./structures/party/Party")); const PartyNotFoundError_1 = tslib_1.__importDefault(require("./exceptions/PartyNotFoundError")); const PartyPermissionError_1 = tslib_1.__importDefault(require("./exceptions/PartyPermissionError")); const SentPartyJoinRequest_1 = tslib_1.__importDefault(require("./structures/party/SentPartyJoinRequest")); const RadioStation_1 = tslib_1.__importDefault(require("./structures/RadioStation")); const CreativeIslandNotFoundError_1 = tslib_1.__importDefault(require("./exceptions/CreativeIslandNotFoundError")); const Stats_1 = tslib_1.__importDefault(require("./structures/Stats")); const NewsMessage_1 = tslib_1.__importDefault(require("./structures/NewsMessage")); const EventTimeoutError_1 = tslib_1.__importDefault(require("./exceptions/EventTimeoutError")); const FortniteServerStatus_1 = tslib_1.__importDefault(require("./structures/FortniteServerStatus")); const EpicgamesServerStatus_1 = tslib_1.__importDefault(require("./structures/EpicgamesServerStatus")); const TournamentManager_1 = tslib_1.__importDefault(require("./managers/TournamentManager")); const enums_1 = require("../resources/enums"); const EpicgamesAPIError_1 = tslib_1.__importDefault(require("./exceptions/EpicgamesAPIError")); const UserManager_1 = tslib_1.__importDefault(require("./managers/UserManager")); const FriendManager_1 = tslib_1.__importDefault(require("./managers/FriendManager")); const STWManager_1 = tslib_1.__importDefault(require("./managers/STWManager")); const STOMP_1 = tslib_1.__importDefault(require("./stomp/STOMP")); const ChatManager_1 = tslib_1.__importDefault(require("./managers/ChatManager")); /** * Represents the main client */ class Client extends events_1.EventEmitter { /** * @param config The client's configuration options */ constructor(config = {}) { var _a, _b; super(); this.config = { savePartyMemberMeta: true, http: {}, debug: undefined, httpDebug: undefined, xmppDebug: undefined, defaultStatus: undefined, defaultOnlineType: Enums_1.default.PresenceOnlineType.ONLINE, platform: 'WIN', defaultPartyMemberMeta: {}, xmppKeepAliveInterval: 30, xmppMaxConnectionRetries: 2, createParty: true, forceNewParty: true, disablePartyService: false, connectToXMPP: true, connectToSTOMP: true, fetchFriends: true, restRetryLimit: 1, handleRatelimits: true, partyBuildId: '1:3:', restartOnInvalidRefresh: false, language: 'en', friendOnlineConnectionTimeout: 30000, friendOfflineTimeout: 300000, eosDeploymentId: '62a9473a2dca46b29ccf17577fcf42d7', xmppConnectionTimeout: 15000, stompConnectionTimeout: 15000, ...config, cacheSettings: { ...config.cacheSettings, presences: { maxLifetime: Infinity, sweepInterval: 0, ...(_a = config.cacheSettings) === null || _a === void 0 ? void 0 : _a.presences, }, users: { maxLifetime: 0, sweepInterval: 0, ...(_b = config.cacheSettings) === null || _b === void 0 ? void 0 : _b.users, }, }, auth: { authorizationCode: async () => (0, Util_1.consoleQuestion)('Please enter an authorization code: '), checkEULA: true, killOtherTokens: true, createLauncherSession: false, authClient: 'fortniteAndroidGameClient', ...config.auth, }, partyConfig: { privacy: Enums_1.default.PartyPrivacy.PUBLIC, joinConfirmation: false, joinability: 'OPEN', maxSize: 16, chatEnabled: true, discoverability: 'ALL', ...config.partyConfig, }, }; this.timeouts = new Set(); this.intervals = new Set(); this.auth = new Auth_1.default(this); this.http = new HTTP_1.default(this); this.xmpp = new XMPP_1.default(this); this.stomp = new STOMP_1.default(this); this.partyLock = new AsyncLock_1.default(); this.cacheLock = new AsyncLock_1.default(); this.isReady = false; this.friend = new FriendManager_1.default(this); this.user = new UserManager_1.default(this); this.party = undefined; this.chat = new ChatManager_1.default(this); this.tournaments = new TournamentManager_1.default(this); this.lastPartyMemberMeta = this.config.defaultPartyMemberMeta; this.stw = new STWManager_1.default(this); } // Events on(event, listener) { return super.on(event, listener); } once(event, listener) { return super.once(event, listener); } emit(event, ...args) { return super.emit(event, ...args); } /* -------------------------------------------------------------------------- */ /* CLIENT LOGIN AND LOGOUT */ /* -------------------------------------------------------------------------- */ /** * Logs the client in. * A valid authentication method must be provided in the client's config. * By default, there will be a console prompt asking for an authorization code * @throws {EpicgamesAPIError} */ async login() { await this.auth.authenticate(); await this.user.fetchSelf(); this.initCacheSweeping(); this.cacheLock.lock(); try { await Promise.all([ this.config.connectToXMPP && this.xmpp.connect(), this.config.connectToSTOMP && this.stomp.connect(), ]); if (this.config.fetchFriends) await this.updateCaches(); } finally { this.cacheLock.unlock(); } if (!this.config.disablePartyService) await this.initParty(this.config.createParty, this.config.forceNewParty); if (this.xmpp.isConnected) this.setStatus(); this.isReady = true; this.emit('ready'); } /** * Logs the client out. * Also clears all caches, etc */ async logout() { await this.auth.revokeAllTokens(); this.xmpp.disconnect(); this.destroy(); this.isReady = false; this.emit('disconnected'); } /** * Restarts the client */ async restart() { var _a; const refreshToken = (_a = this.auth.sessions.get(enums_1.AuthSessionStoreKey.Fortnite)) === null || _a === void 0 ? void 0 : _a.refreshToken; await this.logout(); this.config.auth.refreshToken = refreshToken; await this.login(); this.config.auth.refreshToken = undefined; } /** * Initializes {@link Client#party} * @param createNew Whether to create a new party * @param forceNew Whether to force create a new party */ async initParty(createNew = true, forceNew = true) { this.party = await this.getClientParty(); if (!forceNew && this.party) return; if (createNew) { await this.leaveParty(false); await this.createParty(); } } /** * Internal method that sets a {@link ClientParty} to the value of {@link Client#party} * @param party The party * @private */ setClientParty(party) { this.party = new ClientParty_1.default(this, party); } /** * Waits until the client is ready * @param timeout How long to wait for until an error is thrown */ async waitUntilReady(timeout = 10000) { if (this.isReady) return; this.setMaxListeners(this.getMaxListeners() + 1); try { await this.waitForEvent('ready', timeout); } finally { this.setMaxListeners(this.getMaxListeners() - 1); } } /** * Cleanup method */ destroy() { // Clear timeouts for (const interval of this.intervals) clearInterval(interval); for (const timeout of this.timeouts) clearTimeout(timeout); this.timeouts.clear(); this.intervals.clear(); // Clear remaining caches this.friend.list.clear(); this.friend.pendingList.clear(); this.user.blocklist.clear(); this.user.cache.clear(); } /** * Initializes the sweeping of cached objects */ initCacheSweeping() { const { cacheSettings } = this.config; const presenceCacheSettings = cacheSettings.presences; if (presenceCacheSettings && presenceCacheSettings.sweepInterval && presenceCacheSettings.sweepInterval > 0 && presenceCacheSettings.maxLifetime > 0 && presenceCacheSettings.maxLifetime !== Infinity) { this.setInterval(this.sweepPresences.bind(this), presenceCacheSettings.sweepInterval); } const userCacheSettings = cacheSettings.users; if (userCacheSettings && userCacheSettings.sweepInterval && userCacheSettings.sweepInterval > 0 && userCacheSettings.maxLifetime > 0 && userCacheSettings.maxLifetime !== Infinity) { this.setInterval(this.sweepUsers.bind(this), userCacheSettings.sweepInterval); } } /** * Updates the client's caches */ async updateCaches() { const friendsSummary = await this.http.epicgamesRequest({ url: `${Endpoints_1.default.FRIENDS}/${this.user.self.id}/summary`, }, enums_1.AuthSessionStoreKey.Fortnite); this.friend.list.clear(); this.friend.pendingList.clear(); this.user.blocklist.clear(); friendsSummary.friends.forEach((f) => { this.friend.list.set(f.accountId, new Friend_1.default(this, { ...f, id: f.accountId })); }); friendsSummary.incoming.forEach((f) => { this.friend.pendingList.set(f.accountId, new IncomingPendingFriend_1.default(this, { ...f, id: f.accountId })); }); friendsSummary.outgoing.forEach((f) => { this.friend.pendingList.set(f.accountId, new OutgoingPendingFriend_1.default(this, { ...f, id: f.accountId })); }); friendsSummary.blocklist.forEach((u) => { this.user.blocklist.set(u.accountId, new BlockedUser_1.default(this, { ...u, id: u.accountId })); }); const users = await this.user.fetchMultiple([ ...this.friend.list.values(), ...this.friend.pendingList.values(), ...this.user.blocklist.values(), ] .filter((u) => !!u.id) .map((u) => u.id)); users.forEach((u) => { var _a, _b, _c; (_a = this.friend.list.get(u.id)) === null || _a === void 0 ? void 0 : _a.update(u); (_b = this.friend.pendingList.get(u.id)) === null || _b === void 0 ? void 0 : _b.update(u); (_c = this.user.blocklist.get(u.id)) === null || _c === void 0 ? void 0 : _c.update(u); }); } /** * Removes presences from the client's cache that are older than the max lifetime * @param maxLifetime How old a presence cache entry must be before it can be sweeped (in seconds) * @returns The amount of presences sweeped */ sweepPresences(maxLifetime) { var _a, _b; if (maxLifetime === void 0) { maxLifetime = (_a = this.config.cacheSettings.presences) === null || _a === void 0 ? void 0 : _a.maxLifetime; } if (typeof maxLifetime !== 'number') { throw new TypeError('maxLifetime must be typeof number'); } let presences = 0; for (const friend of this.friend.list.values()) { if (typeof ((_b = friend.presence) === null || _b === void 0 ? void 0 : _b.receivedAt) !== 'undefined' && Date.now() - friend.presence.receivedAt.getTime() > maxLifetime * 1000) { delete friend.presence; presences += 1; } } return presences; } /** * Removes users from the client's cache that are older than the max lifetime * @param maxLifetime How old a user cache entry must be before it can be sweeped (in seconds) * @returns The amount of users sweeped */ sweepUsers(maxLifetime) { var _a; if (maxLifetime === void 0) { maxLifetime = (_a = this.config.cacheSettings.users) === null || _a === void 0 ? void 0 : _a.maxLifetime; } if (typeof maxLifetime !== 'number') { throw new TypeError('maxLifetime must be typeof number'); } let users = 0; for (const user of this.user.cache.values()) { if (Date.now() - user.cachedAt > maxLifetime * 1000) { this.user.cache.delete(user.id); users += 1; } } return users; } /* -------------------------------------------------------------------------- */ /* UTIL */ /* -------------------------------------------------------------------------- */ /** * Wait until an event is emitted * @param event The event that will be waited for * @param timeout The timeout (in milliseconds) * @param filter The filter for the event */ waitForEvent(event, timeout = 5000, // eslint-disable-next-line no-unused-vars filter) { return new Promise((res, rej) => { // eslint-disable-next-line no-undef let rejectionTimeout; const handler = (...data) => { if (!filter || filter(...data)) { this.removeListener(event, handler); if (rejectionTimeout) clearTimeout(rejectionTimeout); res(data); } }; this.on(event, handler); const err = new EventTimeoutError_1.default(event, timeout); rejectionTimeout = setTimeout(() => { this.removeListener(event, handler); rej(err); }, timeout); }); } /** * Sets a timeout that will be automatically cancelled if the client is logged out * @param fn Function to execute * @param delay Time to wait before executing (in milliseconds) * @param args Arguments for the function */ // eslint-disable-next-line no-unused-vars setTimeout(fn, delay, ...args) { const timeout = setTimeout(() => { fn(args); this.timeouts.delete(timeout); }, delay); this.timeouts.add(timeout); return timeout; } /** * Clears a timeout * @param timeout Timeout to cancel */ // eslint-disable-next-line no-undef clearTimeout(timeout) { clearTimeout(timeout); this.timeouts.delete(timeout); } /** * Sets an interval that will be automatically cancelled if the client is logged out * @param fn Function to execute * @param delay Time to wait between executions (in milliseconds) * @param args Arguments for the function */ // eslint-disable-next-line no-unused-vars setInterval(fn, delay, ...args) { const interval = setInterval(fn, delay, ...args); this.intervals.add(interval); return interval; } /** * Clears an interval. * @param interval Interval to cancel */ // eslint-disable-next-line no-undef clearInterval(interval) { clearInterval(interval); this.intervals.delete(interval); } static consoleQuestion(question) { return (0, Util_1.consoleQuestion)(question); } /** * Debug a message using the methods set in the client config * @param message Text to debug * @param type Debug type (regular, http or xmpp) */ debug(message, type = 'regular') { switch (type) { case 'regular': if (typeof this.config.debug === 'function') this.config.debug(message); break; case 'http': if (typeof this.config.httpDebug === 'function') { this.config.httpDebug(message); } break; case 'xmpp': if (typeof this.config.xmppDebug === 'function') { this.config.xmppDebug(message); } break; case 'stomp': if (typeof this.config.stompDebug === 'function') { this.config.stompDebug(message); } break; } } /* -------------------------------------------------------------------------- */ /* STATUS */ /* -------------------------------------------------------------------------- */ /** * Sets the clients XMPP status * @param status The status * @param onlineType The presence's online type (eg "away") * @param friend A specific friend you want to send this status to * @throws {FriendNotFoundError} The user does not exist or is not friends with the client */ setStatus(status, onlineType, friend) { var _a; let toJID; if (friend) { const resolvedFriend = this.friend.resolve(friend); if (!resolvedFriend) throw new FriendNotFoundError_1.default(friend); toJID = `${resolvedFriend.id}@${Endpoints_1.default.EPIC_PROD_ENV}`; } // eslint-disable-next-line no-undef-init let partyJoinInfoData = undefined; if (this.party) { const partyPrivacy = this.party.config.privacy; if (partyPrivacy.presencePermission === 'Noone' || (partyPrivacy.presencePermission === 'Leader' && !((_a = this.party.me) === null || _a === void 0 ? void 0 : _a.isLeader))) { partyJoinInfoData = { isPrivate: true, }; } else { partyJoinInfoData = { sourceId: this.user.self.displayName, sourceDisplayName: this.user.self.displayName, sourcePlatform: this.config.platform, partyId: this.party.id, partyTypeId: 286331153, key: 'k', appId: 'Fortnite', buildId: this.config.partyBuildId, partyFlags: -2024557306, notAcceptingReason: 0, pc: this.party.size, }; } } if (status && !toJID) this.config.defaultStatus = status; if (onlineType && !toJID) this.config.defaultOnlineType = onlineType; const rawStatus = { Status: status || this.config.defaultStatus || (this.party && `Lobby - ${this.party.size} / ${this.party.maxSize}`) || 'Playing Battle Royale', bIsPlaying: false, bIsJoinable: this.party && !this.party.isPrivate && this.party.size !== this.party.maxSize, bHasVoiceSupport: false, SessionId: '', ProductName: 'Fortnite', Properties: { 'party.joininfodata.286331153_j': partyJoinInfoData, FortBasicInfo_j: { homeBaseRating: 0, }, FortLFG_I: '0', FortPartySize_i: 1, FortSubGame_i: 1, InUnjoinableMatch_b: false, FortGameplayStats_j: { state: '', playlist: 'None', numKills: 0, bFellToDeath: false, }, }, }; const rawOnlineType = (onlineType || this.config.defaultOnlineType) === 'online' ? undefined : onlineType || this.config.defaultOnlineType; return this.xmpp.sendStatus(rawStatus, rawOnlineType, toJID); } /** * Resets the client's XMPP status and online type */ async resetStatus() { this.config.defaultStatus = undefined; this.config.defaultOnlineType = 'online'; return this.setStatus(); } /* -------------------------------------------------------------------------- */ /* PARTIES */ /* -------------------------------------------------------------------------- */ /** * Sends a party invitation to a friend * @param friend The friend that will receive the invitation * @throws {FriendNotFoundError} The user does not exist or is not friends with the client * @throws {PartyAlreadyJoinedError} The user is already a member of this party * @throws {PartyMaxSizeReachedError} The party reached its max size * @throws {PartyNotFoundError} The client is not in party * @throws {EpicgamesAPIError} */ async invite(friend) { if (!this.party) throw new PartyNotFoundError_1.default(); return this.party.invite(friend); } /** * Joins a party by its id * @param id The party id * @throws {PartyNotFoundError} The party wasn't found * @throws {PartyPermissionError} The party cannot be fetched * @throws {PartyMaxSizeReachedError} The party has reached its max size * @throws {EpicgamesAPIError} */ async joinParty(id) { const party = (await this.getParty(id)); return party.join(true); } /** * Creates a new party * @param config The party config * @throws {EpicgamesAPIError} */ async createParty(config) { var _a; if (this.party) await this.party.leave(); this.partyLock.lock(); const partyConfig = { ...this.config.partyConfig, ...config }; let party; try { party = await this.http.epicgamesRequest({ method: 'POST', url: `${Endpoints_1.default.BR_PARTY}/parties`, headers: { 'Content-Type': 'application/json', }, data: { config: { join_confirmation: partyConfig.joinConfirmation, joinability: partyConfig.joinability, max_size: partyConfig.maxSize, }, join_info: { connection: { id: this.xmpp.JID, meta: { 'urn:epic:conn:platform_s': this.config.platform, 'urn:epic:conn:type_s': 'game', }, yield_leadership: false, }, meta: { 'urn:epic:member:dn_s': this.user.self.displayName, }, }, meta: { 'urn:epic:cfg:party-type-id_s': 'default', 'urn:epic:cfg:build-id_s': this.config.partyBuildId, 'urn:epic:cfg:join-request-action_s': 'Manual', 'urn:epic:cfg:chat-enabled_b': ((_a = partyConfig.chatEnabled) === null || _a === void 0 ? void 0 : _a.toString()) || 'true', 'urn:epic:cfg:can-join_b': 'true', }, }, }, enums_1.AuthSessionStoreKey.Fortnite); } catch (e) { this.partyLock.unlock(); if (e instanceof EpicgamesAPIError_1.default && e.code === 'errors.com.epicgames.social.party.user_has_party') { await this.leaveParty(false); return this.createParty(config); } throw e; } this.party = new ClientParty_1.default(this, party); const newPrivacy = await this.party.setPrivacy(partyConfig.privacy || Enums_1.default.PartyPrivacy.PUBLIC, false); await this.party.sendPatch({ ...newPrivacy.updated, ...Object.keys(this.party.meta.schema) .filter((k) => !k.startsWith('urn:')) .reduce((obj, key) => { var _a; // eslint-disable-next-line no-param-reassign obj[key] = (_a = this.party) === null || _a === void 0 ? void 0 : _a.meta.schema[key]; return obj; }, {}), }, newPrivacy.deleted); this.partyLock.unlock(); return undefined; } /** * Leaves the client's current party * @param createNew Whether a new party should be created * @throws {EpicgamesAPIError} */ async leaveParty(createNew = true) { if (!this.party) return undefined; return this.party.leave(createNew); } /** * Sends a party join request to a friend. * When the friend confirms this, a party invite will be sent to the client * @param friend The friend * @throws {FriendNotFoundError} The user does not exist or is not friends with the client * @throws {PartyNotFoundError} The friend is not in a party * @throws {EpicgamesAPIError} */ async sendRequestToJoin(friend) { const resolvedFriend = this.friend.list.find((f) => f.displayName === friend || f.id === friend); if (!resolvedFriend) throw new FriendNotFoundError_1.default(friend); let intention; try { intention = await this.http.epicgamesRequest({ method: 'POST', url: `${Endpoints_1.default.BR_PARTY}/members/${resolvedFriend.id}/intentions/${this.user.self.id}`, headers: { 'Content-Type': 'application/json', }, data: { 'urn:epic:invite:platformdata_s': '', }, }, enums_1.AuthSessionStoreKey.Fortnite); } catch (e) { if (e instanceof EpicgamesAPIError_1.default && e.code === 'errors.com.epicgames.social.party.user_has_no_party') { throw new PartyNotFoundError_1.default(); } throw e; } return new SentPartyJoinRequest_1.default(this, this.user.self, resolvedFriend, intention); } /** * Fetches the client's party * @throws {EpicgamesAPIError} */ async getClientParty() { const party = await this.http.epicgamesRequest({ method: 'GET', url: `${Endpoints_1.default.BR_PARTY}/user/${this.user.self.id}`, }, enums_1.AuthSessionStoreKey.Fortnite); if (!(party === null || party === void 0 ? void 0 : party.current[0])) return undefined; return new ClientParty_1.default(this, party.current[0]); } /** * Fetches a party by its id * @param id The party's id * @param raw Whether to return the raw party data * @throws {PartyNotFoundError} The party wasn't found * @throws {PartyPermissionError} The party cannot be fetched due to a permission error * @throws {EpicgamesAPIError} */ async getParty(id, raw = false) { let party; try { party = await this.http.epicgamesRequest({ method: 'GET', url: `${Endpoints_1.default.BR_PARTY}/parties/${id}`, }, enums_1.AuthSessionStoreKey.Fortnite); } catch (e) { if (e instanceof EpicgamesAPIError_1.default) { if (e.code === 'errors.com.epicgames.social.party.party_not_found') { throw new PartyNotFoundError_1.default(); } if (e.code === 'errors.com.epicgames.social.party.party_query_forbidden') { throw new PartyPermissionError_1.default(); } } throw e; } if (raw) return party; const constuctedParty = new Party_1.default(this, party); await constuctedParty.updateMemberBasicInfo(); return constuctedParty; } /* -------------------------------------------------------------------------- */ /* FORTNITE */ /* -------------------------------------------------------------------------- */ /** * Fetches the current Fortnite server status (lightswitch) * @throws {EpicgamesAPIError} */ async getFortniteServerStatus() { const fortniteServerStatus = await this.http.epicgamesRequest({ method: 'GET', url: Endpoints_1.default.BR_SERVER_STATUS, }, enums_1.AuthSessionStoreKey.Fortnite); return new FortniteServerStatus_1.default(this, fortniteServerStatus[0]); } /** * Fetches the current epicgames server status (https://status.epicgames.com/) * @throws {AxiosError} */ async getEpicgamesServerStatus() { const epicgamesServerStatus = await this.http.request({ method: 'GET', url: Endpoints_1.default.SERVER_STATUS_SUMMARY, }); if (!epicgamesServerStatus) { throw new Error('Request returned an empty body'); } return new EpicgamesServerStatus_1.default(this, epicgamesServerStatus); } /** * Fetches the current Fortnite storefronts * @param language The language * @throws {EpicgamesAPIError} */ async getStorefronts(language = 'en') { const store = await this.http.epicgamesRequest({ method: 'GET', url: `${Endpoints_1.default.BR_STORE}?lang=${language}`, }, enums_1.AuthSessionStoreKey.Fortnite); return store.storefronts; } /** * Downloads a blurl stream (eg a radio station stream or a news video) * @param id The stream ID * @throws {AxiosError} */ async downloadBlurlStream(id) { const blurlFile = await this.http.request({ method: 'GET', url: `${Endpoints_1.default.BR_STREAM}/${id}/master.blurl`, responseType: 'arraybuffer', }); const streamData = await (0, Util_1.parseBlurlStream)(blurlFile); const streamMetaData = { subtitles: streamData.subtitles ? JSON.parse(streamData.subtitles) : {}, ucp: streamData.ucp, audioonly: !!streamData.audioonly, aspectratio: streamData.aspectratio, partysync: !!streamData.partysync, lrcs: streamData.lrcs ? JSON.parse(streamData.lrcs) : {}, duration: streamData.duration, }; const languageStreams = streamData.playlists.filter((p) => p.type === 'master').map((s) => { var _a, _b; let baseURL = (_a = s.url.match(/.+\//)) === null || _a === void 0 ? void 0 : _a[0]; if (baseURL && !baseURL.endsWith('/')) baseURL += '/'; const data = (0, Util_1.parseM3U8File)(s.data); let variants = data.streams.map((ss) => { var _a, _b; return ({ data: { codecs: ((_a = ss.data.CODECS) === null || _a === void 0 ? void 0 : _a.split(',')) || [], bandwidth: parseInt(ss.data.BANDWIDTH, 10), resolution: ss.data.RESOLUTION, }, type: ss.data.RESOLUTION ? 'video' : 'audio', url: `${baseURL || ''}${ss.url}`, stream: (_b = streamData.playlists .find((p) => p.type === 'variant' && p.rel_url === ss.url)) === null || _b === void 0 ? void 0 : _b.data.split(/\n/).map((l) => (!l.startsWith('#') && l.length > 0 ? `${baseURL || ''}${l}` : l)).join('\n').replace(/init_/g, `${baseURL || ''}init_`), }); }); if (!streamMetaData.audioonly) { const audioStreamUrl = (_b = variants.find((v) => v.type === 'audio')) === null || _b === void 0 ? void 0 : _b.url; if (audioStreamUrl) { variants = variants.map((v) => ({ ...v, stream: Buffer.from(v.type !== 'video' ? v.stream : v.stream.replace('#EXTINF:', '#EXT-X-STREAM-INF:AUDIO="group_audio"\n' + `#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="group_audio",NAME="audio",DEFAULT=YES,URI="${audioStreamUrl}"\n#EXTINF:`), 'utf8'), })); } } return { language: s.language, url: s.url, variants, }; }); return { languages: languageStreams, data: streamMetaData, }; } /** * Fetches Battle Royale v2 stats for one or multiple players * @param user The id(s) or display name(s) of the user(s) * @param startTime The timestamp in seconds to start fetching stats from, can be null/undefined for lifetime * @param endTime The timestamp in seconds to stop fetching stats from, can be undefined for lifetime * @param stats An array of stats keys. Required if you want to get the stats of multiple users at once (If not, ignore this) * @throws {UserNotFoundError} The user wasn't found * @throws {StatsPrivacyError} The user set their stats to private * @throws {TypeError} You must provide an array of stats keys for multiple user lookup * @throws {EpicgamesAPIError} */ async getBRStats(user, startTime, endTime, stats = []) { const params = []; if (startTime) params.push(`startTime=${startTime}`); if (endTime) params.push(`endTime=${endTime}`); const query = params[0] ? `?${params.join('&')}` : ''; if (typeof user === 'string') { const resolvedUser = await this.user.fetch(user); if (!resolvedUser) throw new UserNotFoundError_1.default(user); let statsResponse; try { statsResponse = await this.http.epicgamesRequest({ method: 'GET', url: `${Endpoints_1.default.BR_STATS_V2}/account/${resolvedUser.id}${query}`, }, enums_1.AuthSessionStoreKey.Fortnite); } catch (e) { if (e instanceof EpicgamesAPIError_1.default) { throw new StatsPrivacyError_1.default(user); } throw e; } return new Stats_1.default(this, statsResponse, resolvedUser); } if (!stats[0]) { throw new TypeError('You need to provide an array of stats keys to fetch multiple user\'s stats'); } const resolvedUsers = await this.user.fetchMultiple(user); const idChunks = resolvedUsers .map((u) => u.id) .reduce((resArr, id, i) => { const chunkIndex = Math.floor(i / 51); // eslint-disable-next-line no-param-reassign if (!resArr[chunkIndex]) resArr[chunkIndex] = []; resArr[chunkIndex].push(id); return resArr; }, []); const statsResponses = await Promise.all(idChunks.map((c) => this.http.epicgamesRequest({ method: 'POST', url: `${Endpoints_1.default.BR_STATS_V2}/query${query}`, headers: { 'Content-Type': 'application/json', }, data: { appId: 'fortnite', owners: c, stats, }, }, enums_1.AuthSessionStoreKey.Fortnite))); return statsResponses .flat(1) .map((r) => new Stats_1.default(this, r, resolvedUsers.find((u) => u.id === r.accountId))); } /** * Fetches the current Battle Royale news * @param language The language of the news * @param customPayload Extra data to send in the request body for a personalized news response (battle pass level, country, etc) * @throws {EpicgamesAPIError} */ async getBRNews(language = Enums_1.default.Language.ENGLISH, customPayload) { const news = await this.http.epicgamesRequest({ method: 'POST', url: Endpoints_1.default.BR_NEWS, headers: { 'Content-Type': 'application/json', 'Accept-Language': language, }, data: { platform: 'Windows', language, country: 'US', serverRegion: 'NA', subscription: false, battlepass: false, battlepassLevel: 1, ...customPayload, }, }, enums_1.AuthSessionStoreKey.Fortnite); return news.contentItems.map((i) => new NewsMessage_1.default(this, i)); } /** * Fetches data for a Support-A-Creator code * @param code The Support-A-Creator code (slug) * @throws {CreatorCodeNotFoundError} The Support-A-Creator code wasnt found * @throws {EpicgamesAPIError} */ async getCreatorCode(code) { let codeResponse; try { codeResponse = await this.http.epicgamesRequest({ method: 'GET', url: `${Endpoints_1.default.BR_SAC}/${encodeURIComponent(code)}`, }, enums_1.AuthSessionStoreKey.FortniteClientCredentials); } catch (e) { if (e instanceof EpicgamesAPIError_1.default && e.code === 'errors.com.epicgames.ecommerce.affiliate.not_found') { throw new CreatorCodeNotFoundError_1.default(code); } throw e; } const owner = await this.user.fetch(codeResponse.id); return new CreatorCode_1.default(this, { ...codeResponse, owner }); } /** * Fetches the current Fortnite Battle Royale radio stations * @throws {EpicgamesAPIError} */ async getRadioStations() { const fortniteContent = await this.http.epicgamesRequest({ method: 'GET', url: Endpoints_1.default.BR_NEWS, }); const radioStations = fortniteContent.radioStations.radioStationList.stations; return radioStations.map((s) => new RadioStation_1.default(this, s)); } /** * Fetches the current Battle Royale event flags * @param language The language * @throws {EpicgamesAPIError} */ async getBREventFlags(language = 'en') { const eventFlags = await this.http.epicgamesRequest({ method: 'GET', url: `${Endpoints_1.default.BR_EVENT_FLAGS}?lang=${language}`, }, enums_1.AuthSessionStoreKey.Fortnite); return eventFlags; } /** * Fetches the Battle Royale account level for one or multiple users * @param user The id(s) and/or display name(s) of the user(s) to fetch the account level for * @param seasonNumber The season number (eg. 16, 17, 18) * @throws {UserNotFoundError} The user wasn't found * @throws {StatsPrivacyError} The user set their stats to private * @throws {EpicgamesAPIError} */ async getBRAccountLevel(user, seasonNumber) { const users = Array.isArray(user) ? user : [user]; const accountLevels = await this.getBRStats(users, undefined, undefined, [`s${seasonNumber}_social_bp_level`]); return accountLevels.map((al) => ({ user: al.user, level: al.levelData[`s${seasonNumber}`] || { level: 0, progress: 0 }, })); } /** * Fetches the storefront keychain * @throws {EpicgamesAPIError} */ async getStorefrontKeychain() { const keychain = await this.http.epicgamesRequest({ method: 'GET', url: Endpoints_1.default.BR_STORE_KEYCHAIN, }, enums_1.AuthSessionStoreKey.Fortnite); return keychain; } /* -------------------------------------------------------------------------- */ /* FORTNITE CREATIVE */ /* -------------------------------------------------------------------------- */ /** * Fetches a creative island by its code * @param code The island code * @throws {CreativeIslandNotFoundError} A creative island with the provided code does not exist * @throws {EpicgamesAPIError} */ async getCreativeIsland(code) { let islandInfo; try { islandInfo = await this.http.epicgamesRequest({ method: 'GET', url: `${Endpoints_1.default.CREATIVE_ISLAND_LOOKUP}/${code}`, }, enums_1.AuthSessionStoreKey.Fortnite); } catch (e) { if (e instanceof EpicgamesAPIError_1.default && e.code === 'errors.com.epicgames.links.no_active_version') { throw new CreativeIslandNotFoundError_1.default(code); } throw e; } return islandInfo; } /** * Fetches the creative discovery surface * @param gameVersion The current game version (MAJOR.MINOR) * @throws {EpicgamesAPIError} */ // kept for backwards compatibility // eslint-disable-next-line @typescript-eslint/default-param-last async getCreativeDiscoveryPanels(gameVersion = '19.40', region) { const creativeDiscovery = await this.http.epicgamesRequest({ method: 'POST', url: `${Endpoints_1.default.CREATIVE_DISCOVERY}/${this.user.self.id}?appId=Fortnite`, headers: { 'Content-Type': 'application/json', 'User-Agent': `Fortnite/++Fortnite+Release-${gameVersion}-CL-00000000 Windows/10.0.19044.1.768.64bit`, }, data: { surfaceName: 'CreativeDiscoverySurface_Frontend', revision: -1, partyMemberIds: [this.user.self.id], matchmakingRegion: region, }, }, enums_1.AuthSessionStoreKey.Fortnite); return creativeDiscovery; } } exports.default = Client; //# sourceMappingURL=Client.js.map