From ad55d0aa93be97902623abdada4266845980290e Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 13 Apr 2026 15:42:44 +0200 Subject: [PATCH] prep for presence --- src/gateway/opcodes/Identify.ts | 10 ++-- src/gateway/util/SessionUtils.ts | 19 -------- src/util/util/Presence.ts | 78 ++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 src/util/util/Presence.ts diff --git a/src/gateway/opcodes/Identify.ts b/src/gateway/opcodes/Identify.ts index 62447ee38..8939f821a 100644 --- a/src/gateway/opcodes/Identify.ts +++ b/src/gateway/opcodes/Identify.ts @@ -18,8 +18,8 @@ import { Capabilities, CLOSECODES, OPCODES, Payload, Send, setupListener, WebSocket } from "@spacebar/gateway"; import { - arrayGroupBy, Application, + arrayGroupBy, Channel, checkToken, Config, @@ -60,8 +60,8 @@ import { 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*"; +import { ChannelType, DefaultUserGuildSettings, DMChannel, IdentifySchema, PrivateUserProjection, PublicUser, PublicUserProjection, RelationshipType } from "@spacebar/schemas"; +import { randomString } from "@spacebar/api"; // TODO: user sharding // TODO: check privileged intents, if defined in the config @@ -193,7 +193,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { event: "PRESENCE_UPDATE", data: { user: tokenData.user.toPublicUser(), - status: this.session.status, + status: this.session.getPublicStatus(), client_status: this.session.client_status, activities: this.session.activities, }, @@ -878,7 +878,7 @@ export async function onIdentify(this: WebSocket, data: Payload) { process.env.LOG_GATEWAY_TRACES ? JSON.stringify(d._trace, null, 2) : "", ); - // actually send presence updates + // actually send presence updates - not using distributePresenceUpdate because we already have all of the data at hand if (presenceUpdateEventData) { for (const rel of d.relationships ?? []) { await emitEvent({ diff --git a/src/gateway/util/SessionUtils.ts b/src/gateway/util/SessionUtils.ts index 24e2c99eb..843341cc5 100644 --- a/src/gateway/util/SessionUtils.ts +++ b/src/gateway/util/SessionUtils.ts @@ -16,8 +16,6 @@ along with this program. If not, see . */ -import { Session } from "@spacebar/util"; - export function genSessionId() { return genRanHex(32); } @@ -29,20 +27,3 @@ export function genVoiceToken() { function genRanHex(size: number) { return [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join(""); } - -export function getMostRelevantSession(sessions: Session[]) { - const statusMap = { - online: 0, - idle: 1, - dnd: 2, - invisible: 3, - offline: 4, - unknown: 5, - }; - // sort sessions by relevance - sessions = sessions.sort((a, b) => { - return statusMap[a.status] - statusMap[b.status] + ((a.activities?.length ?? 0) - (b.activities?.length ?? 0)) * 2; - }); - - return sessions[0]; -} diff --git a/src/util/util/Presence.ts b/src/util/util/Presence.ts new file mode 100644 index 000000000..6703344f2 --- /dev/null +++ b/src/util/util/Presence.ts @@ -0,0 +1,78 @@ +/* + Spacebar: A FOSS re-implementation and extension of the Discord.com backend. + Copyright (C) 2026 Spacebar and Spacebar Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { emitEvent, Member, PresenceUpdateEvent, Recipient, Relationship, Session } from "@spacebar/util"; +import { RelationshipType } from "@spacebar/schemas"; +import { Not } from "typeorm"; + +export function getMostRelevantSession(sessions: Session[]) { + const statusMap = { + online: 0, + idle: 1, + dnd: 2, + invisible: 3, + offline: 4, + unknown: 5, + }; + // sort sessions by relevance + sessions = sessions.sort((a, b) => { + return statusMap[a.status] - statusMap[b.status] + ((a.activities?.length ?? 0) - (b.activities?.length ?? 0)) * 2; + }); + + return sessions[0]; +} + +export async function distributePresenceUpdate(userId: string, data: PresenceUpdateEvent) { + let relationships: Relationship[] | undefined = await Relationship.find({ + where: { from_id: userId, type: RelationshipType.friends }, + select: { from_id: true, to_id: true }, + }); + for (const rel of relationships) + await emitEvent({ + ...data, + user_id: rel.to_id, + }); + // noinspection JSUnusedAssignment - drop array ref + relationships = undefined; + + let memberGuildIds: string[] | undefined = ( + await Member.find({ + where: { id: userId }, + select: { guild_id: true }, + }) + ).map((x) => x.guild_id); + for (const rel of memberGuildIds) + await emitEvent({ + ...data, + guild_id: rel, + }); + // noinspection JSUnusedAssignment - drop array ref + memberGuildIds = undefined; + + const recipients = await Recipient.find({ where: { user_id: userId, closed: false }, relations: { channel: true } }); + for (const recipient of recipients) { + const otherRecipients = await Recipient.find({ where: { user_id: Not(userId), channel_id: recipient.channel_id } }); + for (const otherRcpt of otherRecipients) { + if (otherRcpt.closed) continue; + await emitEvent({ + ...data, + user_id: otherRcpt.user_id, + }); + } + } +}