UNPKG

@fnlb-project/fnbr

Version:

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

291 lines 16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const WebSocket_1 = tslib_1.__importDefault(require("../util/WebSocket")); const Base_1 = tslib_1.__importDefault(require("../Base")); const enums_1 = require("../../resources/enums"); const AuthenticationMissingError_1 = tslib_1.__importDefault(require("../exceptions/AuthenticationMissingError")); const Endpoints_1 = tslib_1.__importDefault(require("../../resources/Endpoints")); const ReceivedFriendMessage_1 = tslib_1.__importDefault(require("../structures/friend/ReceivedFriendMessage")); const PartyMessage_1 = tslib_1.__importDefault(require("../structures/party/PartyMessage")); const FriendPresence_1 = tslib_1.__importDefault(require("../structures/friend/FriendPresence")); const PresenceParty_1 = tslib_1.__importDefault(require("../structures/party/PresenceParty")); const STOMPConnectionTimeoutError_1 = tslib_1.__importDefault(require("../exceptions/STOMPConnectionTimeoutError")); const STOMPMessage_1 = tslib_1.__importDefault(require("./STOMPMessage")); const STOMPConnectionError_1 = tslib_1.__importDefault(require("../exceptions/STOMPConnectionError")); const Util_1 = require("../util/Util"); /** * Represents the client's EOS Connect STOMP manager (i.e. chat messages) */ class STOMP extends Base_1.default { /** * @param client The main client */ constructor(client) { super(client); this.connection = undefined; this.pingInterval = undefined; this.connectionId = undefined; this.connectionRetryCount = 0; } /** * Whether the internal websocket is connected */ get isConnected() { return this.connection && this.connection.readyState === WebSocket_1.default.OPEN; } /** * Connect to the STOMP server * @throws {AuthenticationMissingError} When there is no EOS auth to use for STOMP auth * @throws {STOMPConnectionError} When the connection failed for any reason */ async connect() { if (!this.client.auth.sessions.has(enums_1.AuthSessionStoreKey.FortniteEOS)) { throw new AuthenticationMissingError_1.default(enums_1.AuthSessionStoreKey.FortniteEOS); } this.client.debug('[STOMP] Connecting...'); const connectionStartTime = Date.now(); const stopmHeaders = { Authorization: `Bearer ${this.client.auth.sessions.get(enums_1.AuthSessionStoreKey.FortniteEOS).accessToken}`, 'Sec-Websocket-Protocol': 'v10.stomp,v11.stomp,v12.stomp', 'Epic-Connect-Device-Id': ' ', 'Epic-Connect-Protocol': 'stomp', }; const url = this.client.config.wsTransformURL ? this.client.config.wsTransformURL(`wss://${Endpoints_1.default.EOS_STOMP}`, stopmHeaders) : `wss://${Endpoints_1.default.EOS_STOMP}`; this.connection = new WebSocket_1.default(url, { headers: stopmHeaders, }); return new Promise((res, rej) => { const connectionTimeout = setTimeout(() => { this.disconnect(); rej(new STOMPConnectionTimeoutError_1.default(this.client.config.stompConnectionTimeout)); }, this.client.config.stompConnectionTimeout); this.connection.once('open', () => { clearTimeout(connectionTimeout); this.sendMessage({ command: 'CONNECT', headers: { 'accept-version': '1.0,1.1,1.2', 'heart-beat': '35000,0', }, }); this.registerEvents(res, rej, connectionStartTime); }); this.connection.once('error', (err) => { this.client.debug(`[STOMP] Connection failed: ${err.message}`); clearTimeout(connectionTimeout); rej(new STOMPConnectionError_1.default(err.message)); }); }); } /** * Registers the events for the STOMP connection */ registerEvents(resolve, reject, connectionStartTime) { this.connection.on('close', async (code, reason) => { this.disconnect(); if (this.connectionRetryCount < 2) { this.client.debug('[STOMP] Disconnected, reconnecting in 5 seconds...'); this.connectionRetryCount += 1; await new Promise((res) => setTimeout(res, 5000)); await this.connect(); } else { this.client.debug('[STOMP] Disconnected, retry limit reached'); this.connectionRetryCount = 0; throw new STOMPConnectionError_1.default(`STOMP WS disconnected, retry limit reached. Reason: ${reason}`, code); } }); this.connection.on('message', async (d) => { var _a, _b, _c, _d, _e, _f, _g; let content; if (typeof Blob !== 'undefined' && d instanceof Blob) { content = await d.text(); } else if (typeof ArrayBuffer !== 'undefined' && (d instanceof ArrayBuffer || ArrayBuffer.isView(d))) { content = new TextDecoder().decode(d); } else { content = d.toString(); } const message = STOMPMessage_1.default.fromString(content); switch (message.command) { case 'CONNECTED': this.pingInterval = setInterval(() => { if (this.connection && this.connection.readyState === WebSocket_1.default.OPEN) { this.connection.send('\n'); } }, 35000); this.sendMessage({ command: 'SUBSCRIBE', headers: { id: 'sub-0', destination: `${this.client.config.eosDeploymentId}/account/${this.client.user.self.id}`, }, }); break; case 'MESSAGE': { if (!message.body) break; const data = JSON.parse(message.body); switch (data.type) { case 'core.connect.v1.connected': this.client.debug(`[STOMP] Successfully connected (${((Date.now() - connectionStartTime) / 1000).toFixed(2)}s)`); this.connectionId = data.connectionId; this.connectionRetryCount = 0; this.client.setStatus(); resolve(); break; case 'core.connect.v1.connect-failed': this.client.debug(`[STOMP] Connection failed: ${data.statusCode} - ${data.message}`); reject(new STOMPConnectionError_1.default(data.message, data.statusCode)); break; case 'social.chat.v1.NEW_WHISPER': { const { senderId, body, time } = data.payload.message; const friend = this.client.friend.list.get(senderId); if (!friend || senderId === this.client.user.self.id) return; const friendMessage = new ReceivedFriendMessage_1.default(this.client, { content: (0, Util_1.decodeSTOMPMessageBody)(body), author: friend, id: data.id, sentAt: new Date(time), }); this.client.emit('friend:message', friendMessage); break; } case 'social.chat.v1.NEW_MESSAGE': { if (data.payload.conversation.type !== 'party') return; await this.client.partyLock.wait(); const { conversation: { conversationId }, message: { senderId, body, time } } = data.payload; const partyId = conversationId.replace('p-', ''); if (!this.client.party || this.client.party.id !== partyId || senderId === this.client.user.self.id) { return; } const authorMember = this.client.party.members.get(senderId); if (!authorMember) return; const partyMessage = new PartyMessage_1.default(this.client, { content: (0, Util_1.decodeSTOMPMessageBody)(body), author: authorMember, sentAt: new Date(time), id: data.id, party: this.client.party, }); this.client.emit('party:member:message', partyMessage); break; } case 'presence.v1.UPDATE': { const friendId = data.payload.accountId; let friend = this.client.friend.list.get(friendId); if (!friend) { try { const result = await this.client.waitForEvent('friend:added', 1000, (f) => f.id === friendId); friend = result[0]; } catch { return; } } if (!friend) return; if (data.payload.status === 'offline') { friend.lastAvailableTimestamp = undefined; friend.party = undefined; this.client.emit('friend:offline', friend); break; } const wasUnavailable = !friend.lastAvailableTimestamp; friend.lastAvailableTimestamp = Date.now(); const perNs = ((_a = data.payload.perNs) === null || _a === void 0 ? void 0 : _a[0]) || {}; const props = data.payload.props || {}; const parsedProps = {}; for (const [key, value] of Object.entries(props)) { if (typeof value === 'string' && !key.startsWith('EOS_')) { parsedProps[key] = value.slice(1); try { parsedProps[key] = JSON.parse(parsedProps[key]); } catch { } } else { parsedProps[key] = value; } } const mappedData = { Status: perNs.status || '', bIsPlaying: false, bIsJoinable: false, bHasVoiceSupport: false, SessionId: parsedProps.SessionIdAttributeKey || '', Properties: { FortBasicInfo_j: parsedProps.FortBasicInfo, FortSubGame_i: parsedProps.FortSubGame, InUnjoinableMatch_b: parsedProps.InUnjoinableMatch === 'true', GamePlaylistName_s: parsedProps.GamePlaylistName || parsedProps.IslandCode, Event_PartySize_s: (_b = parsedProps.Event_PartySize) === null || _b === void 0 ? void 0 : _b.toString(), Event_PartyMaxSize_s: (_c = parsedProps.Event_PartyMaxSize) === null || _c === void 0 ? void 0 : _c.toString(), ServerPlayerCount_i: (_d = parsedProps.ServerPlayerCount) === null || _d === void 0 ? void 0 : _d.toString(), FortGameplayStats_j: parsedProps.FortGameplayStats, 'party.joininfodata.286331153_j': parsedProps['party.joininfodata.286331153'] } }; const show = data.payload.status === 'away' ? 'away' : 'online'; const from = `fake_jid/${parsedProps.EOS_Platform || 'WIN'}::fake`; const before = friend.presence; const after = new FriendPresence_1.default(this.client, mappedData, friend, show, from); if ((((_e = this.client.config.cacheSettings.presences) === null || _e === void 0 ? void 0 : _e.maxLifetime) || 0) > 0) { friend.presence = after; } if ((_f = mappedData.Properties) === null || _f === void 0 ? void 0 : _f['party.joininfodata.286331153_j']) { friend.party = new PresenceParty_1.default(this.client, mappedData.Properties['party.joininfodata.286331153_j']); } else { friend.party = undefined; } if (wasUnavailable) { this.client.emit('friend:online', friend); } this.client.emit('friend:presence', before, after); break; } default: this.client.debug(`[STOMP] Unknown message type: ${data.type} ${message.body}`); } } break; default: this.client.debug(`[STOMP] Unknown command: ${message.command} ${(_g = message.body) !== null && _g !== void 0 ? _g : 'no body'}`); } }); } /** * Disconnects the STOMP client. * Also performs a cleanup */ async disconnect() { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = undefined; } if (this.connection) { this.connection.removeAllListeners(); if (this.connection.readyState === WebSocket_1.default.OPEN) { this.connection.close(); } this.connection = undefined; } this.connectionId = undefined; } /** * Sends a message to the STOMP server * @param message The message to send */ sendMessage(message) { this.connection.send(new STOMPMessage_1.default(message).toString()); } } exports.default = STOMP; //# sourceMappingURL=STOMP.js.map