@fnlb-project/fnbr
Version:
A library to interact with Epic Games' Fortnite HTTP and XMPP services
291 lines • 16 kB
JavaScript
"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