diff --git a/assets/openapi.json b/assets/openapi.json index b7d25d7..56d5a2c 100644 --- a/assets/openapi.json +++ b/assets/openapi.json @@ -7512,7 +7512,8 @@ "idle", "invisible", "offline", - "online" + "online", + "unknown" ], "type": "string" }, @@ -9411,6 +9412,9 @@ }, "embedded": { "type": "string" + }, + "vr": { + "type": "string" } } }, diff --git a/assets/schemas.json b/assets/schemas.json index 55de553..82bc886 100644 --- a/assets/schemas.json +++ b/assets/schemas.json @@ -8000,7 +8000,8 @@ "idle", "invisible", "offline", - "online" + "online", + "unknown" ], "type": "string", "$schema": "http://json-schema.org/draft-07/schema#" @@ -10009,6 +10010,9 @@ }, "embedded": { "type": "string" + }, + "vr": { + "type": "string" } }, "additionalProperties": false, diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts index 149b0ed..c3f85e0 100644 --- a/src/gateway/listener/listener.ts +++ b/src/gateway/listener/listener.ts @@ -205,6 +205,13 @@ opts.acknowledge?.(); // console.log("event", event); + // deduplicate gateway messages + if (opts.transaction_id) { + if (this.recentTransactions.includes(opts.transaction_id)) return; + this.recentTransactions.push(opts.transaction_id); + if (this.recentTransactions.length > 100) this.recentTransactions = this.recentTransactions.slice(1); + } + // special codes switch (event) { case "SB_SESSION_CLOSE": diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index 012632d..62447ee 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -61,6 +61,7 @@ import { In, Not } from "typeorm"; import { PreloadedUserSettings } from "discord-protos"; import { ChannelType, DefaultUserGuildSettings, DMChannel, IdentifySchema, PrivateUserProjection, PublicUser, PublicUserProjection } from "@spacebar/schemas"; +import { randomString } from "@spacebar/api*"; // TODO: user sharding // TODO: check privileged intents, if defined in the config @@ -162,7 +163,7 @@ this.session_id = session.session_id; this.session = session; - this.session.status = identify.presence?.status || "online"; + // this.session.status = identify.presence?.status || "online"; this.session.last_seen = new Date(); this.session.client_info ??= {}; this.session.client_info.platform = identify.properties?.$device ?? identify.properties?.$device; @@ -175,6 +176,33 @@ await this.session.updateIpInfo(); } + let mustAnnouncePresence = false; + let presenceUpdateEventData: PresenceUpdateEvent | undefined; + + if (identify.presence?.status) { + let newStatus = identify.presence.status; + if (newStatus == "unknown") newStatus = this.session.status; + if (newStatus == "offline") { + newStatus = "online"; + mustAnnouncePresence = true; + } + + this.session.status = newStatus; + if (mustAnnouncePresence) { + presenceUpdateEventData = { + event: "PRESENCE_UPDATE", + data: { + user: tokenData.user.toPublicUser(), + status: this.session.status, + client_status: this.session.client_status, + activities: this.session.activities, + }, + origin: "GATEWAY_IDENTIFY", + transaction_id: `IDENT_${this.user_id}_${randomString()}`, + } satisfies PresenceUpdateEvent; + } + } + const createSessionTime = taskSw.getElapsedAndReset(); // Get from database: @@ -849,4 +877,30 @@ `[Gateway/${this.user_id}] IDENTIFY ${this.user_id} in ${totalSw.elapsed().totalMilliseconds}ms`, process.env.LOG_GATEWAY_TRACES ? JSON.stringify(d._trace, null, 2) : "", ); + + // actually send presence updates + if (presenceUpdateEventData) { + for (const rel of d.relationships ?? []) { + await emitEvent({ + ...presenceUpdateEventData, + user_id: rel.user.id, + }); + } + for (const guild of d.guilds) { + await emitEvent({ + ...presenceUpdateEventData, + guild_id: guild.id, + }); + } + for (const dmChannel of d.private_channels) { + // TODO: check if other side has the channel still open + for (const recpt of dmChannel.recipients) { + if (recpt.id != this.user_id) + await emitEvent({ + ...presenceUpdateEventData, + user_id: recpt.id, + }); + } + } + } } diff --git a/src/gateway/util/SessionUtils.ts b/src/gateway/util/SessionUtils.ts index 9d2f01b..24e2c99 100644 --- a/src/gateway/util/SessionUtils.ts +++ b/src/gateway/util/SessionUtils.ts @@ -37,6 +37,7 @@ dnd: 2, invisible: 3, offline: 4, + unknown: 5, }; // sort sessions by relevance sessions = sessions.sort((a, b) => { diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts index 301b051..41dc93c 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts @@ -24,6 +24,7 @@ import { QoSPayload } from "../opcodes/Heartbeat"; export interface WebSocket extends WS { + recentTransactions: string[]; version: number; user_id: string; session_id: string; diff --git a/src/util/interfaces/Event.ts b/src/util/interfaces/Event.ts index 7f7b017..63e206f 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -64,6 +64,7 @@ data?: any; reconnect_delay?: number; origin?: string; + transaction_id?: string; } // ! Custom Events that shouldn't get sent to the client but processed by the server diff --git a/src/util/interfaces/Status.ts b/src/util/interfaces/Status.ts index 4ee0b84..8edd379 100644 --- a/src/util/interfaces/Status.ts +++ b/src/util/interfaces/Status.ts @@ -16,11 +16,20 @@ along with this program. If not, see . */ -export type Status = "idle" | "dnd" | "online" | "offline" | "invisible"; +export type Status = + | "idle" + | "dnd" + | "online" + | "offline" + // Send only + | "invisible" + // Identify only + | "unknown"; export interface ClientStatus { desktop?: string; // e.g. Windows/Linux/Mac mobile?: string; // e.g. iOS/Android web?: string; // e.g. browser, bot account, unknown embedded?: string; // e.g. embedded + vr?: string; }