From 0ae2ccff5f560a0eadd400f300043f7581150645 Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 13 Apr 2026 14:08:29 +0200 Subject: [PATCH] Emit presence on identify, support deduped gw events --- assets/openapi.json | Bin 966601 -> 966724 bytes assets/schemas.json | Bin 430516 -> 430607 bytes src/gateway/listener/listener.ts | 7 ++++ src/gateway/opcodes/Identify.ts | 56 ++++++++++++++++++++++++++++++- src/gateway/util/SessionUtils.ts | 1 + src/gateway/util/WebSocket.ts | 1 + src/util/interfaces/Event.ts | 1 + src/util/interfaces/Status.ts | 11 +++++- 8 files changed, 75 insertions(+), 2 deletions(-) diff --git a/assets/openapi.json b/assets/openapi.json index b7d25d70ef3b77593dc4f2a1c43809d18d0d5ae8..56d5a2cd142185637a78c40fc83375d85cd5618c 100644 GIT binary patch delta 116 zcmX^4-|9$%bwdkd3sVd878V!T>1whp;%udP*?IZplNtXDPj`so5}oY8D?Oc$pG9!{ zG+CC(|E3FUVV9UZ@si7A0Y4!i=AFD?n?QSn3@Z?`0Wmuea{w_X5OV=BHxTmxG4J*W I8NR~B0Qv1DA^-pY delta 89 zcmX@oV14qxRYMD73sVd878V!T>HjV;3Qjk0Zd&YTErn OS%8>zyMHL#0UH3BEfs_S diff --git a/src/gateway/listener/listener.ts b/src/gateway/listener/listener.ts index 149b0edb7..c3f85e01a 100644 --- a/src/gateway/listener/listener.ts +++ b/src/gateway/listener/listener.ts @@ -205,6 +205,13 @@ async function consume(this: WebSocket, opts: EventOpts) { 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 012632d39..62447ee38 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -61,6 +61,7 @@ import { check } from "./instanceOf"; 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 @@ export async function onIdentify(this: WebSocket, data: Payload) { 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 @@ export async function onIdentify(this: WebSocket, data: Payload) { 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 @@ export async function onIdentify(this: WebSocket, data: Payload) { `[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 9d2f01b33..24e2c99eb 100644 --- a/src/gateway/util/SessionUtils.ts +++ b/src/gateway/util/SessionUtils.ts @@ -37,6 +37,7 @@ export function getMostRelevantSession(sessions: Session[]) { 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 301b0515d..41dc93c11 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts @@ -24,6 +24,7 @@ import { Decoder, Encoder } from "@toondepauw/node-zstd"; 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 7f7b017fc..63e206f37 100644 --- a/src/util/interfaces/Event.ts +++ b/src/util/interfaces/Event.ts @@ -64,6 +64,7 @@ export interface Event { 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 4ee0b843d..8edd379c1 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; }