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,
+ });
+ }
+ }
+}