From 8ded2d5c9046b5064958b3cc7e056e0f25f8d205 Mon Sep 17 00:00:00 2001 From: Sivert Date: Mon, 5 Oct 2026 14:32:30 +0200 Subject: [PATCH] Tell the uploader when the worker refuses their avatar or banner (GRYT-1667) Co-Authored-By: Claude Opus 5.5 --- src/routes/uploads.ts | 8 ++++++-- src/socket/utils/emitToMember.test.ts | 25 +++++++++++++++++++++++++ src/socket/utils/server.ts | 8 ++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/socket/utils/emitToMember.test.ts diff --git a/src/routes/uploads.ts b/src/routes/uploads.ts index 86eab7bb..8c5964a3 100644 --- a/src/routes/uploads.ts +++ b/src/routes/uploads.ts @@ -20,7 +20,7 @@ import { sendStoredBody } from "../utils/sendStoredBody"; import { ensurePermission } from "../middleware/requirePermission"; import { sanitizeSvg } from "../utils/svgSanitize"; import { quarantineUpload, settleQuarantine, waitOutOfQuarantine } from "../services/quarantineUpload"; -import { broadcastMembersUpdate } from "../socket/utils/server"; +import { broadcastMembersUpdate, emitToMember } from "../socket/utils/server"; /** How long a read waits for the worker to write a fresh upload out; a test sets it short. */ const QUARANTINE_WAIT_MS = Number(process.env.GRYT_QUARANTINE_WAIT_MS) || 15_000; @@ -41,7 +41,11 @@ async function applyWhenSettled(purpose: "avatar" | "banner", serverUserId: stri pendingMedia.delete(slot); if (verdict !== "ready") { // A timeout leaves it in quarantine for the worker to finish; a refusal goes now. - if (verdict === "refused") await deleteUnreferencedFiles([fileId]); + if (verdict === "refused") { + await deleteUnreferencedFiles([fileId]); + // Without this the uploader sees nothing happen (GRYT-1667). The words live in the client. + emitToMember(serverUserId, "profile:media-refused", { purpose, fileId }); + } consola.info(`[uploads] ${purpose} ${fileId} not applied: ${verdict}`); return; } diff --git a/src/socket/utils/emitToMember.test.ts b/src/socket/utils/emitToMember.test.ts new file mode 100644 index 00000000..12effc1b --- /dev/null +++ b/src/socket/utils/emitToMember.test.ts @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import type { Server } from "socket.io"; + +import type { Clients } from "../../types"; +import { emitToMember, setSocketRefs } from "./server"; + +/* GRYT-1667: a refused avatar is told to the person who uploaded it, on every socket they have open, and nobody else. */ +describe("emitToMember", () => { + it("reaches each of that member's sockets and no one else's", () => { + const got: string[] = []; + const socket = (id: string) => ({ emit: (event: string, payload: unknown) => got.push(`${id} ${event} ${JSON.stringify(payload)}`) }); + const sockets = new Map([["a1", socket("a1")], ["a2", socket("a2")], ["b1", socket("b1")]]); + const io = { sockets: { sockets } } as unknown as Server; + const clients = { a1: { serverUserId: "alice" }, a2: { serverUserId: "alice" }, b1: { serverUserId: "bob" } } as unknown as Clients; + setSocketRefs(io, "srv", clients); + + emitToMember("alice", "profile:media-refused", { purpose: "avatar" }); + assert.deepEqual(got.sort(), [ + 'a1 profile:media-refused {"purpose":"avatar"}', + 'a2 profile:media-refused {"purpose":"avatar"}', + ]); + }); +}); diff --git a/src/socket/utils/server.ts b/src/socket/utils/server.ts index 28b09643..0988bd82 100644 --- a/src/socket/utils/server.ts +++ b/src/socket/utils/server.ts @@ -66,6 +66,14 @@ export function broadcastServerUiUpdate(reason: "settings" | "icon" | "other" = void pushVoiceCapabilities(); } +/** To every socket one member has open, from a REST route that has no socket of its own. */ +export function emitToMember(serverUserId: string, event: string, payload: unknown): void { + if (!_io || !_clientsInfo) return; + for (const [clientId, ci] of Object.entries(_clientsInfo)) { + if (ci.serverUserId === serverUserId) _io.sockets.sockets.get(clientId)?.emit(event, payload); + } +} + /** For a REST route that changed a member, such as an avatar the worker has just cleared. */ export function broadcastMembersUpdate(): void { if (!_io || !_serverId || !_clientsInfo) return;