diff --git a/assets/openapi.json b/assets/openapi.json index b7d25d70e..56d5a2cd1 100644 Binary files a/assets/openapi.json and b/assets/openapi.json differ diff --git a/assets/schemas.json b/assets/schemas.json index 55de55381..82bc8860f 100644 Binary files a/assets/schemas.json and b/assets/schemas.json differ 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; }