Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/routes/uploads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down
25 changes: 25 additions & 0 deletions src/socket/utils/emitToMember.test.ts
Original file line number Diff line number Diff line change
@@ -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"}',
]);
});
});
8 changes: 8 additions & 0 deletions src/socket/utils/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading