diff --git a/packages/server/src/routes/dm.ts b/packages/server/src/routes/dm.ts index 150e6581..8cf3b66b 100644 --- a/packages/server/src/routes/dm.ts +++ b/packages/server/src/routes/dm.ts @@ -44,6 +44,7 @@ import { isFederationRelayEnabled, computeFederatedId, sendTypingRelay, + normalizeIconForWire, } from '../utils/federationOutbox.js'; import { getOurOrigin } from '../utils/federationAuth.js'; import type { FederationRelayEvent } from '@backspace/shared'; @@ -1301,11 +1302,7 @@ export async function dmRoutes(app: FastifyInstance): Promise { // relay) stay correct without touching this site. const channelRow = db.select().from(schema.dmChannels) .where(eq(schema.dmChannels.id, dmChannelId)).get(); - const wireIcon = channelRow?.icon - ? (channelRow.icon.startsWith('http://') || channelRow.icon.startsWith('https://') - ? channelRow.icon - : `${domainOrigin}/api/uploads/${channelRow.icon}`) - : null; + const wireIcon = normalizeIconForWire(channelRow?.icon ?? null, domainOrigin); for (const targetUser of targetUsers) { if (!targetUser.homeInstance || targetUser.homeInstance === domainOrigin) continue; @@ -1864,11 +1861,7 @@ export async function dmRoutes(app: FastifyInstance): Promise { // patched between the start of this handler and now. const channelRow = db.select().from(schema.dmChannels) .where(eq(schema.dmChannels.id, id)).get(); - const wireIcon = channelRow?.icon - ? (channelRow.icon.startsWith('http://') || channelRow.icon.startsWith('https://') - ? channelRow.icon - : `${domainOrigin}/api/uploads/${channelRow.icon}`) - : null; + const wireIcon = normalizeIconForWire(channelRow?.icon ?? null, domainOrigin); const memberAddPayload: FederationRelayEvent = { eventType: 'member_add', diff --git a/packages/server/src/utils/federationOutbox.ts b/packages/server/src/utils/federationOutbox.ts index 07dc56b2..c0f56ba4 100644 --- a/packages/server/src/utils/federationOutbox.ts +++ b/packages/server/src/utils/federationOutbox.ts @@ -47,6 +47,23 @@ function fetchSettings(): CachedSettings { // ─── Public API ────────────────────────────────────────────────────────────── +/** + * Normalize a stored icon value (bare filename, absolute http(s) URL, or null) + * to an absolute URL suitable for federation wire payloads. Bare filenames are + * resolved against `ourOrigin`; absolute URLs pass through unchanged; null + * short-circuits. + * + * @param icon Either a bare filename (e.g. "1234567890.png"), an + * already-absolute http(s) URL, or null. + * @param ourOrigin Owner-instance origin (e.g. "https://nova.ddns.net"). + * @returns Absolute URL or null. + */ +export function normalizeIconForWire(icon: string | null, ourOrigin: string): string | null { + if (!icon) return null; + if (icon.startsWith('http://') || icon.startsWith('https://')) return icon; + return `${ourOrigin}/api/uploads/${icon}`; +} + /** * Returns true if the federation relay feature is enabled in instance settings. * Result is cached for 30 seconds to avoid repeated DB reads. @@ -884,9 +901,7 @@ export function queueGroupMetadataRelay( } const ourOrigin = getOurOrigin(); - const wireIcon = payload.icon - ? ((payload.icon.startsWith('http://') || payload.icon.startsWith('https://')) ? payload.icon : `${ourOrigin}/api/uploads/${payload.icon}`) - : null; + const wireIcon = normalizeIconForWire(payload.icon, ourOrigin); // Resolve actor's full FederationRelayParticipant. Mirrors the participant // construction in getDmParticipants() / buildRelayPayload() so receivers