From e6f7430c610ad94b82c0868f56e24edebe1743a8 Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Tue, 23 Jul 2024 22:25:28 +0200 Subject: [PATCH 1/8] (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/8] 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/8] 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/8] 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/8] 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(); } From d48214e699522d90c5b0997f1cc8761ff14a180d Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Thu, 25 Jul 2024 22:23:40 +0200 Subject: [PATCH 6/8] feat(docs): add message jsdoc & typo --- resources/structs.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/resources/structs.ts b/resources/structs.ts index 62e974c3..34c22773 100644 --- a/resources/structs.ts +++ b/resources/structs.ts @@ -1473,7 +1473,13 @@ export interface AuthSessionStore extends Collection { /* EOS CHAT */ /* ------------------------------------------------------------------------------ */ +/** + * Represents a chat message data + */ export interface ChatMessagePayload { + /** + * The message body, should not be empty and not exceeed the limit of 256 characters + */ body: string; } @@ -1513,7 +1519,7 @@ export interface EOSConnectChatNewMsgMessage extends BaseEOSConnectMessage { namespace: string; conversation: { conversationId: string; - type: string; // i.e. 'party + type: string; // i.e. 'party' }; message: { body: string; From b3632e620e86d4eea42a045e9ee2683c9866f9f4 Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Fri, 26 Jul 2024 18:21:48 +0200 Subject: [PATCH 7/8] feat(docs): proper deprecations, fix typos, throw error on empty party message --- index.ts | 1 + resources/structs.ts | 3 +- src/Client.ts | 3 +- src/exceptions/CreativeIslandNotFoundError.ts | 2 +- src/exceptions/CreatorCodeNotFoundError.ts | 2 +- src/exceptions/DuplicateFriendshipError.ts | 2 +- src/exceptions/EpicgamesAPIError.ts | 2 +- src/exceptions/EventTimeoutError.ts | 2 +- src/exceptions/FriendNotFoundError.ts | 2 +- .../FriendshipRequestAlreadySentError.ts | 2 +- ...iteeFriendshipRequestLimitExceededError.ts | 2 +- .../InviteeFriendshipSettingsError.ts | 2 +- .../InviteeFriendshipsLimitExceededError.ts | 2 +- .../InviterFriendshipsLimitExceededError.ts | 2 +- src/exceptions/MatchNotFoundError.ts | 2 +- src/exceptions/OfferNotFoundError.ts | 2 +- src/exceptions/PartyAlreadyJoinedError.ts | 2 +- .../PartyChatConversationNotFound.ts | 12 ++++ src/exceptions/PartyInvitationExpiredError.ts | 2 +- src/exceptions/PartyMaxSizeReachedError.ts | 2 +- src/exceptions/PartyMemberNotFoundError.ts | 2 +- src/exceptions/PartyNotFoundError.ts | 2 +- src/exceptions/PartyPermissionError.ts | 2 +- src/exceptions/SendMessageError.ts | 2 +- src/exceptions/StatsPrivacyError.ts | 2 +- src/exceptions/UserNotFoundError.ts | 2 +- src/managers/ChatManager.ts | 4 ++ src/stomp/EOSConnect.ts | 3 + src/structures/party/ClientParty.ts | 11 +++- src/structures/party/PartyChat.ts | 64 ++++++++++--------- src/xmpp/XMPP.ts | 38 +++++++---- 31 files changed, 113 insertions(+), 70 deletions(-) create mode 100644 src/exceptions/PartyChatConversationNotFound.ts diff --git a/index.ts b/index.ts index 59174210..d888cb8c 100644 --- a/index.ts +++ b/index.ts @@ -24,6 +24,7 @@ export { default as InviterFriendshipsLimitExceededError } from './src/exception export { default as MatchNotFoundError } from './src/exceptions/MatchNotFoundError'; export { default as OfferNotFoundError } from './src/exceptions/OfferNotFoundError'; export { default as PartyAlreadyJoinedError } from './src/exceptions/PartyAlreadyJoinedError'; +export { default as PartyChatConversationNotFound } from './src/exceptions/PartyChatConversationNotFound'; export { default as PartyInvitationExpiredError } from './src/exceptions/PartyInvitationExpiredError'; export { default as PartyMaxSizeReachedError } from './src/exceptions/PartyMaxSizeReachedError'; export { default as PartyMemberNotFoundError } from './src/exceptions/PartyMemberNotFoundError'; diff --git a/resources/structs.ts b/resources/structs.ts index 34c22773..fc0c6b40 100644 --- a/resources/structs.ts +++ b/resources/structs.ts @@ -1478,7 +1478,7 @@ export interface AuthSessionStore extends Collection { */ export interface ChatMessagePayload { /** - * The message body, should not be empty and not exceeed the limit of 256 characters + * The message body, should not be empty and not exceed the limit of 256 characters. Please note that emojis count as 2 characters. */ body: string; } @@ -1563,4 +1563,3 @@ export type EOSConnectMessage = | EOSConnectChatNewMsgMessage | EOSConnectChatMemberLeftMessage | EOSConnectChatNewWhisperMessage; - diff --git a/src/Client.ts b/src/Client.ts index ea28d057..58b9ff77 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -48,7 +48,7 @@ import type { } from '../resources/structs'; /** - * Represets the main client + * Represents the main client */ class Client extends EventEmitter { /** @@ -771,7 +771,6 @@ class Client extends EventEmitter { }, newPrivacy.deleted); this.partyLock.unlock(); - await this.party.chat.join(); return undefined; } diff --git a/src/exceptions/CreativeIslandNotFoundError.ts b/src/exceptions/CreativeIslandNotFoundError.ts index d3225fc7..6c16a061 100644 --- a/src/exceptions/CreativeIslandNotFoundError.ts +++ b/src/exceptions/CreativeIslandNotFoundError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a creative island does not exist + * Represents an error thrown because a creative island does not exist */ class CreativeIslandNotFoundError extends Error { /** diff --git a/src/exceptions/CreatorCodeNotFoundError.ts b/src/exceptions/CreatorCodeNotFoundError.ts index 15f55cc5..abe25a5d 100644 --- a/src/exceptions/CreatorCodeNotFoundError.ts +++ b/src/exceptions/CreatorCodeNotFoundError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a creator code does not exist + * Represents an error thrown because a creator code does not exist */ class CreatorCodeNotFoundError extends Error { /** diff --git a/src/exceptions/DuplicateFriendshipError.ts b/src/exceptions/DuplicateFriendshipError.ts index 970d4ab3..06108fab 100644 --- a/src/exceptions/DuplicateFriendshipError.ts +++ b/src/exceptions/DuplicateFriendshipError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a friendship already exists + * Represents an error thrown because a friendship already exists */ class DuplicateFriendshipError extends Error { /** diff --git a/src/exceptions/EpicgamesAPIError.ts b/src/exceptions/EpicgamesAPIError.ts index d2a99950..6b5708c3 100644 --- a/src/exceptions/EpicgamesAPIError.ts +++ b/src/exceptions/EpicgamesAPIError.ts @@ -2,7 +2,7 @@ import type { AxiosRequestConfig } from 'axios'; import type { EpicgamesAPIErrorData } from '../../resources/httpResponses'; /** - * Represets an HTTP error from the Epicgames API + * Represents an HTTP error from the Epicgames API */ class EpicgamesAPIError extends Error { /** diff --git a/src/exceptions/EventTimeoutError.ts b/src/exceptions/EventTimeoutError.ts index 1ab3b6aa..7110a855 100644 --- a/src/exceptions/EventTimeoutError.ts +++ b/src/exceptions/EventTimeoutError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because an event timeout was exceeded + * Represents an error thrown because an event timeout was exceeded */ class EventTimeoutError extends Error { /** diff --git a/src/exceptions/FriendNotFoundError.ts b/src/exceptions/FriendNotFoundError.ts index 2ef66cad..92422ef3 100644 --- a/src/exceptions/FriendNotFoundError.ts +++ b/src/exceptions/FriendNotFoundError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a friend does not exist + * Represents an error thrown because a friend does not exist */ class FriendNotFoundError extends Error { /** diff --git a/src/exceptions/FriendshipRequestAlreadySentError.ts b/src/exceptions/FriendshipRequestAlreadySentError.ts index c7e351e9..44079493 100644 --- a/src/exceptions/FriendshipRequestAlreadySentError.ts +++ b/src/exceptions/FriendshipRequestAlreadySentError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a friendship request has already been sent + * Represents an error thrown because a friendship request has already been sent */ class FriendshipRequestAlreadySentError extends Error { /** diff --git a/src/exceptions/InviteeFriendshipRequestLimitExceededError.ts b/src/exceptions/InviteeFriendshipRequestLimitExceededError.ts index a08f646b..9802f00a 100644 --- a/src/exceptions/InviteeFriendshipRequestLimitExceededError.ts +++ b/src/exceptions/InviteeFriendshipRequestLimitExceededError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because the friendship invitee reached their friendship requests limit + * Represents an error thrown because the friendship invitee reached their friendship requests limit */ class InviteeFriendshipRequestLimitExceededError extends Error { /** diff --git a/src/exceptions/InviteeFriendshipSettingsError.ts b/src/exceptions/InviteeFriendshipSettingsError.ts index 1ea37f39..ca564c62 100644 --- a/src/exceptions/InviteeFriendshipSettingsError.ts +++ b/src/exceptions/InviteeFriendshipSettingsError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because the friendship invitee does disabled friendship requests + * Represents an error thrown because the friendship invitee does disabled friendship requests */ class InviteeFriendshipSettingsError extends Error { /** diff --git a/src/exceptions/InviteeFriendshipsLimitExceededError.ts b/src/exceptions/InviteeFriendshipsLimitExceededError.ts index 980a6f2c..b5efbdab 100644 --- a/src/exceptions/InviteeFriendshipsLimitExceededError.ts +++ b/src/exceptions/InviteeFriendshipsLimitExceededError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because the friendship invitee reached their friendships limit + * Represents an error thrown because the friendship invitee reached their friendships limit */ class InviteeFriendshipsLimitExceededError extends Error { /** diff --git a/src/exceptions/InviterFriendshipsLimitExceededError.ts b/src/exceptions/InviterFriendshipsLimitExceededError.ts index 68da9f98..c239963b 100644 --- a/src/exceptions/InviterFriendshipsLimitExceededError.ts +++ b/src/exceptions/InviterFriendshipsLimitExceededError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because the client reached its friendships limit + * Represents an error thrown because the client reached its friendships limit */ class InviterFriendshipsLimitExceededError extends Error { /** diff --git a/src/exceptions/MatchNotFoundError.ts b/src/exceptions/MatchNotFoundError.ts index 1cd465ad..e7e21b98 100644 --- a/src/exceptions/MatchNotFoundError.ts +++ b/src/exceptions/MatchNotFoundError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a match does not exist + * Represents an error thrown because a match does not exist */ class MatchNotFoundError extends Error { /** diff --git a/src/exceptions/OfferNotFoundError.ts b/src/exceptions/OfferNotFoundError.ts index 44709a4e..3f76243b 100644 --- a/src/exceptions/OfferNotFoundError.ts +++ b/src/exceptions/OfferNotFoundError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because an offer does not exist (anymore) + * Represents an error thrown because an offer does not exist (anymore) */ class OfferNotFoundError extends Error { /** diff --git a/src/exceptions/PartyAlreadyJoinedError.ts b/src/exceptions/PartyAlreadyJoinedError.ts index 1064f2ed..e578b7e4 100644 --- a/src/exceptions/PartyAlreadyJoinedError.ts +++ b/src/exceptions/PartyAlreadyJoinedError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a member (or the client) already joined a party + * Represents an error thrown because a member (or the client) already joined a party */ class PartyAlreadyJoinedError extends Error { constructor() { diff --git a/src/exceptions/PartyChatConversationNotFound.ts b/src/exceptions/PartyChatConversationNotFound.ts new file mode 100644 index 00000000..4c952a45 --- /dev/null +++ b/src/exceptions/PartyChatConversationNotFound.ts @@ -0,0 +1,12 @@ +/** + * Represents an error thrown because you tried to send a party chat message, but there is no chat yet, because you are the only person in the party + */ +class PartyChatConversationNotFound extends Error { + constructor() { + super(); + this.name = 'PartyChatConversationNotFound'; + this.message = 'There is no party chat conversation yet. You cannot send party chat messages when you are the only party member.'; + } +} + +export default PartyChatConversationNotFound; diff --git a/src/exceptions/PartyInvitationExpiredError.ts b/src/exceptions/PartyInvitationExpiredError.ts index 3d9a712e..b6a63fb4 100644 --- a/src/exceptions/PartyInvitationExpiredError.ts +++ b/src/exceptions/PartyInvitationExpiredError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because the client does not have permission to perform a certain action + * Represents an error thrown because the client does not have permission to perform a certain action */ class PartyInvitationExpiredError extends Error { constructor() { diff --git a/src/exceptions/PartyMaxSizeReachedError.ts b/src/exceptions/PartyMaxSizeReachedError.ts index aad286db..a5c111c0 100644 --- a/src/exceptions/PartyMaxSizeReachedError.ts +++ b/src/exceptions/PartyMaxSizeReachedError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a party already reached its max member count + * Represents an error thrown because a party already reached its max member count */ class PartyMaxSizeReachedError extends Error { constructor() { diff --git a/src/exceptions/PartyMemberNotFoundError.ts b/src/exceptions/PartyMemberNotFoundError.ts index 1df0e728..b6c4cfdd 100644 --- a/src/exceptions/PartyMemberNotFoundError.ts +++ b/src/exceptions/PartyMemberNotFoundError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a party member does not exist + * Represents an error thrown because a party member does not exist */ class PartyMemberNotFoundError extends Error { /** diff --git a/src/exceptions/PartyNotFoundError.ts b/src/exceptions/PartyNotFoundError.ts index 9888e45b..adef2fba 100644 --- a/src/exceptions/PartyNotFoundError.ts +++ b/src/exceptions/PartyNotFoundError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a party does not exist + * Represents an error thrown because a party does not exist */ class PartyNotFoundError extends Error { constructor() { diff --git a/src/exceptions/PartyPermissionError.ts b/src/exceptions/PartyPermissionError.ts index d942e72d..4aa7f9ea 100644 --- a/src/exceptions/PartyPermissionError.ts +++ b/src/exceptions/PartyPermissionError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because the client does not have permission to perform a certain party related action + * Represents an error thrown because the client does not have permission to perform a certain party related action */ class PartyPermissionError extends Error { constructor() { diff --git a/src/exceptions/SendMessageError.ts b/src/exceptions/SendMessageError.ts index c4ba90b9..d763753c 100644 --- a/src/exceptions/SendMessageError.ts +++ b/src/exceptions/SendMessageError.ts @@ -3,7 +3,7 @@ import type ClientParty from '../structures/party/ClientParty'; import type Friend from '../structures/friend/Friend'; /** - * Represets an error thrown because a user does not exist + * Represents an error thrown because a user does not exist */ class SendMessageError extends Error { /** diff --git a/src/exceptions/StatsPrivacyError.ts b/src/exceptions/StatsPrivacyError.ts index e7846ea3..4e62b719 100644 --- a/src/exceptions/StatsPrivacyError.ts +++ b/src/exceptions/StatsPrivacyError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a user set their stats to private + * Represents an error thrown because a user set their stats to private */ class StatsPrivacyError extends Error { /** diff --git a/src/exceptions/UserNotFoundError.ts b/src/exceptions/UserNotFoundError.ts index d01d791f..5a83e743 100644 --- a/src/exceptions/UserNotFoundError.ts +++ b/src/exceptions/UserNotFoundError.ts @@ -1,5 +1,5 @@ /** - * Represets an error thrown because a user does not exist + * Represents an error thrown because a user does not exist */ class UserNotFoundError extends Error { /** diff --git a/src/managers/ChatManager.ts b/src/managers/ChatManager.ts index 58f3e738..1c4a630a 100644 --- a/src/managers/ChatManager.ts +++ b/src/managers/ChatManager.ts @@ -3,6 +3,7 @@ import Endpoints from '../../resources/Endpoints'; import { AuthSessionStoreKey } from '../../resources/enums'; import Base from '../Base'; import UserNotFoundError from '../exceptions/UserNotFoundError'; +import EpicgamesAPIError from '../exceptions/EpicgamesAPIError'; import type { ChatMessagePayload } from '../../resources/structs'; // private scope @@ -24,6 +25,8 @@ class ChatManager extends Base { * @param user the account id or displayname * @param message the message object * @returns the message id + * @throws {UserNotFoundError} When the specified user was not found + * @throws {EpicgamesAPIError} When the api request failed */ public async whisperUser(user: string, message: ChatMessagePayload) { const accountId = await this.client.user.resolveId(user); @@ -55,6 +58,7 @@ class ChatManager extends Base { * @param message the message object * @param allowedRecipients the account ids, that should receive the message * @returns the message id + * @throws {EpicgamesAPIError} When the api request failed */ 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 096a45ba..9cf30487 100644 --- a/src/stomp/EOSConnect.ts +++ b/src/stomp/EOSConnect.ts @@ -51,6 +51,9 @@ class EOSConnect extends Base { /** * connect to the eos connect stomp server + * @throws {AuthenticationMissingError} When there is no eos auth to use for stomp auth + * @throws {StompConnectionError} When the connection failed for any reason + * @throws {Error} When there was an error with the underlying websocket */ public async connect() { if (!this.client.auth.sessions.has(AuthSessionStoreKey.FortniteEOS)) { diff --git a/src/structures/party/ClientParty.ts b/src/structures/party/ClientParty.ts index f948a21c..e52d2e8c 100644 --- a/src/structures/party/ClientParty.ts +++ b/src/structures/party/ClientParty.ts @@ -1,5 +1,6 @@ import { Collection } from '@discordjs/collection'; import { AsyncQueue } from '@sapphire/async-queue'; +import { deprecate } from 'util'; import Endpoints from '../../../resources/Endpoints'; import FriendNotFoundError from '../../exceptions/FriendNotFoundError'; import PartyAlreadyJoinedError from '../../exceptions/PartyAlreadyJoinedError'; @@ -21,6 +22,8 @@ import type { import type PartyMember from './PartyMember'; import type Friend from '../friend/Friend'; +const deprecationNotOverXmppAnymore = 'Party Chat is not done over XMPP anymore, this function will be removed in a future version'; + /** * Represents a party that the client is a member of */ @@ -86,8 +89,6 @@ class ClientParty extends Party { public async leave(createNew = true) { this.client.partyLock.lock(); - if (this.chat.isConnected) await this.chat.leave(); - try { await this.client.http.epicgamesRequest({ method: 'DELETE', @@ -262,9 +263,13 @@ class ClientParty extends Party { /** * Ban a member from this party chat * @param member The member that should be banned + * @deprecated This feature has been deprecated since epic moved chatting away from xmpp */ + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars public async chatBan(member: string) { - return this.chat.ban(member); + const deprecatedFn = deprecate(() => { }, deprecationNotOverXmppAnymore); + + return deprecatedFn(); } /** diff --git a/src/structures/party/PartyChat.ts b/src/structures/party/PartyChat.ts index 7ab23266..6e356220 100644 --- a/src/structures/party/PartyChat.ts +++ b/src/structures/party/PartyChat.ts @@ -1,22 +1,26 @@ +import { deprecate } from 'util'; import Base from '../../Base'; import AsyncLock from '../../util/AsyncLock'; import PartyMessage from './PartyMessage'; +import PartyChatConversationNotFound from '../../exceptions/PartyChatConversationNotFound'; import type Client from '../../Client'; import type ClientParty from './ClientParty'; import type ClientPartyMember from './ClientPartyMember'; +const deprecationNotOverXmppAnymore = 'Party Chat is not done over XMPP anymore, this function will be removed in a future version'; + /** * 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 + * @deprecated since chat is not done over xmpp anymore, this property will always be an empty string and will be removed in a future version */ public jid: string; /** - * the party chats conversation id + * the party chat's conversation id */ public get conversationId() { return `p-${this.party.id}`; @@ -24,13 +28,13 @@ class PartyChat extends Base { /** * The client's chat room nickname - * @deprecated since chat is not done over xmpp anymore, this property will always be an empty string + * @deprecated since chat is not done over xmpp anymore, this property will always be an empty string and will be removed in a future version */ public nick: string; /** * The chat room's join lock - * @deprecated since chat is not done over xmpp anymore, this is not used anymore + * @deprecated since chat is not done over xmpp anymore, this is not used anymore and will be removed in a future version */ public joinLock: AsyncLock; @@ -41,15 +45,10 @@ 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 + * @deprecated since chat is not done over xmpp anymore, this property will always be true and will be removed in a future version */ 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 @@ -64,21 +63,24 @@ class PartyChat extends Base { this.isConnected = true; this.party = party; - this.bannedAccountIds = new Set(); } /** * Sends a message to this party chat * @param content The message that will be sent + * @throws {PartyChatConversationNotFound} When the client is the only party member */ public async send(content: string) { + if (this.party.members.size < 2) { + throw new PartyChatConversationNotFound(); + } + 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), ); @@ -92,32 +94,36 @@ class PartyChat extends Base { /** * Joins this party chat - * @deprecated since chat is not done over xmpp anymore, this function will do nothing + * @deprecated since chat is not done over xmpp anymore, this function will do nothing and will be removed in a future version */ - // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function - public async join() { } + // eslint-disable-next-line class-methods-use-this + public async join() { + const deprecatedFn = deprecate(() => { }, deprecationNotOverXmppAnymore); + + return deprecatedFn(); + } /** * Leaves this party chat - * @deprecated since chat is not done over xmpp anymore, this function will do nothing + * @deprecated since chat is not done over xmpp anymore, this function will do nothing and will be removed in a future version */ - // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function - public async leave() { } + // eslint-disable-next-line class-methods-use-this + public async leave() { + const deprecatedFn = deprecate(() => { }, deprecationNotOverXmppAnymore); - /** - * 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) { - this.bannedAccountIds.add(member); + return deprecatedFn(); } /** - * 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); + * Ban a member from this party chat + * @param member The member that should be banned + * @deprecated since chat is not done over xmpp anymore, this function will do nothing and will be removed in a future version + */ + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars + public async ban(member: string) { + const deprecatedFn = deprecate(() => { }, deprecationNotOverXmppAnymore); + + return deprecatedFn(); } } diff --git a/src/xmpp/XMPP.ts b/src/xmpp/XMPP.ts index 973aad5a..6a5db037 100644 --- a/src/xmpp/XMPP.ts +++ b/src/xmpp/XMPP.ts @@ -1,5 +1,7 @@ +/* eslint-disable max-len */ import { createClient as createStanzaClient } from 'stanza'; import crypto from 'crypto'; +import { deprecate } from 'util'; import Base from '../Base'; import Endpoints from '../../resources/Endpoints'; import PartyMessage from '../structures/party/PartyMessage'; @@ -27,6 +29,8 @@ import XMPPConnectionError from '../exceptions/XMPPConnectionError'; import type { Stanzas, Agent, Constants } from 'stanza'; import type Client from '../Client'; +const deprecationNotOverXmppAnymore = 'Chatting is not done over XMPP anymore, this function will be removed in a future version'; + /** * Represents the client's XMPP manager * @private @@ -671,24 +675,26 @@ 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} + * @deprecated this doesn't work anymore, since chat messages are handled via an rest api now see {@link Client#chat}. This function will be removed in a future version */ public async sendMessage(to: string, content: string, type: Constants.MessageType = 'chat') { - return this.waitForSentMessage(this.connection!.sendMessage({ + const deprecatedFn = deprecate(async () => this.waitForSentMessage(this.connection!.sendMessage({ to, body: content, type, - })); + })), deprecationNotOverXmppAnymore); + + return deprecatedFn(); } /** * 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} + * @deprecated this doesn't work anymore, since chat messages are handled via an rest api now see {@link Client#chat}. This function will be removed in a future version */ public waitForSentMessage(id: string, timeout = 1000) { - return new Promise((res) => { + const deprecatedFn = deprecate(async () => new Promise((res) => { // eslint-disable-next-line no-undef let messageTimeout: NodeJS.Timeout; @@ -705,36 +711,44 @@ class XMPP extends Base { res(undefined); this.connection!.removeListener('message:sent', listener); }, timeout); - }); + }), deprecationNotOverXmppAnymore); + + return deprecatedFn(); } /** * 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} + * @deprecated this doesn't work anymore, since chat messages are handled via an rest api now see {@link Client#chat}. This function will be removed in a future version */ public async joinMUC(jid: string, nick: string) { - return this.connection!.joinRoom(jid, nick); + const deprecatedFn = deprecate(async () => this.connection!.joinRoom(jid, nick), deprecationNotOverXmppAnymore); + + return deprecatedFn(); } /** * 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} + * @deprecated this doesn't work anymore, since chat messages are handled via an rest api now see {@link Client#chat}. This function will be removed in a future version */ public async leaveMUC(jid: string, nick: string) { - return this.connection!.leaveRoom(jid, nick); + const deprecatedFn = deprecate(async () => this.connection!.leaveRoom(jid, nick), deprecationNotOverXmppAnymore); + + return deprecatedFn(); } /** * 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} + * @deprecated this doesn't work anymore, since chat messages are handled via an rest api now see {@link Client#chat}. This function will be removed in a future version */ public async ban(jid: string, member: string) { - return this.connection!.ban(jid, `${member}@${Endpoints.EPIC_PROD_ENV}`); + const deprecatedFn = deprecate(async () => this.connection!.ban(jid, `${member}@${Endpoints.EPIC_PROD_ENV}`), deprecationNotOverXmppAnymore); + + return deprecatedFn(); } } From b2c99db14ade915e50e18849bf859bfbdef28719 Mon Sep 17 00:00:00 2001 From: LeleDerGrasshalmi Date: Fri, 26 Jul 2024 18:26:16 +0200 Subject: [PATCH 8/8] fix(errors): fix missing "Error" suffix --- index.ts | 2 +- ...ionNotFound.ts => PartyChatConversationNotFoundError.ts} | 6 +++--- src/structures/party/PartyChat.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) rename src/exceptions/{PartyChatConversationNotFound.ts => PartyChatConversationNotFoundError.ts} (67%) diff --git a/index.ts b/index.ts index d888cb8c..e188c038 100644 --- a/index.ts +++ b/index.ts @@ -24,7 +24,7 @@ export { default as InviterFriendshipsLimitExceededError } from './src/exception export { default as MatchNotFoundError } from './src/exceptions/MatchNotFoundError'; export { default as OfferNotFoundError } from './src/exceptions/OfferNotFoundError'; export { default as PartyAlreadyJoinedError } from './src/exceptions/PartyAlreadyJoinedError'; -export { default as PartyChatConversationNotFound } from './src/exceptions/PartyChatConversationNotFound'; +export { default as PartyChatConversationNotFoundError } from './src/exceptions/PartyChatConversationNotFoundError'; export { default as PartyInvitationExpiredError } from './src/exceptions/PartyInvitationExpiredError'; export { default as PartyMaxSizeReachedError } from './src/exceptions/PartyMaxSizeReachedError'; export { default as PartyMemberNotFoundError } from './src/exceptions/PartyMemberNotFoundError'; diff --git a/src/exceptions/PartyChatConversationNotFound.ts b/src/exceptions/PartyChatConversationNotFoundError.ts similarity index 67% rename from src/exceptions/PartyChatConversationNotFound.ts rename to src/exceptions/PartyChatConversationNotFoundError.ts index 4c952a45..d9b87dad 100644 --- a/src/exceptions/PartyChatConversationNotFound.ts +++ b/src/exceptions/PartyChatConversationNotFoundError.ts @@ -1,12 +1,12 @@ /** * Represents an error thrown because you tried to send a party chat message, but there is no chat yet, because you are the only person in the party */ -class PartyChatConversationNotFound extends Error { +class PartyChatConversationNotFoundError extends Error { constructor() { super(); - this.name = 'PartyChatConversationNotFound'; + this.name = 'PartyChatConversationNotFoundError'; this.message = 'There is no party chat conversation yet. You cannot send party chat messages when you are the only party member.'; } } -export default PartyChatConversationNotFound; +export default PartyChatConversationNotFoundError; diff --git a/src/structures/party/PartyChat.ts b/src/structures/party/PartyChat.ts index 6e356220..f4564505 100644 --- a/src/structures/party/PartyChat.ts +++ b/src/structures/party/PartyChat.ts @@ -2,7 +2,7 @@ import { deprecate } from 'util'; import Base from '../../Base'; import AsyncLock from '../../util/AsyncLock'; import PartyMessage from './PartyMessage'; -import PartyChatConversationNotFound from '../../exceptions/PartyChatConversationNotFound'; +import PartyChatConversationNotFoundError from '../../exceptions/PartyChatConversationNotFoundError'; import type Client from '../../Client'; import type ClientParty from './ClientParty'; import type ClientPartyMember from './ClientPartyMember'; @@ -68,11 +68,11 @@ class PartyChat extends Base { /** * Sends a message to this party chat * @param content The message that will be sent - * @throws {PartyChatConversationNotFound} When the client is the only party member + * @throws {PartyChatConversationNotFoundError} When the client is the only party member */ public async send(content: string) { if (this.party.members.size < 2) { - throw new PartyChatConversationNotFound(); + throw new PartyChatConversationNotFoundError(); } const messageId = await this.client.chat.sendMessageInConversation(