From e6f7430c610ad94b82c0868f56e24edebe1743a8 Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Tue, 23 Jul 2024 22:25:28 +0200 Subject: [PATCH 1/5] (feat): initial test for eos connect stomp --- docs/examples/simple.js | 2 +- package-lock.json | 93 ++++++++++++++++++-- package.json | 4 +- resources/Endpoints.ts | 3 + resources/structs.ts | 23 ++++- src/Client.ts | 15 +++- src/structures/eos/connect.ts | 78 +++++++++++++++++ src/xmpp/EOSConnect.ts | 157 ++++++++++++++++++++++++++++++++++ 8 files changed, 361 insertions(+), 14 deletions(-) create mode 100644 src/structures/eos/connect.ts create mode 100644 src/xmpp/EOSConnect.ts diff --git a/docs/examples/simple.js b/docs/examples/simple.js index 50d296d7..ccf49fc7 100644 --- a/docs/examples/simple.js +++ b/docs/examples/simple.js @@ -1,5 +1,5 @@ /* eslint-disable */ -const { Client } = require('fnbr'); +const { Client } = require('../../'); const client = new Client(); diff --git a/package-lock.json b/package-lock.json index 1f1302af..6e6fd888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "dependencies": { "@discordjs/collection": "^2.0.0", "@sapphire/async-queue": "^1.5.0", + "@stomp/stompjs": "^7.0.0", "axios": "^1.3.5", "stanza": "^12.18.0", - "tslib": "^2.5.0" + "tslib": "^2.5.0", + "ws": "^8.18.0" }, "devDependencies": { "@types/jest": "^29.5.0", @@ -1264,6 +1266,11 @@ "@sinonjs/commons": "^2.0.0" } }, + "node_modules/@stomp/stompjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz", + "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==" + }, "node_modules/@types/async": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.12.tgz", @@ -1969,6 +1976,20 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -4607,6 +4628,18 @@ } } }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "optional": true, + "peer": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5655,6 +5688,20 @@ "punycode": "^2.1.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -5795,9 +5842,9 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, @@ -6825,6 +6872,11 @@ "@sinonjs/commons": "^2.0.0" } }, + "@stomp/stompjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.0.0.tgz", + "integrity": "sha512-fGdq4wPDnSV/KyOsjq4P+zLc8MFWC3lMmP5FBgLWKPJTYcuCbAIrnRGjB7q2jHZdYCOD5vxLuFoKIYLy5/u8Pw==" + }, "@types/async": { "version": "3.2.12", "resolved": "https://registry.npmjs.org/@types/async/-/async-3.2.12.tgz", @@ -7340,6 +7392,16 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "optional": true, + "peer": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -9290,6 +9352,13 @@ "whatwg-url": "^5.0.0" } }, + "node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "optional": true, + "peer": true + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10038,6 +10107,16 @@ "punycode": "^2.1.0" } }, + "utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "optional": true, + "peer": true, + "requires": { + "node-gyp-build": "^4.3.0" + } + }, "v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -10150,9 +10229,9 @@ } }, "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "requires": {} }, "y18n": { diff --git a/package.json b/package.json index 271102de..8e1ec383 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,11 @@ "dependencies": { "@discordjs/collection": "^2.0.0", "@sapphire/async-queue": "^1.5.0", + "@stomp/stompjs": "^7.0.0", "axios": "^1.3.5", "stanza": "^12.18.0", - "tslib": "^2.5.0" + "tslib": "^2.5.0", + "ws": "^8.18.0" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/resources/Endpoints.ts b/resources/Endpoints.ts index ea92639e..19995564 100644 --- a/resources/Endpoints.ts +++ b/resources/Endpoints.ts @@ -20,6 +20,9 @@ export default Object.freeze({ XMPP_SERVER: 'xmpp-service-prod.ol.epicgames.com', EPIC_PROD_ENV: 'prod.ol.epicgames.com', + // EOS CONNECT STOMP + STOMP_EOS_CONNECT_SERVER: 'connect.epicgames.dev', + // BATTLE ROYALE BR_STATS_V2: 'https://statsproxy-public-service-live.ol.epicgames.com/statsproxy/api/statsv2', BR_SERVER_STATUS: 'https://lightswitch-public-service-prod06.ol.epicgames.com/lightswitch/api/service/bulk/status?serviceId=Fortnite', diff --git a/resources/structs.ts b/resources/structs.ts index 1211ff2b..9394f6ae 100644 --- a/resources/structs.ts +++ b/resources/structs.ts @@ -42,7 +42,7 @@ export type PartySchema = Partial & { export type Schema = Record; export type Language = 'de' | 'ru' | 'ko' | 'zh-hant' | 'pt-br' | 'en' -| 'it' | 'fr' | 'zh-cn' | 'es' | 'ar' | 'ja' | 'pl' | 'es-419' | 'tr'; + | 'it' | 'fr' | 'zh-cn' | 'es' | 'ar' | 'ja' | 'pl' | 'es-419' | 'tr'; export type StringFunction = () => string; @@ -88,7 +88,7 @@ export type AuthStringResolveable = string | PathLike | StringFunction | StringF export type Platform = 'WIN' | 'MAC' | 'PSN' | 'XBL' | 'SWT' | 'IOS' | 'AND' | 'PS5' | 'XSX'; export type AuthClient = 'fortnitePCGameClient' | 'fortniteIOSGameClient' | 'fortniteAndroidGameClient' -| 'fortniteSwitchGameClient' | 'fortniteCNGameClient' | 'launcherAppClient2' | 'Diesel - Dauntless'; + | 'fortniteSwitchGameClient' | 'fortniteCNGameClient' | 'launcherAppClient2' | 'Diesel - Dauntless'; export interface RefreshTokenData { /** @@ -257,6 +257,11 @@ export interface ClientConfig { */ xmppDebug?: (message: string) => void; + /** + * Debug function used for incoming and outgoing eos connect messages + */ + eosConnectDebug?: (message: string) => void; + /** * Default friend presence of the bot (eg. "Playing Battle Royale") */ @@ -325,6 +330,11 @@ export interface ClientConfig { */ connectToXMPP: boolean; + /** + * WIP - disable if you dont care about dms or group messages + */ + connectToEOSConnect: boolean; + /** * Whether the client should fetch all friends on startup. * NOTE: If you disable this, almost all features related to friend caching will no longer work. @@ -376,6 +386,11 @@ export interface ClientConfig { * Can be useful if you want to use data in the game files to determine the stats playlist type */ statsPlaylistTypeParser?: (playlistId: string) => StatsPlaylistType; + + /** + * fortnite deployment id (eos) + */ + eosDeploymentId: string; } export interface MatchMeta { @@ -386,7 +401,7 @@ export interface MatchMeta { matchStartedAt?: Date; } -export interface ClientOptions extends Partial {} +export interface ClientOptions extends Partial { } export interface ClientEvents { /** @@ -1170,7 +1185,7 @@ export interface ReplayDownloadConfig { addStatsPlaceholder: boolean; } -export interface ReplayDownloadOptions extends Partial {} +export interface ReplayDownloadOptions extends Partial { } export interface EventTokensResponse { user: User; diff --git a/src/Client.ts b/src/Client.ts index b5aa2d76..fe63200b 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -34,6 +34,7 @@ import EpicgamesAPIError from './exceptions/EpicgamesAPIError'; import UserManager from './managers/UserManager'; import FriendManager from './managers/FriendManager'; import STWManager from './managers/STWManager'; +import EOSConnect from './xmpp/EOSConnect'; import type { PresenceShow } from 'stanza/Constants'; import type { BlurlStreamData, CreativeIslandData, @@ -101,6 +102,11 @@ class Client extends EventEmitter { */ public xmpp: XMPP; + /** + * EOS Connect STOMP manager + */ + public eosConnect: EOSConnect; + /** * Friend manager */ @@ -148,6 +154,7 @@ class Client extends EventEmitter { forceNewParty: true, disablePartyService: false, connectToXMPP: true, + connectToEOSConnect: true, fetchFriends: true, restRetryLimit: 1, handleRatelimits: true, @@ -156,6 +163,7 @@ class Client extends EventEmitter { language: 'en', friendOnlineConnectionTimeout: 30000, friendOfflineTimeout: 300000, + eosDeploymentId: '62a9473a2dca46b29ccf17577fcf42d7', ...config, cacheSettings: { ...config.cacheSettings, @@ -195,6 +203,7 @@ class Client extends EventEmitter { this.auth = new Auth(this); this.http = new Http(this); this.xmpp = new XMPP(this); + this.eosConnect = new EOSConnect(this); this.partyLock = new AsyncLock(); this.cacheLock = new AsyncLock(); @@ -244,6 +253,7 @@ class Client extends EventEmitter { this.cacheLock.lock(); try { if (this.config.connectToXMPP) await this.xmpp.connect(); + if (this.config.connectToEOSConnect) await this.eosConnect.connect(); if (this.config.fetchFriends) await this.updateCaches(); } finally { this.cacheLock.unlock(); @@ -534,7 +544,7 @@ class Client extends EventEmitter { * @param message Text to debug * @param type Debug type (regular, http or xmpp) */ - public debug(message: string, type: 'regular' | 'http' | 'xmpp' = 'regular') { + public debug(message: string, type: 'regular' | 'http' | 'xmpp' | 'eos-connect' = 'regular') { switch (type) { case 'regular': if (typeof this.config.debug === 'function') this.config.debug(message); @@ -545,6 +555,9 @@ class Client extends EventEmitter { case 'xmpp': if (typeof this.config.xmppDebug === 'function') { this.config.xmppDebug(message); } break; + case 'eos-connect': + if (typeof this.config.eosConnectDebug === 'function') { this.config.eosConnectDebug(message); } + break; } } diff --git a/src/structures/eos/connect.ts b/src/structures/eos/connect.ts new file mode 100644 index 00000000..b04aef2a --- /dev/null +++ b/src/structures/eos/connect.ts @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/indent */ +interface BaseEOSConnectMessage { + correlationId: string; + timestamp: number; // unix + id?: string; + connectionId?: string; +} + +export interface EOSConnectCoreConnected extends BaseEOSConnectMessage { + type: 'core.connect.v1.connected'; +} + +export interface EOSConnectCoreConnectFailed extends BaseEOSConnectMessage { + message: string; + statusCode: number; // i.e. 4005 + type: 'core.connect.v1.connect-failed'; +} + +export interface EOSConnectChatMemberLeftMessage extends BaseEOSConnectMessage { + payload: { + // deployment id + namespace: string; + conversationId: string; + members: string[]; + }; + type: 'social.chat.v1.MEMBERS_LEFT'; +} + +export interface EOSConnectChatNewMsgMessage extends BaseEOSConnectMessage { + payload: { + // deployment id + namespace: string; + conversation: { + conversationId: string; + type: string; // i.e. 'party + }; + message: { + body: string; + senderId: string; + time: number; + } + }; + type: 'social.chat.v1.NEW_MESSAGE'; +} + +export interface EOSConnectChatConversionCreatedMessage extends BaseEOSConnectMessage { + payload: { + // deployment id + namespace: string; + conversationId: string; + type: string; // i.e. 'party' + members: string[]; + }; + type: 'social.chat.v1.CONVERSATION_CREATED'; +} + +export interface EOSConnectChatNewWhisperMessage extends BaseEOSConnectMessage { + payload: { + // deployment id + namespace: string; + message: { + body: string; + senderId: string; + time: number; + } + }; + type: 'social.chat.v1.NEW_WHISPER'; +} + +export type EOSConnectMessage = + // Core + EOSConnectCoreConnected + | EOSConnectCoreConnectFailed + // Social chat + | EOSConnectChatConversionCreatedMessage + | EOSConnectChatNewMsgMessage + | EOSConnectChatMemberLeftMessage + | EOSConnectChatNewWhisperMessage; diff --git a/src/xmpp/EOSConnect.ts b/src/xmpp/EOSConnect.ts new file mode 100644 index 00000000..25b4e9c7 --- /dev/null +++ b/src/xmpp/EOSConnect.ts @@ -0,0 +1,157 @@ +import { Client as StompClient, Versions } from '@stomp/stompjs'; +import WebSocket from 'ws'; +import Base from '../Base'; +import { AuthSessionStoreKey } from '../../resources/enums'; +import AuthenticationMissingError from '../exceptions/AuthenticationMissingError'; +import Endpoints from '../../resources/Endpoints'; +import AuthClients from '../../resources/AuthClients'; +import ReceivedFriendMessage from '../structures/friend/ReceivedFriendMessage'; +import type { EOSConnectMessage } from '../structures/eos/connect'; +import type { IMessage } from '@stomp/stompjs'; +import type Client from '../Client'; +import type Friend from '../structures/friend/Friend'; + +/** + * Represents the client's EOS Connect STOMP manager (i.e. chat messages) + */ +class EOSConnect extends Base { + private wsConnection?: StompClient; + + /** + * @param client The main client + */ + constructor(client: Client) { + super(client); + + this.wsConnection = undefined; + } + + public async connect() { + if (!this.client.auth.sessions.has(AuthSessionStoreKey.Fortnite)) { + throw new AuthenticationMissingError(AuthSessionStoreKey.Fortnite); + } + + // own func + const { code: exchangeCode } = await this.client.http.epicgamesRequest({ + url: Endpoints.OAUTH_EXCHANGE, + headers: { + Authorization: `bearer ${this.client.auth.sessions.get(AuthSessionStoreKey.Fortnite)!.accessToken}`, + }, + }); + + // own func - maybe actual auth like others? + const epicIdAuth = await this.client.http.epicgamesRequest<{ access_token: string }>({ + method: 'POST', + url: 'https://api.epicgames.dev/epic/oauth/v2/token', + headers: { + Authorization: + `basic ${Buffer.from(`${AuthClients.fortniteIOSGameClient.clientId}:${AuthClients.fortniteIOSGameClient.secret}`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: new URLSearchParams({ + grant_type: 'exchange_code', + exchange_code: exchangeCode, + token_type: 'epic_id', + deployment_id: this.client.config.eosDeploymentId, + }).toString(), + }); + + console.log(epicIdAuth); + + const brokerURL = `wss://${Endpoints.STOMP_EOS_CONNECT_SERVER}`; + + this.wsConnection = new StompClient({ + brokerURL, + stompVersions: new Versions([Versions.V1_0, Versions.V1_1, Versions.V1_2]), + heartbeatOutgoing: 30_000, + heartbeatIncoming: 0, + webSocketFactory: () => new WebSocket( + brokerURL, + { + headers: { + Authorization: `Bearer ${epicIdAuth.access_token}`, + 'Epic-Connect-Protocol': 'stomp', + 'Epic-Connect-Device-Id': '', + }, + }, + ), + debug: (str) => { + console.log(`STOMP: ${str}`); + }, + onConnect: (idk) => { + console.log(idk); + + this.setupSubscription(); + }, + onStompError(frame) { + console.log(frame.command); + console.log(frame.headers); + console.log(frame.body); + }, + onChangeState(state) { + console.log('state', state); + }, + onDisconnect(frame) { + console.log(frame); + }, + onUnhandledFrame(frame) { + console.log('onUnhandledFrame', frame.command, frame.body); + }, + onUnhandledReceipt(frame) { + console.log(frame); + }, + onWebSocketError: (evt) => { + console.log(evt); + }, + onUnhandledMessage: (msg) => { + console.log(msg.command, msg.command); + }, + }); + + console.log(this.wsConnection); + + this.wsConnection.activate(); + } + + private setupSubscription() { + this.wsConnection!.subscribe( + `${this.client.config.eosDeploymentId}/account/${this.client.user.self!.id}`, + this.subscriptionCallback, + { + id: 'sub-0', + }, + ); + } + + // eslint-disable-next-line class-methods-use-this + private async subscriptionCallback(message: IMessage) { + const isJson = message.headers['content-type']?.includes('application/json'); + + if (!isJson) { + return; + } + + const messageData = JSON.parse(message.body); + + console.log(messageData); + + this.client.debug(`new message '${messageData.type}' - ${message.body}`, 'eos-connect'); + + switch (messageData.type) { + case 'social.chat.v1.NEW_WHISPER': { + // const friend = await this.client.xmpp.waitForFriend(m.from.split('@')[0]); + // if (!friend) return; + + const friendMessage = new ReceivedFriendMessage(this.client, { + // TODO: FRIEND via xmpp??? + content: messageData.payload.message.body || '', author: {}, id: messageData.id!, sentAt: new Date(), + }); + + this.client.emit('friend:message', friendMessage); + break; + } + } + } +} + +export default EOSConnect; From ae0a810a3144ecbde6e0512c58439ccd6e88c3ff Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Tue, 23 Jul 2024 22:51:22 +0200 Subject: [PATCH 2/5] feat(eos-connect): handle dm & group messages --- docs/examples/simple.js | 4 ++ index.ts | 1 + src/structures/eos/connect.ts | 2 + src/xmpp/EOSConnect.ts | 73 ++++++++++++++++++++++++++++------- 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/docs/examples/simple.js b/docs/examples/simple.js index ccf49fc7..79f8bf21 100644 --- a/docs/examples/simple.js +++ b/docs/examples/simple.js @@ -10,6 +10,10 @@ client.on('friend:message', (message) => { } }); +client.on('party:member:message', (message) => { + console.log(`Party Message from ${message.author.displayName}: ${message.content}`); +}); + client.on('ready', () => { console.log(`Logged in as ${client.user.self.displayName}`); }); diff --git a/index.ts b/index.ts index 27489e50..0d701fc6 100644 --- a/index.ts +++ b/index.ts @@ -53,6 +53,7 @@ export { default as RadioStation } from './src/structures/RadioStation'; export { default as Stats } from './src/structures/Stats'; export { default as Tournament } from './src/structures/Tournament'; export { default as TournamentWindow } from './src/structures/TournamentWindow'; +export { default as connect } from './src/structures/eos/connect'; export { default as BaseFriendMessage } from './src/structures/friend/BaseFriendMessage'; export { default as BasePendingFriend } from './src/structures/friend/BasePendingFriend'; export { default as Friend } from './src/structures/friend/Friend'; diff --git a/src/structures/eos/connect.ts b/src/structures/eos/connect.ts index b04aef2a..b6e308d7 100644 --- a/src/structures/eos/connect.ts +++ b/src/structures/eos/connect.ts @@ -76,3 +76,5 @@ export type EOSConnectMessage = | EOSConnectChatNewMsgMessage | EOSConnectChatMemberLeftMessage | EOSConnectChatNewWhisperMessage; + +export default {}; diff --git a/src/xmpp/EOSConnect.ts b/src/xmpp/EOSConnect.ts index 25b4e9c7..3dfdf59c 100644 --- a/src/xmpp/EOSConnect.ts +++ b/src/xmpp/EOSConnect.ts @@ -6,16 +6,19 @@ import AuthenticationMissingError from '../exceptions/AuthenticationMissingError import Endpoints from '../../resources/Endpoints'; import AuthClients from '../../resources/AuthClients'; import ReceivedFriendMessage from '../structures/friend/ReceivedFriendMessage'; +import PartyMessage from '../structures/party/PartyMessage'; import type { EOSConnectMessage } from '../structures/eos/connect'; import type { IMessage } from '@stomp/stompjs'; import type Client from '../Client'; -import type Friend from '../structures/friend/Friend'; /** * Represents the client's EOS Connect STOMP manager (i.e. chat messages) */ class EOSConnect extends Base { - private wsConnection?: StompClient; + /** + * private stomp connection + */ + private stompConnection?: StompClient; /** * @param client The main client @@ -23,7 +26,7 @@ class EOSConnect extends Base { constructor(client: Client) { super(client); - this.wsConnection = undefined; + this.stompConnection = undefined; } public async connect() { @@ -60,7 +63,7 @@ class EOSConnect extends Base { const brokerURL = `wss://${Endpoints.STOMP_EOS_CONNECT_SERVER}`; - this.wsConnection = new StompClient({ + this.stompConnection = new StompClient({ brokerURL, stompVersions: new Versions([Versions.V1_0, Versions.V1_1, Versions.V1_2]), heartbeatOutgoing: 30_000, @@ -108,22 +111,19 @@ class EOSConnect extends Base { }, }); - console.log(this.wsConnection); - - this.wsConnection.activate(); + this.stompConnection.activate(); } private setupSubscription() { - this.wsConnection!.subscribe( + this.stompConnection!.subscribe( `${this.client.config.eosDeploymentId}/account/${this.client.user.self!.id}`, - this.subscriptionCallback, + (message) => this.subscriptionCallback(message), { id: 'sub-0', }, ); } - // eslint-disable-next-line class-methods-use-this private async subscriptionCallback(message: IMessage) { const isJson = message.headers['content-type']?.includes('application/json'); @@ -139,17 +139,62 @@ class EOSConnect extends Base { switch (messageData.type) { case 'social.chat.v1.NEW_WHISPER': { - // const friend = await this.client.xmpp.waitForFriend(m.from.split('@')[0]); - // if (!friend) return; + const friend = this.client.friend.list.get(messageData.payload.message.senderId); + + if (!friend) { + return; + } const friendMessage = new ReceivedFriendMessage(this.client, { - // TODO: FRIEND via xmpp??? - content: messageData.payload.message.body || '', author: {}, id: messageData.id!, sentAt: new Date(), + content: messageData.payload.message.body || '', + author: friend, + id: messageData.id!, + sentAt: new Date(messageData.payload.message.time), }); this.client.emit('friend:message', friendMessage); break; } + + case 'social.chat.v1.NEW_MESSAGE': { + if (messageData.payload.conversation.type !== 'party') { + return; + } + + await this.client.partyLock.wait(); + + const partyId = messageData.payload.conversation.conversationId.replace('p-', ''); + const authorId = messageData.payload.message.senderId; + + if (!this.client.party + || this.client.party.id !== partyId + || authorId === this.client.user.self!.id + ) { + return; + } + + const authorMember = this.client.party.members.get(authorId); + + if (!authorMember) { + return; + } + + const partyMessage = new PartyMessage(this.client, { + content: messageData.payload.message.body || '', + author: authorMember, + sentAt: new Date(messageData.payload.message.time), + id: messageData.id!, + party: this.client.party, + }); + + this.client.emit('party:member:message', partyMessage); + break; + } + + case 'core.connect.v1.connect-failed': { + this.client.debug(`failed connecting to eos connect: ${messageData.statusCode} - ${messageData.message}`); + break; + } } } } From 774508c8e468ea000ec014e59b5ec63dfc5e1573 Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Thu, 25 Jul 2024 19:47:59 +0200 Subject: [PATCH 3/5] feat(eos-connect): push working version --- index.ts | 2 +- resources/Endpoints.ts | 8 +- resources/enums.ts | 2 + resources/structs.ts | 111 ++++++++++++- src/Client.ts | 19 ++- src/auth/Auth.ts | 32 +++- src/auth/EOSAuthSession.ts | 102 ++++++++++++ src/exceptions/EpicgamesAPIError.ts | 6 + src/exceptions/StompConnectionError.ts | 23 +++ src/managers/ChatManager.ts | 59 +++++++ src/managers/FriendManager.ts | 17 +- src/stomp/EOSConnect.ts | 219 +++++++++++++++++++++++++ src/structures/eos/connect.ts | 80 --------- src/structures/party/Party.ts | 1 - src/structures/party/PartyChat.ts | 76 ++++++--- src/xmpp/EOSConnect.ts | 202 ----------------------- 16 files changed, 622 insertions(+), 337 deletions(-) create mode 100644 src/auth/EOSAuthSession.ts create mode 100644 src/exceptions/StompConnectionError.ts create mode 100644 src/managers/ChatManager.ts create mode 100644 src/stomp/EOSConnect.ts delete mode 100644 src/structures/eos/connect.ts delete mode 100644 src/xmpp/EOSConnect.ts diff --git a/index.ts b/index.ts index 0d701fc6..59174210 100644 --- a/index.ts +++ b/index.ts @@ -31,6 +31,7 @@ export { default as PartyNotFoundError } from './src/exceptions/PartyNotFoundErr export { default as PartyPermissionError } from './src/exceptions/PartyPermissionError'; export { default as SendMessageError } from './src/exceptions/SendMessageError'; export { default as StatsPrivacyError } from './src/exceptions/StatsPrivacyError'; +export { default as StompConnectionError } from './src/exceptions/StompConnectionError'; export { default as UserNotFoundError } from './src/exceptions/UserNotFoundError'; export { default as XMPPConnectionError } from './src/exceptions/XMPPConnectionError'; export { default as XMPPConnectionTimeoutError } from './src/exceptions/XMPPConnectionTimeoutError'; @@ -53,7 +54,6 @@ export { default as RadioStation } from './src/structures/RadioStation'; export { default as Stats } from './src/structures/Stats'; export { default as Tournament } from './src/structures/Tournament'; export { default as TournamentWindow } from './src/structures/TournamentWindow'; -export { default as connect } from './src/structures/eos/connect'; export { default as BaseFriendMessage } from './src/structures/friend/BaseFriendMessage'; export { default as BasePendingFriend } from './src/structures/friend/BasePendingFriend'; export { default as Friend } from './src/structures/friend/Friend'; diff --git a/resources/Endpoints.ts b/resources/Endpoints.ts index 19995564..a3087750 100644 --- a/resources/Endpoints.ts +++ b/resources/Endpoints.ts @@ -20,8 +20,12 @@ export default Object.freeze({ XMPP_SERVER: 'xmpp-service-prod.ol.epicgames.com', EPIC_PROD_ENV: 'prod.ol.epicgames.com', - // EOS CONNECT STOMP - STOMP_EOS_CONNECT_SERVER: 'connect.epicgames.dev', + // EOS + EOS_STOMP: 'connect.epicgames.dev', + EOS_TOKEN: 'https://api.epicgames.dev/epic/oauth/v2/token', + EOS_TOKEN_INFO: 'https://api.epicgames.dev/epic/oauth/v2/tokenInfo', + EOS_TOKEN_REVOKE: 'https://api.epicgames.dev/epic/oauth/v2/revoke', + EOS_CHAT: 'https://api.epicgames.dev/epic/chat', // BATTLE ROYALE BR_STATS_V2: 'https://statsproxy-public-service-live.ol.epicgames.com/statsproxy/api/statsv2', diff --git a/resources/enums.ts b/resources/enums.ts index ea99ccf9..a41bd2d4 100644 --- a/resources/enums.ts +++ b/resources/enums.ts @@ -1,11 +1,13 @@ export enum AuthSessionType { Fortnite = 'fortnite', FortniteClientCredentials = 'fortniteClientCredentials', + EOS = 'eos', Launcher = 'launcher', } export enum AuthSessionStoreKey { Fortnite = 'fortnite', FortniteClientCredentials = 'fortniteClientCredentials', + FortniteEOS = 'fortniteEOS', Launcher = 'launcher', } diff --git a/resources/structs.ts b/resources/structs.ts index 9394f6ae..62e974c3 100644 --- a/resources/structs.ts +++ b/resources/structs.ts @@ -29,6 +29,7 @@ import type { AuthSessionStoreKey } from './enums'; import type FortniteAuthSession from '../src/auth/FortniteAuthSession'; import type LauncherAuthSession from '../src/auth/LauncherAuthSession'; import type FortniteClientCredentialsAuthSession from '../src/auth/FortniteClientCredentialsAuthSession'; +import type EOSAuthSession from '../src/auth/EOSAuthSession'; export type PartyMemberSchema = Partial; export type PartySchema = Partial & { @@ -258,9 +259,9 @@ export interface ClientConfig { xmppDebug?: (message: string) => void; /** - * Debug function used for incoming and outgoing eos connect messages + * Debug function used for incoming and outgoing stomp eos connect messages */ - eosConnectDebug?: (message: string) => void; + stompEosConnectDebug?: (message: string) => void; /** * Default friend presence of the bot (eg. "Playing Battle Royale") @@ -331,9 +332,11 @@ export interface ClientConfig { connectToXMPP: boolean; /** - * WIP - disable if you dont care about dms or group messages + * Whether the client should connect to eos connect stomp + * NOTE: If you disable this, receiving party or private messages will no longer work. + * Do not disable this unless you know what you're doing */ - connectToEOSConnect: boolean; + connectToStompEOSConnect: boolean; /** * Whether the client should fetch all friends on startup. @@ -1449,9 +1452,109 @@ export interface FortniteClientCredentialsAuthData extends AuthData { application_id: string; } +export interface EOSAuthData extends AuthData { + refresh_expires: number; + refresh_expires_at: string; + refresh_token: string; + application_id: string; + merged_accounts: string[]; + scope: string; +} + export interface AuthSessionStore extends Collection { get(key: AuthSessionStoreKey.Fortnite): FortniteAuthSession | undefined; get(key: AuthSessionStoreKey.Launcher): LauncherAuthSession | undefined; + get(key: AuthSessionStoreKey.FortniteEOS): EOSAuthSession | undefined; get(key: AuthSessionStoreKey.FortniteClientCredentials): FortniteClientCredentialsAuthSession | undefined; get(key: K): V | undefined; } + +/* ------------------------------------------------------------------------------ */ +/* EOS CHAT */ +/* ------------------------------------------------------------------------------ */ + +export interface ChatMessagePayload { + body: string; +} + +/* --------------------------------------------------------------------------------------- */ +/* EOS Connect STOMP */ +/* --------------------------------------------------------------------------------------- */ +interface BaseEOSConnectMessage { + correlationId: string; + timestamp: number; // unix + id?: string; + connectionId?: string; +} + +export interface EOSConnectCoreConnected extends BaseEOSConnectMessage { + type: 'core.connect.v1.connected'; +} + +export interface EOSConnectCoreConnectFailed extends BaseEOSConnectMessage { + message: string; + statusCode: number; // i.e. 4005 + type: 'core.connect.v1.connect-failed'; +} + +export interface EOSConnectChatMemberLeftMessage extends BaseEOSConnectMessage { + payload: { + // deployment id + namespace: string; + conversationId: string; + members: string[]; + }; + type: 'social.chat.v1.MEMBERS_LEFT'; +} + +export interface EOSConnectChatNewMsgMessage extends BaseEOSConnectMessage { + payload: { + // deployment id + namespace: string; + conversation: { + conversationId: string; + type: string; // i.e. 'party + }; + message: { + body: string; + senderId: string; + time: number; + } + }; + type: 'social.chat.v1.NEW_MESSAGE'; +} + +export interface EOSConnectChatConversionCreatedMessage extends BaseEOSConnectMessage { + payload: { + // deployment id + namespace: string; + conversationId: string; + type: string; // i.e. 'party' + members: string[]; + }; + type: 'social.chat.v1.CONVERSATION_CREATED'; +} + +export interface EOSConnectChatNewWhisperMessage extends BaseEOSConnectMessage { + payload: { + // deployment id + namespace: string; + message: { + body: string; + senderId: string; + time: number; + } + }; + type: 'social.chat.v1.NEW_WHISPER'; +} + +export type EOSConnectMessage = + // Core + EOSConnectCoreConnected + | EOSConnectCoreConnectFailed + // Social chat + | EOSConnectChatConversionCreatedMessage + | EOSConnectChatNewMsgMessage + | EOSConnectChatMemberLeftMessage + | EOSConnectChatNewWhisperMessage; + diff --git a/src/Client.ts b/src/Client.ts index fe63200b..ea28d057 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -34,7 +34,8 @@ import EpicgamesAPIError from './exceptions/EpicgamesAPIError'; import UserManager from './managers/UserManager'; import FriendManager from './managers/FriendManager'; import STWManager from './managers/STWManager'; -import EOSConnect from './xmpp/EOSConnect'; +import EOSConnect from './stomp/EOSConnect'; +import ChatManager from './managers/ChatManager'; import type { PresenceShow } from 'stanza/Constants'; import type { BlurlStreamData, CreativeIslandData, @@ -105,7 +106,7 @@ class Client extends EventEmitter { /** * EOS Connect STOMP manager */ - public eosConnect: EOSConnect; + public stompEOSConnect: EOSConnect; /** * Friend manager @@ -132,6 +133,11 @@ class Client extends EventEmitter { */ public stw: STWManager; + /** + * EOS: Chat Manager + */ + public chat: ChatManager; + /** * @param config The client's configuration options */ @@ -154,7 +160,7 @@ class Client extends EventEmitter { forceNewParty: true, disablePartyService: false, connectToXMPP: true, - connectToEOSConnect: true, + connectToStompEOSConnect: true, fetchFriends: true, restRetryLimit: 1, handleRatelimits: true, @@ -203,7 +209,7 @@ class Client extends EventEmitter { this.auth = new Auth(this); this.http = new Http(this); this.xmpp = new XMPP(this); - this.eosConnect = new EOSConnect(this); + this.stompEOSConnect = new EOSConnect(this); this.partyLock = new AsyncLock(); this.cacheLock = new AsyncLock(); @@ -214,6 +220,7 @@ class Client extends EventEmitter { this.user = new UserManager(this); this.party = undefined; + this.chat = new ChatManager(this); this.tournaments = new TournamentManager(this); this.lastPartyMemberMeta = this.config.defaultPartyMemberMeta; @@ -253,7 +260,7 @@ class Client extends EventEmitter { this.cacheLock.lock(); try { if (this.config.connectToXMPP) await this.xmpp.connect(); - if (this.config.connectToEOSConnect) await this.eosConnect.connect(); + if (this.config.connectToStompEOSConnect) await this.stompEOSConnect.connect(); if (this.config.fetchFriends) await this.updateCaches(); } finally { this.cacheLock.unlock(); @@ -556,7 +563,7 @@ class Client extends EventEmitter { if (typeof this.config.xmppDebug === 'function') { this.config.xmppDebug(message); } break; case 'eos-connect': - if (typeof this.config.eosConnectDebug === 'function') { this.config.eosConnectDebug(message); } + if (typeof this.config.stompEosConnectDebug === 'function') { this.config.stompEosConnectDebug(message); } break; } } diff --git a/src/auth/Auth.ts b/src/auth/Auth.ts index 61e338b5..6b5c6039 100644 --- a/src/auth/Auth.ts +++ b/src/auth/Auth.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/indent */ /* eslint-disable no-restricted-syntax */ import { Collection } from '@discordjs/collection'; import { promises as fs } from 'fs'; @@ -11,13 +12,18 @@ import AuthClients from '../../resources/AuthClients'; import { resolveAuthObject, resolveAuthString } from '../util/Util'; import EpicgamesAPIError from '../exceptions/EpicgamesAPIError'; import FortniteClientCredentialsAuthSession from './FortniteClientCredentialsAuthSession'; +import EOSAuthSession from './EOSAuthSession'; import type { AuthClient, AuthStringResolveable, DeviceAuthResolveable, DeviceAuthWithSnakeCaseSupport, AuthSessionStore, } from '../../resources/structs'; import type Client from '../Client'; -type AuthSessionStoreType = AuthSessionStore; +type AuthSessionStoreType = AuthSessionStore; /** * Represents the client's authentication manager @@ -116,6 +122,11 @@ class Auth extends Base { this.sessions.set(AuthSessionStoreKey.FortniteClientCredentials, fortniteClientCredsSession); + // only create eos token if we connect to stomp + if (this.client.config.connectToStompEOSConnect) { + await this.fortniteEOSAuthenticate(); + } + this.client.debug(`[AUTH] Authentification successful (${((Date.now() - authStartTime) / 1000).toFixed(2)}s)`); } @@ -280,6 +291,25 @@ class Auth extends Base { this.sessions.set(AuthSessionStoreKey.Fortnite, fortniteSession); } + + private async fortniteEOSAuthenticate() { + const exchangeCode = await this.sessions.get(AuthSessionStoreKey.Fortnite)!.createExchangeCode(); + const authClient = this.client.config.auth.authClient!; + + const eosSession = await EOSAuthSession.create( + this.client, + AuthClients[authClient].clientId, + AuthClients[authClient].secret, + { + grant_type: 'exchange_code', + exchange_code: exchangeCode, + token_type: 'epic_id', + deployment_id: this.client.config.eosDeploymentId, + }, + ); + + this.sessions.set(AuthSessionStoreKey.FortniteEOS, eosSession); + } } export default Auth; diff --git a/src/auth/EOSAuthSession.ts b/src/auth/EOSAuthSession.ts new file mode 100644 index 00000000..c810e782 --- /dev/null +++ b/src/auth/EOSAuthSession.ts @@ -0,0 +1,102 @@ +import { URLSearchParams } from 'url'; +import AuthSession from './AuthSession'; +import { AuthSessionType } from '../../resources/enums'; +import Endpoints from '../../resources/Endpoints'; +import type Client from '../Client'; +import type { EOSAuthData } from '../../resources/structs'; + +class EOSAuthSession extends AuthSession { + public refreshToken: string; + public refreshTokenExpiresAt: Date; + public applicationId: string; + public mergedAccounts: string[]; + public scope: string; + + public refreshTimeout?: NodeJS.Timeout; + + constructor(client: Client, data: EOSAuthData, clientSecret: string) { + super(client, data, clientSecret, AuthSessionType.EOS); + + this.applicationId = data.application_id; + this.mergedAccounts = data.merged_accounts; + this.scope = data.scope; + this.refreshToken = data.refresh_token; + this.refreshTokenExpiresAt = new Date(data.refresh_expires_at); + } + + public async checkIsValid(forceVerify = false) { + if (!forceVerify && this.isExpired) { + return false; + } + + const validation = await this.client.http.epicgamesRequest({ + method: 'POST', + url: Endpoints.EOS_TOKEN_INFO, + headers: { + Authorization: `bearer ${this.accessToken}`, + }, + }); + + return validation.active === true; + } + + public async revoke() { + clearTimeout(this.refreshTimeout); + this.refreshTimeout = undefined; + + await this.client.http.epicgamesRequest({ + method: 'POST', + url: Endpoints.EOS_TOKEN_REVOKE, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: new URLSearchParams({ + token: this.accessToken, + }).toString(), + }); + } + + public async refresh() { + clearTimeout(this.refreshTimeout); + this.refreshTimeout = undefined; + + const refreshedSession = await EOSAuthSession.create(this.client, this.clientId, this.clientSecret, { + grant_type: 'refresh_token', + refresh_token: this.refreshToken, + }); + + this.accessToken = refreshedSession.accessToken; + this.expiresAt = refreshedSession.expiresAt; + this.refreshToken = refreshedSession.refreshToken; + this.refreshTokenExpiresAt = refreshedSession.refreshTokenExpiresAt; + this.applicationId = refreshedSession.applicationId; + this.mergedAccounts = refreshedSession.mergedAccounts; + this.scope = refreshedSession.scope; + + this.initRefreshTimeout(); + } + + public initRefreshTimeout() { + clearTimeout(this.refreshTimeout); + this.refreshTimeout = setTimeout(() => this.refresh(), this.expiresAt.getTime() - Date.now() - 15 * 60 * 1000); + } + + public static async create(client: Client, clientId: string, clientSecret: string, data: Record) { + const response = await client.http.epicgamesRequest({ + method: 'POST', + url: Endpoints.EOS_TOKEN, + headers: { + Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + data: new URLSearchParams(data).toString(), + }); + + const session = new EOSAuthSession(client, response, clientSecret); + session.initRefreshTimeout(); + + return session; + } +} + +export default EOSAuthSession; diff --git a/src/exceptions/EpicgamesAPIError.ts b/src/exceptions/EpicgamesAPIError.ts index 6d42406d..d2a99950 100644 --- a/src/exceptions/EpicgamesAPIError.ts +++ b/src/exceptions/EpicgamesAPIError.ts @@ -20,6 +20,11 @@ class EpicgamesAPIError extends Error { */ public code: string; + /** + * The Epicgames numeric error code (defaults to null) + */ + public numericCode: number | null; + /** * The HTTP status code */ @@ -48,6 +53,7 @@ class EpicgamesAPIError extends Error { this.method = request.method?.toUpperCase() || 'GET'; this.url = request.url || ''; this.code = error.errorCode; + this.numericCode = typeof error.numericErrorCode === 'number' ? error.numericErrorCode : null; this.messageVars = error.messageVars || []; this.httpStatus = status; this.requestData = request.data; diff --git a/src/exceptions/StompConnectionError.ts b/src/exceptions/StompConnectionError.ts new file mode 100644 index 00000000..51801076 --- /dev/null +++ b/src/exceptions/StompConnectionError.ts @@ -0,0 +1,23 @@ +/** + * Represents an error that is thrown when the stomp websocket connection fails to be established + */ +class StompConnectionError extends Error { + /** + * The error status code + */ + public statusCode: number; + + /** + * @param error The error that caused the connection to fail + */ + constructor(message: string, statusCode: number) { + super(); + + this.name = 'StompConnectionError'; + this.message = `Failed to connect to eos connect stomp: ${message} (status code ${statusCode})`; + + this.statusCode = statusCode; + } +} + +export default StompConnectionError; diff --git a/src/managers/ChatManager.ts b/src/managers/ChatManager.ts new file mode 100644 index 00000000..8347a164 --- /dev/null +++ b/src/managers/ChatManager.ts @@ -0,0 +1,59 @@ +import { randomUUID } from 'crypto'; +import Endpoints from '../../resources/Endpoints'; +import { AuthSessionStoreKey } from '../../resources/enums'; +import Base from '../Base'; +import UserNotFoundError from '../exceptions/UserNotFoundError'; +import type { ChatMessagePayload } from '../../resources/structs'; + +const generateCustomCorrelationId = () => `EOS-${Date.now()}-${randomUUID()}`; + +class ChatManager extends Base { + private get namespace() { + return this.client.config.eosDeploymentId; + } + + public async whisperUser(user: string, message: ChatMessagePayload) { + const accountId = await this.client.user.resolveId(user); + + if (!accountId) { + throw new UserNotFoundError(user); + } + + const correlationId = generateCustomCorrelationId(); + + await this.client.http.epicgamesRequest({ + method: 'POST', + url: `${Endpoints.EOS_CHAT}/v1/public/${this.namespace}/whisper/${this.client.user.self!.id}/${accountId}`, + headers: { + 'Content-Type': 'application/json', + 'X-Epic-Correlation-ID': correlationId, + }, + data: { + message, + }, + }, AuthSessionStoreKey.FortniteEOS); + + return correlationId; + } + + public async sendMessageInConversation(conversationId: string, message: ChatMessagePayload, allowedRecipients: string[]) { + const correlationId = generateCustomCorrelationId(); + + await this.client.http.epicgamesRequest({ + method: 'POST', + url: `${Endpoints.EOS_CHAT}/v1/public/${this.namespace}/conversations/${conversationId}/messages?fromAccountId=${this.client.user.self!.id}`, + headers: { + 'Content-Type': 'application/json', + 'X-Epic-Correlation-ID': correlationId, + }, + data: { + allowedRecipients, + message, + }, + }, AuthSessionStoreKey.FortniteEOS); + + return correlationId; + } +} + +export default ChatManager; diff --git a/src/managers/FriendManager.ts b/src/managers/FriendManager.ts index 5e3917c2..8f5bd7bc 100644 --- a/src/managers/FriendManager.ts +++ b/src/managers/FriendManager.ts @@ -9,7 +9,6 @@ import InviteeFriendshipsLimitExceededError from '../exceptions/InviteeFriendshi import InviteeFriendshipRequestLimitExceededError from '../exceptions/InviteeFriendshipRequestLimitExceededError'; import InviteeFriendshipSettingsError from '../exceptions/InviteeFriendshipSettingsError'; import OfferNotFoundError from '../exceptions/OfferNotFoundError'; -import SendMessageError from '../exceptions/SendMessageError'; import SentFriendMessage from '../structures/friend/SentFriendMessage'; import BasePendingFriend from '../structures/friend/BasePendingFriend'; import Base from '../Base'; @@ -185,25 +184,17 @@ class FriendManager extends Base { */ public async sendMessage(friend: string, content: string) { const resolvedFriend = this.resolve(friend); - if (!resolvedFriend) throw new FriendNotFoundError(friend); - if (!this.client.xmpp.isConnected) { - throw new SendMessageError('You\'re not connected via XMPP', 'FRIEND', resolvedFriend); + if (!resolvedFriend) { + throw new FriendNotFoundError(friend); } - const message = await this.client.xmpp.sendMessage( - `${resolvedFriend.id}@${Endpoints.EPIC_PROD_ENV}`, - content, - ); - - if (!message) { - throw new SendMessageError('Message timeout exceeded', 'FRIEND', resolvedFriend); - } + const messageId = await this.client.chat.whisperUser(resolvedFriend.id, { body: content }); return new SentFriendMessage(this.client, { author: this.client.user.self!, content, - id: message.id as string, + id: messageId, sentAt: new Date(), }); } diff --git a/src/stomp/EOSConnect.ts b/src/stomp/EOSConnect.ts new file mode 100644 index 00000000..29ab9627 --- /dev/null +++ b/src/stomp/EOSConnect.ts @@ -0,0 +1,219 @@ +/* eslint-disable no-console */ +import { Client as StompClient, Versions } from '@stomp/stompjs'; +import WebSocket, { type ErrorEvent } from 'ws'; +import Base from '../Base'; +import { AuthSessionStoreKey } from '../../resources/enums'; +import AuthenticationMissingError from '../exceptions/AuthenticationMissingError'; +import StompConnectionError from '../exceptions/StompConnectionError'; +import Endpoints from '../../resources/Endpoints'; +import ReceivedFriendMessage from '../structures/friend/ReceivedFriendMessage'; +import PartyMessage from '../structures/party/PartyMessage'; +import type { EOSConnectMessage } from '../../resources/structs'; +import type { IMessage } from '@stomp/stompjs'; +import type Client from '../Client'; + +/** + * Represents the client's EOS Connect STOMP manager (i.e. chat messages) + */ +class EOSConnect extends Base { + /** + * private stomp connection + */ + private stompConnection?: StompClient; + + /** + * private stomp connection id (i.e. used for eos presence) + */ + private stompConnectionId?: string; + + /** + * @param client The main client + */ + constructor(client: Client) { + super(client); + + this.stompConnection = undefined; + this.stompConnectionId = undefined; + } + + /** + * Whether the internal websocket is connected + */ + public get isConnected() { + return !!this.stompConnection && this.stompConnection.connected; + } + + /** + * Returns the eos stomp connection id + */ + public get connectionId() { + return this.stompConnectionId; + } + + public async connect() { + if (!this.client.auth.sessions.has(AuthSessionStoreKey.FortniteEOS)) { + throw new AuthenticationMissingError(AuthSessionStoreKey.FortniteEOS); + } + + return new Promise((resolve, reject) => { + const brokerURL = `wss://${Endpoints.EOS_STOMP}`; + + this.stompConnection = new StompClient({ + brokerURL, + stompVersions: new Versions([Versions.V1_0, Versions.V1_1, Versions.V1_2]), + heartbeatOutgoing: 30_000, + heartbeatIncoming: 0, + webSocketFactory: () => new WebSocket( + brokerURL, + { + headers: { + Authorization: `Bearer ${this.client.auth.sessions.get(AuthSessionStoreKey.FortniteEOS)!.accessToken}`, + 'Sec-Websocket-Protocol': 'v10.stomp,v11.stomp,v12.stomp', + 'Epic-Connect-Protocol': 'stomp', + 'Epic-Connect-Device-Id': '', + }, + }, + ), + debug: (str) => { + this.client.config.stompEosConnectDebug?.(str); + }, + onConnect: () => { + this.setupSubscription(resolve, reject); + }, + onStompError: (frame) => { + reject(new StompConnectionError(frame.body, -1)); + }, + onWebSocketError: (event) => { + const errorEvent = event; + + if (errorEvent.error || errorEvent.message) { + let error: Error = undefined!; + + if (errorEvent.error instanceof Error) { + error = errorEvent.error; + } else { + error = new Error(errorEvent.message); + } + + error.message += ' (eos connect stomp)'; + + reject(error); + } + }, + }); + + this.client.debug('[STOMP EOS Connect] Connecting...'); + + this.stompConnection.activate(); + }); + } + + /** + * Disconnects the stomp websocket client. + * Also performs a cleanup + */ + public disconnect() { + if (!this.stompConnection) return; + + this.stompConnection.forceDisconnect(); + this.stompConnection.deactivate(); + this.stompConnection = undefined; + this.stompConnectionId = undefined; + + this.client.debug('[STOMP EOS Connect] Disconnected'); + } + + private setupSubscription(connectionResolve: (value: unknown) => void, connectionReject: (reason?: unknown) => void) { + this.stompConnection!.subscribe( + `${this.client.config.eosDeploymentId}/account/${this.client.user.self!.id}`, + async (message: IMessage) => { + if (!message.headers['content-type']?.includes('application/json')) { + return; + } + + const messageData = JSON.parse(message.body); + + this.client.config.stompEosConnectDebug?.(message.body); + + this.client.debug(`new message '${messageData.type}' - ${message.body}`, 'eos-connect'); + + switch (messageData.type) { + case 'core.connect.v1.connected': { + this.stompConnectionId = messageData.connectionId!; + + this.client.debug(`[STOMP EOS Connect] Connected as ${messageData.connectionId!}`); + + connectionResolve(messageData.connectionId!); + break; + } + + case 'core.connect.v1.connect-failed': { + this.client.debug(`failed connecting to eos connect: ${messageData.statusCode} - ${messageData.message}`); + + connectionReject(new StompConnectionError(messageData.message, messageData.statusCode)); + break; + } + + case 'social.chat.v1.NEW_WHISPER': { + const { senderId, body, time } = messageData.payload.message; + const friend = this.client.friend.list.get(senderId); + + if (!friend || senderId === this.client.user.self!.id) { + return; + } + + const friendMessage = new ReceivedFriendMessage(this.client, { + content: body || '', + author: friend, + id: messageData.id!, + sentAt: new Date(time), + }); + + this.client.emit('friend:message', friendMessage); + break; + } + + case 'social.chat.v1.NEW_MESSAGE': { + if (messageData.payload.conversation.type !== 'party') { + return; + } + + await this.client.partyLock.wait(); + + const { conversation: { conversationId }, message: { senderId, body, time } } = messageData.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(this.client, { + content: body || '', + author: authorMember, + sentAt: new Date(time), + id: messageData.id!, + party: this.client.party, + }); + + this.client.emit('party:member:message', partyMessage); + break; + } + } + }, + { + id: 'sub-0', + }, + ); + } +} + +export default EOSConnect; diff --git a/src/structures/eos/connect.ts b/src/structures/eos/connect.ts deleted file mode 100644 index b6e308d7..00000000 --- a/src/structures/eos/connect.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable @typescript-eslint/indent */ -interface BaseEOSConnectMessage { - correlationId: string; - timestamp: number; // unix - id?: string; - connectionId?: string; -} - -export interface EOSConnectCoreConnected extends BaseEOSConnectMessage { - type: 'core.connect.v1.connected'; -} - -export interface EOSConnectCoreConnectFailed extends BaseEOSConnectMessage { - message: string; - statusCode: number; // i.e. 4005 - type: 'core.connect.v1.connect-failed'; -} - -export interface EOSConnectChatMemberLeftMessage extends BaseEOSConnectMessage { - payload: { - // deployment id - namespace: string; - conversationId: string; - members: string[]; - }; - type: 'social.chat.v1.MEMBERS_LEFT'; -} - -export interface EOSConnectChatNewMsgMessage extends BaseEOSConnectMessage { - payload: { - // deployment id - namespace: string; - conversation: { - conversationId: string; - type: string; // i.e. 'party - }; - message: { - body: string; - senderId: string; - time: number; - } - }; - type: 'social.chat.v1.NEW_MESSAGE'; -} - -export interface EOSConnectChatConversionCreatedMessage extends BaseEOSConnectMessage { - payload: { - // deployment id - namespace: string; - conversationId: string; - type: string; // i.e. 'party' - members: string[]; - }; - type: 'social.chat.v1.CONVERSATION_CREATED'; -} - -export interface EOSConnectChatNewWhisperMessage extends BaseEOSConnectMessage { - payload: { - // deployment id - namespace: string; - message: { - body: string; - senderId: string; - time: number; - } - }; - type: 'social.chat.v1.NEW_WHISPER'; -} - -export type EOSConnectMessage = - // Core - EOSConnectCoreConnected - | EOSConnectCoreConnectFailed - // Social chat - | EOSConnectChatConversionCreatedMessage - | EOSConnectChatNewMsgMessage - | EOSConnectChatMemberLeftMessage - | EOSConnectChatNewWhisperMessage; - -export default {}; diff --git a/src/structures/party/Party.ts b/src/structures/party/Party.ts index dedea4db..61957bb1 100644 --- a/src/structures/party/Party.ts +++ b/src/structures/party/Party.ts @@ -170,7 +170,6 @@ class Party extends Base { } this.client.setClientParty(this); - await this.client.party!.chat.join(); this.client.partyLock.unlock(); } diff --git a/src/structures/party/PartyChat.ts b/src/structures/party/PartyChat.ts index 61fb846e..354bc76f 100644 --- a/src/structures/party/PartyChat.ts +++ b/src/structures/party/PartyChat.ts @@ -1,5 +1,4 @@ import Base from '../../Base'; -import SendMessageError from '../../exceptions/SendMessageError'; import AsyncLock from '../../util/AsyncLock'; import PartyMessage from './PartyMessage'; import type Client from '../../Client'; @@ -7,21 +6,29 @@ import type ClientParty from './ClientParty'; import type ClientPartyMember from './ClientPartyMember'; /** - * Represents a party's multi user chat room (MUC) + * Represents a party's conversation */ class PartyChat extends Base { /** * The chat room's JID + * @deprecated since chat is not done over xmpp anymore, this property will always be an empty string */ public jid: string; + /** + * the party chats conversation id + */ + public conversationId: string; + /** * The client's chat room nickname + * @deprecated since chat is not done over xmpp anymore, this property will always be an empty string */ public nick: string; /** * The chat room's join lock + * @deprecated since chat is not done over xmpp anymore, this is not used anymore */ public joinLock: AsyncLock; @@ -32,9 +39,15 @@ class PartyChat extends Base { /** * Whether the client is connected to the party chat + * @deprecated since chat is not done over xmpp anymore, this property will always be true */ public isConnected: boolean; + /** + * Holds the account ids, which will not receive party messages anymore from the currently logged in user + */ + public bannedAccountIds: Set; + /** * @param client The main client * @param party The chat room's party @@ -42,13 +55,15 @@ class PartyChat extends Base { constructor(client: Client, party: ClientParty) { super(client); + // xmpp legacy (only here for backwards compatibility) this.joinLock = new AsyncLock(); - this.joinLock.lock(); + this.nick = ''; + this.jid = ''; + this.isConnected = true; this.party = party; - this.jid = `Party-${this.party?.id}@muc.prod.ol.epicgames.com`; - this.nick = `${this.client.user.self!.displayName}:${this.client.user.self!.id}:${this.client.xmpp.resource}`; - this.isConnected = false; + this.conversationId = `p-${party.id}`; + this.bannedAccountIds = new Set(); } /** @@ -56,45 +71,52 @@ class PartyChat extends Base { * @param content The message that will be sent */ public async send(content: string) { - await this.joinLock.wait(); - if (!this.isConnected) await this.join(); - - const message = await this.client.xmpp.sendMessage(this.jid, content, 'groupchat'); - - if (!message) throw new SendMessageError('Message timeout exceeded', 'PARTY', this.party); + const messageId = await this.client.chat.sendMessageInConversation( + this.conversationId, + { + body: content, + }, + this.party.members + .filter((x) => !this.bannedAccountIds.has(x.id)) + .map((x) => x.id), + ); return new PartyMessage(this.client, { - author: this.party.me as ClientPartyMember, content, party: this.party, id: message.id as string, + author: this.party.me as ClientPartyMember, + content, + party: this.party, + id: messageId, }); } /** * Joins this party chat + * @deprecated since chat is not done over xmpp anymore, this function will do nothing */ - public async join() { - this.joinLock.lock(); - await this.client.xmpp.joinMUC(this.jid, this.nick); - this.isConnected = true; - this.joinLock.unlock(); - } + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + public async join() { } /** * Leaves this party chat + * @deprecated since chat is not done over xmpp anymore, this function will do nothing */ - public async leave() { - await this.client.xmpp.leaveMUC(this.jid, this.nick); - this.isConnected = false; - } + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + public async leave() { } /** - * Ban a member from this party chat + * Ban a member from receiving party messages from the logged in user * @param member The member that should be banned */ public async ban(member: string) { - await this.joinLock.wait(); - if (!this.isConnected) await this.join(); + this.bannedAccountIds.add(member); + } - return this.client.xmpp?.ban(this.jid, member); + /** + * Unban a member from receiving party messages from the logged in user + * @param member The member that should be unbanned + */ + public async unban(member: string) { + this.bannedAccountIds.delete(member); } } diff --git a/src/xmpp/EOSConnect.ts b/src/xmpp/EOSConnect.ts deleted file mode 100644 index 3dfdf59c..00000000 --- a/src/xmpp/EOSConnect.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { Client as StompClient, Versions } from '@stomp/stompjs'; -import WebSocket from 'ws'; -import Base from '../Base'; -import { AuthSessionStoreKey } from '../../resources/enums'; -import AuthenticationMissingError from '../exceptions/AuthenticationMissingError'; -import Endpoints from '../../resources/Endpoints'; -import AuthClients from '../../resources/AuthClients'; -import ReceivedFriendMessage from '../structures/friend/ReceivedFriendMessage'; -import PartyMessage from '../structures/party/PartyMessage'; -import type { EOSConnectMessage } from '../structures/eos/connect'; -import type { IMessage } from '@stomp/stompjs'; -import type Client from '../Client'; - -/** - * Represents the client's EOS Connect STOMP manager (i.e. chat messages) - */ -class EOSConnect extends Base { - /** - * private stomp connection - */ - private stompConnection?: StompClient; - - /** - * @param client The main client - */ - constructor(client: Client) { - super(client); - - this.stompConnection = undefined; - } - - public async connect() { - if (!this.client.auth.sessions.has(AuthSessionStoreKey.Fortnite)) { - throw new AuthenticationMissingError(AuthSessionStoreKey.Fortnite); - } - - // own func - const { code: exchangeCode } = await this.client.http.epicgamesRequest({ - url: Endpoints.OAUTH_EXCHANGE, - headers: { - Authorization: `bearer ${this.client.auth.sessions.get(AuthSessionStoreKey.Fortnite)!.accessToken}`, - }, - }); - - // own func - maybe actual auth like others? - const epicIdAuth = await this.client.http.epicgamesRequest<{ access_token: string }>({ - method: 'POST', - url: 'https://api.epicgames.dev/epic/oauth/v2/token', - headers: { - Authorization: - `basic ${Buffer.from(`${AuthClients.fortniteIOSGameClient.clientId}:${AuthClients.fortniteIOSGameClient.secret}`).toString('base64')}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - data: new URLSearchParams({ - grant_type: 'exchange_code', - exchange_code: exchangeCode, - token_type: 'epic_id', - deployment_id: this.client.config.eosDeploymentId, - }).toString(), - }); - - console.log(epicIdAuth); - - const brokerURL = `wss://${Endpoints.STOMP_EOS_CONNECT_SERVER}`; - - this.stompConnection = new StompClient({ - brokerURL, - stompVersions: new Versions([Versions.V1_0, Versions.V1_1, Versions.V1_2]), - heartbeatOutgoing: 30_000, - heartbeatIncoming: 0, - webSocketFactory: () => new WebSocket( - brokerURL, - { - headers: { - Authorization: `Bearer ${epicIdAuth.access_token}`, - 'Epic-Connect-Protocol': 'stomp', - 'Epic-Connect-Device-Id': '', - }, - }, - ), - debug: (str) => { - console.log(`STOMP: ${str}`); - }, - onConnect: (idk) => { - console.log(idk); - - this.setupSubscription(); - }, - onStompError(frame) { - console.log(frame.command); - console.log(frame.headers); - console.log(frame.body); - }, - onChangeState(state) { - console.log('state', state); - }, - onDisconnect(frame) { - console.log(frame); - }, - onUnhandledFrame(frame) { - console.log('onUnhandledFrame', frame.command, frame.body); - }, - onUnhandledReceipt(frame) { - console.log(frame); - }, - onWebSocketError: (evt) => { - console.log(evt); - }, - onUnhandledMessage: (msg) => { - console.log(msg.command, msg.command); - }, - }); - - this.stompConnection.activate(); - } - - private setupSubscription() { - this.stompConnection!.subscribe( - `${this.client.config.eosDeploymentId}/account/${this.client.user.self!.id}`, - (message) => this.subscriptionCallback(message), - { - id: 'sub-0', - }, - ); - } - - private async subscriptionCallback(message: IMessage) { - const isJson = message.headers['content-type']?.includes('application/json'); - - if (!isJson) { - return; - } - - const messageData = JSON.parse(message.body); - - console.log(messageData); - - this.client.debug(`new message '${messageData.type}' - ${message.body}`, 'eos-connect'); - - switch (messageData.type) { - case 'social.chat.v1.NEW_WHISPER': { - const friend = this.client.friend.list.get(messageData.payload.message.senderId); - - if (!friend) { - return; - } - - const friendMessage = new ReceivedFriendMessage(this.client, { - content: messageData.payload.message.body || '', - author: friend, - id: messageData.id!, - sentAt: new Date(messageData.payload.message.time), - }); - - this.client.emit('friend:message', friendMessage); - break; - } - - case 'social.chat.v1.NEW_MESSAGE': { - if (messageData.payload.conversation.type !== 'party') { - return; - } - - await this.client.partyLock.wait(); - - const partyId = messageData.payload.conversation.conversationId.replace('p-', ''); - const authorId = messageData.payload.message.senderId; - - if (!this.client.party - || this.client.party.id !== partyId - || authorId === this.client.user.self!.id - ) { - return; - } - - const authorMember = this.client.party.members.get(authorId); - - if (!authorMember) { - return; - } - - const partyMessage = new PartyMessage(this.client, { - content: messageData.payload.message.body || '', - author: authorMember, - sentAt: new Date(messageData.payload.message.time), - id: messageData.id!, - party: this.client.party, - }); - - this.client.emit('party:member:message', partyMessage); - break; - } - - case 'core.connect.v1.connect-failed': { - this.client.debug(`failed connecting to eos connect: ${messageData.statusCode} - ${messageData.message}`); - break; - } - } - } -} - -export default EOSConnect; From f549ca1e568a8e33563a60e4a3cf7d4842d4acf9 Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Thu, 25 Jul 2024 20:04:39 +0200 Subject: [PATCH 4/5] feat(docs): improve eos connect changes docs --- src/managers/ChatManager.ts | 22 +++++++++++++++++++++- src/stomp/EOSConnect.ts | 9 ++++++++- src/xmpp/XMPP.ts | 5 +++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/managers/ChatManager.ts b/src/managers/ChatManager.ts index 8347a164..58f3e738 100644 --- a/src/managers/ChatManager.ts +++ b/src/managers/ChatManager.ts @@ -5,13 +5,26 @@ import Base from '../Base'; import UserNotFoundError from '../exceptions/UserNotFoundError'; import type { ChatMessagePayload } from '../../resources/structs'; +// private scope const generateCustomCorrelationId = () => `EOS-${Date.now()}-${randomUUID()}`; +/** + * Represent's the client's chat manager (dm, party chat) via eos. + */ class ChatManager extends Base { - private get namespace() { + /** + * Returns the chat namespace, this is the eos deployment id + */ + public get namespace() { return this.client.config.eosDeploymentId; } + /** + * Sends a private message to the specified user + * @param user the account id or displayname + * @param message the message object + * @returns the message id + */ public async whisperUser(user: string, message: ChatMessagePayload) { const accountId = await this.client.user.resolveId(user); @@ -36,6 +49,13 @@ class ChatManager extends Base { return correlationId; } + /** + * Sends a message in the specified conversation (party chat) + * @param conversationId the conversation id, usually `p-[PARTYID]` + * @param message the message object + * @param allowedRecipients the account ids, that should receive the message + * @returns the message id + */ public async sendMessageInConversation(conversationId: string, message: ChatMessagePayload, allowedRecipients: string[]) { const correlationId = generateCustomCorrelationId(); diff --git a/src/stomp/EOSConnect.ts b/src/stomp/EOSConnect.ts index 29ab9627..096a45ba 100644 --- a/src/stomp/EOSConnect.ts +++ b/src/stomp/EOSConnect.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { Client as StompClient, Versions } from '@stomp/stompjs'; import WebSocket, { type ErrorEvent } from 'ws'; import Base from '../Base'; @@ -50,6 +49,9 @@ class EOSConnect extends Base { return this.stompConnectionId; } + /** + * connect to the eos connect stomp server + */ public async connect() { if (!this.client.auth.sessions.has(AuthSessionStoreKey.FortniteEOS)) { throw new AuthenticationMissingError(AuthSessionStoreKey.FortniteEOS); @@ -123,6 +125,11 @@ class EOSConnect extends Base { this.client.debug('[STOMP EOS Connect] Disconnected'); } + /** + * Sets up the subscription for the deployment + * @param connectionResolve connect promise resolve + * @param connectionReject connect promise reject + */ private setupSubscription(connectionResolve: (value: unknown) => void, connectionReject: (reason?: unknown) => void) { this.stompConnection!.subscribe( `${this.client.config.eosDeploymentId}/account/${this.client.user.self!.id}`, diff --git a/src/xmpp/XMPP.ts b/src/xmpp/XMPP.ts index cb2f66f1..973aad5a 100644 --- a/src/xmpp/XMPP.ts +++ b/src/xmpp/XMPP.ts @@ -671,6 +671,7 @@ class XMPP extends Base { * @param to The message receiver's JID * @param content The message that will be sent * @param type The message type (eg "chat" or "groupchat") + * @deprecated this is useless now, since chat messages are handeled via an rest api now see {@link Client#chat} */ public async sendMessage(to: string, content: string, type: Constants.MessageType = 'chat') { return this.waitForSentMessage(this.connection!.sendMessage({ @@ -684,6 +685,7 @@ class XMPP extends Base { * Wait until a message is sent * @param id The message id * @param timeout How long to wait for the message + * @deprecated this is useless now, since chat messages are handeled via an rest api now see {@link Client#chat} */ public waitForSentMessage(id: string, timeout = 1000) { return new Promise((res) => { @@ -710,6 +712,7 @@ class XMPP extends Base { * Joins a multi user chat room (MUC) * @param jid The room's JID * @param nick The client's nickname + * @deprecated this is useless now, since chat messages are handeled via an rest api now see {@link Client#chat} */ public async joinMUC(jid: string, nick: string) { return this.connection!.joinRoom(jid, nick); @@ -719,6 +722,7 @@ class XMPP extends Base { * Leaves a multi user chat room (MUC) * @param jid The room's JID * @param nick The client's nickname + * @deprecated this is useless now, since chat messages are handeled via an rest api now see {@link Client#chat} */ public async leaveMUC(jid: string, nick: string) { return this.connection!.leaveRoom(jid, nick); @@ -727,6 +731,7 @@ class XMPP extends Base { /** * Bans a member from a multi user chat room * @param member The member that should be banned + * @deprecated this is useless now, since chat messages are handeled via an rest api now see {@link Client#chat} */ public async ban(jid: string, member: string) { return this.connection!.ban(jid, `${member}@${Endpoints.EPIC_PROD_ENV}`); From c78a0b3c7f32e53a9dd726a69cfb8dde35922491 Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Thu, 25 Jul 2024 20:40:59 +0200 Subject: [PATCH 5/5] fix(example): fix example import, dont allow to set conversationId --- docs/examples/simple.js | 2 +- src/structures/party/PartyChat.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/examples/simple.js b/docs/examples/simple.js index 79f8bf21..2d99803d 100644 --- a/docs/examples/simple.js +++ b/docs/examples/simple.js @@ -1,5 +1,5 @@ /* eslint-disable */ -const { Client } = require('../../'); +const { Client } = require('fnbr'); const client = new Client(); diff --git a/src/structures/party/PartyChat.ts b/src/structures/party/PartyChat.ts index 354bc76f..7ab23266 100644 --- a/src/structures/party/PartyChat.ts +++ b/src/structures/party/PartyChat.ts @@ -18,7 +18,9 @@ class PartyChat extends Base { /** * the party chats conversation id */ - public conversationId: string; + public get conversationId() { + return `p-${this.party.id}`; + } /** * The client's chat room nickname @@ -62,7 +64,6 @@ class PartyChat extends Base { this.isConnected = true; this.party = party; - this.conversationId = `p-${party.id}`; this.bannedAccountIds = new Set(); }