feat: wire embeds into all message query and creation paths

Integrate embed infrastructure into the complete message flow:
- messages.ts: batch-fetch embeds in GET, resolve on POST, re-resolve on PATCH
- dm.ts: same pattern for DM messages with isDm=true
- search.ts: include embeds in all 4 search/around endpoints
- events.ts: embed resolution in WS message create/edit for both space and DM
- Fix embedClassifier.ts type errors (regex match undefined → null)
- Add embeds: [] to all inline MessageWithUser/DmMessageWithUser constructions
- Add embeds: [] to chatStore optimistic message
This commit is contained in:
Jannis Braun
2026-03-20 23:41:53 +01:00
parent 5fe25fb077
commit 01ee7ab67a
6 changed files with 93 additions and 13 deletions
+29 -1
View File
@@ -5,9 +5,10 @@ import { connectionManager } from './handler.js';
import type { VoiceRoom, DmRoomMeta, SpaceRoomMeta } from './handler.js';
import { isMember, getChannelSpaceId, isDmMember, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js';
import { broadcastDmMessage, getDmMessageWithUser } from '../routes/dm.js';
import { MAX_MESSAGE_LENGTH, type MessageWithUser, type Attachment, type DmMessageWithUser } from '@backspace/shared';
import { MAX_MESSAGE_LENGTH, type MessageWithUser, type Attachment, type DmMessageWithUser, type Embed } from '@backspace/shared';
import { sanitizeUser } from '../utils/sanitize.js';
import { deleteAttachmentFiles } from '../utils/fileCleanup.js';
import { resolveEmbeds, reResolveEmbeds, embedRowToEmbed } from '../utils/embedResolver.js';
/**
* Re-evaluate SPEAK permission for all participants in voice channels
@@ -76,6 +77,11 @@ function getMessageWithUser(messageId: string): MessageWithUser | null {
createdAt: r.createdAt,
}));
const embedRows = db.select()
.from(schema.embeds)
.where(eq(schema.embeds.messageId, messageId))
.all();
let replyTo: MessageWithUser | null = null;
if (message.replyToId) {
// Simple fetch for replyTo (one level deep to avoid recursion loops)
@@ -93,6 +99,7 @@ function getMessageWithUser(messageId: string): MessageWithUser | null {
createdAt: replyMsg.createdAt,
user: sanitizeUser(replyUser),
attachments: [], // Don't fetch attachments for replies to save bandwidth
embeds: [],
reactions: [], // Don't fetch reactions for replies
};
}
@@ -109,6 +116,7 @@ function getMessageWithUser(messageId: string): MessageWithUser | null {
createdAt: message.createdAt,
user: sanitizeUser(user),
attachments,
embeds: embedRows.map(e => embedRowToEmbed(e)),
reactions,
replyTo,
};
@@ -257,6 +265,11 @@ function handleMessageCreate(event: Record<string, unknown>, userId: string): vo
type: 'message_created',
message: messageWithUser,
});
// Resolve embeds asynchronously
setImmediate(() => {
resolveEmbeds(messageId, content.trim(), channelId, false, spaceId).catch(() => {});
});
}
}
@@ -306,6 +319,11 @@ function handleMessageEdit(event: Record<string, unknown>, userId: string): void
type: 'message_updated',
message: updatedMessage,
});
// Re-resolve embeds asynchronously
setImmediate(() => {
reResolveEmbeds(messageId, content.trim(), message.channelId, false, spaceId).catch(() => {});
});
}
}
@@ -763,6 +781,11 @@ function handleDmMessageCreate(event: Record<string, unknown>, userId: string):
// Broadcast to all DM members (including those who closed the channel)
broadcastDmMessage(dmChannelId, dmMessage);
// Resolve embeds asynchronously
setImmediate(() => {
resolveEmbeds(messageId, hasContent ? content!.trim() : null, dmChannelId, true, null).catch(() => {});
});
}
function handleDmTypingStart(event: Record<string, unknown>, userId: string, username: string): void {
@@ -853,6 +876,11 @@ function handleDmMessageEdit(event: Record<string, unknown>, userId: string): vo
message: updated,
});
}
// Re-resolve embeds asynchronously
setImmediate(() => {
reResolveEmbeds(messageId, content.trim(), msg.dmChannelId, true, null).catch(() => {});
});
}
function handleDmMessageDelete(event: Record<string, unknown>, userId: string): void {