fix(federation): create attachment rows immediately in processCreateEvent
Insert attachment rows with sourceUrl as interim filename so the initial WebSocket broadcast includes working remote URLs. AttachmentRenderer already handles http:// filenames as direct URLs. The file worker will UPDATE the filename to the local path after download completes. This fixes the invisible bubble bug where attachment-only messages (content: null, attachments: []) appeared as empty messages.
This commit is contained in:
@@ -10,7 +10,7 @@ import { connectionManager } from '../ws/handler.js';
|
|||||||
import { sanitizeUser } from '../utils/sanitize.js';
|
import { sanitizeUser } from '../utils/sanitize.js';
|
||||||
import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
|
import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
|
||||||
import { canonicalDmPairId, getDmParticipants } from '../utils/federationOutbox.js';
|
import { canonicalDmPairId, getDmParticipants } from '../utils/federationOutbox.js';
|
||||||
import { broadcastDmMessage } from './dm.js';
|
import { broadcastDmMessage, getDmMessageWithUser } from './dm.js';
|
||||||
import type { FederationRelayRequest, FederationRelayResponse, FederationRelayEvent, FederationRelayAttachment, FederationSyncRequest, FederationSyncResponse, DmMessageWithUser } from '@backspace/shared';
|
import type { FederationRelayRequest, FederationRelayResponse, FederationRelayEvent, FederationRelayAttachment, FederationSyncRequest, FederationSyncResponse, DmMessageWithUser } from '@backspace/shared';
|
||||||
|
|
||||||
/** Fields safe to expose to admin callers (everything except hmacSecret). */
|
/** Fields safe to expose to admin callers (everything except hmacSecret). */
|
||||||
@@ -1022,7 +1022,10 @@ function processCreateEvent(
|
|||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
// Queue attachment downloads (SSRF-validated)
|
// Create attachment rows and queue file downloads (SSRF-validated).
|
||||||
|
// Attachment rows are created immediately with filename = sourceUrl so the
|
||||||
|
// initial WebSocket broadcast includes working remote URLs. The background
|
||||||
|
// file worker will UPDATE the filename to the local path after download.
|
||||||
if (event.message.attachments && event.message.attachments.length > 0) {
|
if (event.message.attachments && event.message.attachments.length > 0) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const attachment of event.message.attachments) {
|
for (const attachment of event.message.attachments) {
|
||||||
@@ -1033,6 +1036,30 @@ function processCreateEvent(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create the attachment row with sourceUrl as the interim filename.
|
||||||
|
// AttachmentRenderer already handles filenames starting with 'http' —
|
||||||
|
// it uses them as direct URLs. When the file worker downloads the file,
|
||||||
|
// it updates this row's filename to the local path.
|
||||||
|
const attachmentId = generateSnowflake();
|
||||||
|
db.insert(schema.attachments)
|
||||||
|
.values({
|
||||||
|
id: attachmentId,
|
||||||
|
dmMessageId: localMessageId,
|
||||||
|
uploaderId: null,
|
||||||
|
filename: attachment.sourceUrl,
|
||||||
|
originalName: attachment.originalName,
|
||||||
|
mimetype: attachment.mimetype,
|
||||||
|
size: attachment.size,
|
||||||
|
width: attachment.width ?? null,
|
||||||
|
height: attachment.height ?? null,
|
||||||
|
duration: attachment.duration ?? null,
|
||||||
|
thumbnailFilename: attachment.thumbnailFilename ?? null,
|
||||||
|
sourceUrl: attachment.sourceUrl,
|
||||||
|
createdAt: now,
|
||||||
|
})
|
||||||
|
.run();
|
||||||
|
|
||||||
|
// Queue the background file download
|
||||||
db.insert(schema.federationFileQueue)
|
db.insert(schema.federationFileQueue)
|
||||||
.values({
|
.values({
|
||||||
id: generateSnowflake(),
|
id: generateSnowflake(),
|
||||||
@@ -1051,20 +1078,12 @@ function processCreateEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broadcast to local WebSocket clients
|
// Broadcast to local WebSocket clients — use getDmMessageWithUser to pick up
|
||||||
const messagePayload = buildDmMessagePayload(
|
// the attachment rows we just created (with remote URLs as filenames)
|
||||||
{
|
const fullMessage = getDmMessageWithUser(localMessageId);
|
||||||
id: localMessageId,
|
if (fullMessage) {
|
||||||
dmChannelId: localDmChannelId,
|
broadcastDmMessage(localDmChannelId, fullMessage);
|
||||||
userId: authorUser.id,
|
}
|
||||||
content: event.message.content,
|
|
||||||
replyToId: null,
|
|
||||||
editedAt: null,
|
|
||||||
createdAt: event.message.createdAt,
|
|
||||||
},
|
|
||||||
authorUser,
|
|
||||||
);
|
|
||||||
broadcastDmMessage(localDmChannelId, messagePayload);
|
|
||||||
|
|
||||||
accepted.push(event.messageId);
|
accepted.push(event.messageId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user