fnbr
Version:
A library to interact with Epic Games' Fortnite HTTP and XMPP services
1,043 lines • 44.4 kB
JavaScript
"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