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:
@@ -16,9 +16,11 @@ import {
|
|||||||
type PaginatedQuery,
|
type PaginatedQuery,
|
||||||
type Attachment,
|
type Attachment,
|
||||||
type Reaction,
|
type Reaction,
|
||||||
|
type Embed,
|
||||||
} from '@backspace/shared';
|
} from '@backspace/shared';
|
||||||
import { sanitizeUser } from '../utils/sanitize.js';
|
import { sanitizeUser } from '../utils/sanitize.js';
|
||||||
import { deleteUploadFile, deleteAttachmentFiles } from '../utils/fileCleanup.js';
|
import { deleteUploadFile, deleteAttachmentFiles } from '../utils/fileCleanup.js';
|
||||||
|
import { fetchDmEmbedsForMessages, resolveEmbeds, reResolveEmbeds, embedRowToEmbed } from '../utils/embedResolver.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch-fetch reactions for a set of DM message IDs.
|
* Batch-fetch reactions for a set of DM message IDs.
|
||||||
@@ -67,6 +69,7 @@ export function buildDmMessageWithUser(
|
|||||||
attachmentRows: (typeof schema.attachments.$inferSelect)[],
|
attachmentRows: (typeof schema.attachments.$inferSelect)[],
|
||||||
reactions: Reaction[] = [],
|
reactions: Reaction[] = [],
|
||||||
replyTo: DmMessageWithUser | null = null,
|
replyTo: DmMessageWithUser | null = null,
|
||||||
|
embedRows: (typeof schema.embeds.$inferSelect)[] = [],
|
||||||
): DmMessageWithUser {
|
): DmMessageWithUser {
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
@@ -87,6 +90,7 @@ export function buildDmMessageWithUser(
|
|||||||
thumbnailFilename: a.thumbnailFilename ?? null,
|
thumbnailFilename: a.thumbnailFilename ?? null,
|
||||||
createdAt: a.createdAt,
|
createdAt: a.createdAt,
|
||||||
})),
|
})),
|
||||||
|
embeds: embedRows.map(e => embedRowToEmbed(e)),
|
||||||
reactions,
|
reactions,
|
||||||
replyTo,
|
replyTo,
|
||||||
};
|
};
|
||||||
@@ -108,6 +112,11 @@ export function getDmMessageWithUser(dmMessageId: string): DmMessageWithUser | n
|
|||||||
.where(eq(schema.attachments.dmMessageId, dmMessageId))
|
.where(eq(schema.attachments.dmMessageId, dmMessageId))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
|
const embedRows = db.select()
|
||||||
|
.from(schema.embeds)
|
||||||
|
.where(eq(schema.embeds.dmMessageId, dmMessageId))
|
||||||
|
.all();
|
||||||
|
|
||||||
const reactionsMap = fetchDmReactionsForMessages([dmMessageId]);
|
const reactionsMap = fetchDmReactionsForMessages([dmMessageId]);
|
||||||
const reactions = reactionsMap.get(dmMessageId) ?? [];
|
const reactions = reactionsMap.get(dmMessageId) ?? [];
|
||||||
|
|
||||||
@@ -127,13 +136,14 @@ export function getDmMessageWithUser(dmMessageId: string): DmMessageWithUser | n
|
|||||||
createdAt: replyMsg.createdAt,
|
createdAt: replyMsg.createdAt,
|
||||||
user: sanitizeUser(replyUser),
|
user: sanitizeUser(replyUser),
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
reactions: [],
|
reactions: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildDmMessageWithUser(message, user, attachmentRows, reactions, replyTo);
|
return buildDmMessageWithUser(message, user, attachmentRows, reactions, replyTo, embedRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -830,6 +840,9 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
// Batch fetch reactions
|
// Batch fetch reactions
|
||||||
const reactionsMap = fetchDmReactionsForMessages(messageIds);
|
const reactionsMap = fetchDmReactionsForMessages(messageIds);
|
||||||
|
|
||||||
|
// Batch fetch embeds
|
||||||
|
const embedMap = fetchDmEmbedsForMessages(messageIds);
|
||||||
|
|
||||||
// Batch fetch reply-to messages
|
// Batch fetch reply-to messages
|
||||||
const replyToIds = messageRows
|
const replyToIds = messageRows
|
||||||
.map(m => m.replyToId)
|
.map(m => m.replyToId)
|
||||||
@@ -860,6 +873,7 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
createdAt: rm.createdAt,
|
createdAt: rm.createdAt,
|
||||||
user: sanitizeUser(rUser),
|
user: sanitizeUser(rUser),
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
reactions: [],
|
reactions: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -871,7 +885,7 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const reactions = reactionsMap.get(m.id) ?? [];
|
const reactions = reactionsMap.get(m.id) ?? [];
|
||||||
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
||||||
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
|
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
|
||||||
})
|
})
|
||||||
.filter((m): m is DmMessageWithUser => m !== null);
|
.filter((m): m is DmMessageWithUser => m !== null);
|
||||||
|
|
||||||
@@ -953,6 +967,11 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
// Broadcast to all DM members (including those who closed the channel)
|
// Broadcast to all DM members (including those who closed the channel)
|
||||||
broadcastDmMessage(id, message);
|
broadcastDmMessage(id, message);
|
||||||
|
|
||||||
|
// Resolve embeds asynchronously after responding
|
||||||
|
setImmediate(() => {
|
||||||
|
resolveEmbeds(messageId, content?.trim() || null, id, true, null).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
return reply.code(201).send(message);
|
return reply.code(201).send(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1006,6 +1025,11 @@ export async function dmRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-resolve embeds asynchronously after responding
|
||||||
|
setImmediate(() => {
|
||||||
|
reResolveEmbeds(id, content.trim(), msg.dmChannelId, true, null).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
return reply.code(200).send(updated);
|
return reply.code(200).send(updated);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ import {
|
|||||||
type PaginatedQuery,
|
type PaginatedQuery,
|
||||||
type MessageWithUser,
|
type MessageWithUser,
|
||||||
type Reaction,
|
type Reaction,
|
||||||
|
type Embed,
|
||||||
} from '@backspace/shared';
|
} from '@backspace/shared';
|
||||||
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 { fetchEmbedsForMessages, resolveEmbeds, reResolveEmbeds, embedRowToEmbed } from '../utils/embedResolver.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch reactions for a set of message IDs.
|
* Fetch reactions for a set of message IDs.
|
||||||
@@ -114,6 +116,7 @@ export function fetchReplyToMessages(messages: (typeof schema.messages.$inferSel
|
|||||||
size: a.size,
|
size: a.size,
|
||||||
createdAt: a.createdAt,
|
createdAt: a.createdAt,
|
||||||
})),
|
})),
|
||||||
|
embeds: [],
|
||||||
reactions: [],
|
reactions: [],
|
||||||
replyTo: null,
|
replyTo: null,
|
||||||
});
|
});
|
||||||
@@ -127,6 +130,7 @@ export function buildMessageWithUser(
|
|||||||
attachmentRows: (typeof schema.attachments.$inferSelect)[],
|
attachmentRows: (typeof schema.attachments.$inferSelect)[],
|
||||||
reactions: Reaction[] = [],
|
reactions: Reaction[] = [],
|
||||||
replyTo: MessageWithUser | null = null,
|
replyTo: MessageWithUser | null = null,
|
||||||
|
embedRows: (typeof schema.embeds.$inferSelect)[] = [],
|
||||||
): MessageWithUser {
|
): MessageWithUser {
|
||||||
return {
|
return {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
@@ -147,6 +151,7 @@ export function buildMessageWithUser(
|
|||||||
thumbnailFilename: a.thumbnailFilename ?? null,
|
thumbnailFilename: a.thumbnailFilename ?? null,
|
||||||
createdAt: a.createdAt,
|
createdAt: a.createdAt,
|
||||||
})),
|
})),
|
||||||
|
embeds: embedRows.map(e => embedRowToEmbed(e)),
|
||||||
reactions,
|
reactions,
|
||||||
replyTo,
|
replyTo,
|
||||||
};
|
};
|
||||||
@@ -224,6 +229,9 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
// Batch fetch reactions for all messages
|
// Batch fetch reactions for all messages
|
||||||
const reactionsMap = fetchReactionsForMessages(messageIds);
|
const reactionsMap = fetchReactionsForMessages(messageIds);
|
||||||
|
|
||||||
|
// Batch fetch embeds for all messages
|
||||||
|
const embedMap = fetchEmbedsForMessages(messageIds);
|
||||||
|
|
||||||
// Batch fetch reply-to messages
|
// Batch fetch reply-to messages
|
||||||
const replyToMap = fetchReplyToMessages(messageRows);
|
const replyToMap = fetchReplyToMessages(messageRows);
|
||||||
|
|
||||||
@@ -233,7 +241,7 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const reactions = reactionsMap.get(m.id) ?? [];
|
const reactions = reactionsMap.get(m.id) ?? [];
|
||||||
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
||||||
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
|
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
|
||||||
})
|
})
|
||||||
.filter((m): m is MessageWithUser => m !== null);
|
.filter((m): m is MessageWithUser => m !== null);
|
||||||
|
|
||||||
@@ -348,6 +356,11 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
message: messageWithUser,
|
message: messageWithUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Resolve embeds asynchronously after responding
|
||||||
|
setImmediate(() => {
|
||||||
|
resolveEmbeds(messageId, content?.trim() || null, id, false, spaceId).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
return reply.code(201).send(messageWithUser);
|
return reply.code(201).send(messageWithUser);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -397,16 +410,18 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
.where(eq(schema.attachments.messageId, id))
|
.where(eq(schema.attachments.messageId, id))
|
||||||
.all();
|
.all();
|
||||||
|
|
||||||
// Hydrate reactions and reply-to
|
// Hydrate reactions, embeds, and reply-to
|
||||||
const reactionsMap = fetchReactionsForMessages([id]);
|
const reactionsMap = fetchReactionsForMessages([id]);
|
||||||
const reactions = reactionsMap.get(id) ?? [];
|
const reactions = reactionsMap.get(id) ?? [];
|
||||||
|
const embedMap = fetchEmbedsForMessages([id]);
|
||||||
|
const embedRows = embedMap.get(id) ?? [];
|
||||||
let replyTo: MessageWithUser | null = null;
|
let replyTo: MessageWithUser | null = null;
|
||||||
if (updatedMessage.replyToId) {
|
if (updatedMessage.replyToId) {
|
||||||
const replyToMap = fetchReplyToMessages([updatedMessage]);
|
const replyToMap = fetchReplyToMessages([updatedMessage]);
|
||||||
replyTo = replyToMap.get(updatedMessage.replyToId) ?? null;
|
replyTo = replyToMap.get(updatedMessage.replyToId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageWithUser = buildMessageWithUser(updatedMessage, user, attachmentRows, reactions, replyTo);
|
const messageWithUser = buildMessageWithUser(updatedMessage, user, attachmentRows, reactions, replyTo, embedRows);
|
||||||
|
|
||||||
// Broadcast edit
|
// Broadcast edit
|
||||||
const spaceId = getChannelSpaceId(message.channelId);
|
const spaceId = getChannelSpaceId(message.channelId);
|
||||||
@@ -415,6 +430,11 @@ export async function messageRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
type: 'message_updated',
|
type: 'message_updated',
|
||||||
message: messageWithUser,
|
message: messageWithUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Re-resolve embeds asynchronously after responding
|
||||||
|
setImmediate(() => {
|
||||||
|
reResolveEmbeds(id, content.trim(), message.channelId, false, spaceId).catch(() => {});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply.code(200).send(messageWithUser);
|
return reply.code(200).send(messageWithUser);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { fetchReactionsForMessages, fetchReplyToMessages, buildMessageWithUser }
|
|||||||
import { fetchDmReactionsForMessages, buildDmMessageWithUser } from './dm.js';
|
import { fetchDmReactionsForMessages, buildDmMessageWithUser } from './dm.js';
|
||||||
import { sanitizeUser } from '../utils/sanitize.js';
|
import { sanitizeUser } from '../utils/sanitize.js';
|
||||||
import type { MessageWithUser, DmMessageWithUser } from '@backspace/shared';
|
import type { MessageWithUser, DmMessageWithUser } from '@backspace/shared';
|
||||||
|
import { fetchEmbedsForMessages, fetchDmEmbedsForMessages } from '../utils/embedResolver.js';
|
||||||
|
|
||||||
interface SearchQuery {
|
interface SearchQuery {
|
||||||
q?: string;
|
q?: string;
|
||||||
@@ -143,6 +144,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reactionsMap = fetchReactionsForMessages(messageIds);
|
const reactionsMap = fetchReactionsForMessages(messageIds);
|
||||||
|
const embedMap = fetchEmbedsForMessages(messageIds);
|
||||||
const replyToMap = fetchReplyToMessages(messageRows);
|
const replyToMap = fetchReplyToMessages(messageRows);
|
||||||
|
|
||||||
const results: MessageWithUser[] = messageRows
|
const results: MessageWithUser[] = messageRows
|
||||||
@@ -151,7 +153,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const reactions = reactionsMap.get(m.id) ?? [];
|
const reactions = reactionsMap.get(m.id) ?? [];
|
||||||
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
||||||
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
|
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
|
||||||
})
|
})
|
||||||
.filter((m): m is MessageWithUser => m !== null);
|
.filter((m): m is MessageWithUser => m !== null);
|
||||||
|
|
||||||
@@ -269,6 +271,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reactionsMap = fetchDmReactionsForMessages(messageIds);
|
const reactionsMap = fetchDmReactionsForMessages(messageIds);
|
||||||
|
const embedMap = fetchDmEmbedsForMessages(messageIds);
|
||||||
|
|
||||||
// Fetch reply-to messages for DMs
|
// Fetch reply-to messages for DMs
|
||||||
const replyToIds = messageRows
|
const replyToIds = messageRows
|
||||||
@@ -299,6 +302,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
createdAt: rm.createdAt,
|
createdAt: rm.createdAt,
|
||||||
user: sanitizeUser(rUser),
|
user: sanitizeUser(rUser),
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
reactions: [],
|
reactions: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -310,7 +314,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const reactions = reactionsMap.get(m.id) ?? [];
|
const reactions = reactionsMap.get(m.id) ?? [];
|
||||||
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
||||||
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
|
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
|
||||||
})
|
})
|
||||||
.filter((m): m is DmMessageWithUser => m !== null);
|
.filter((m): m is DmMessageWithUser => m !== null);
|
||||||
|
|
||||||
@@ -405,6 +409,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reactionsMap = fetchReactionsForMessages(msgIds);
|
const reactionsMap = fetchReactionsForMessages(msgIds);
|
||||||
|
const embedMap = fetchEmbedsForMessages(msgIds);
|
||||||
const replyToMap = fetchReplyToMessages(uniqueRows);
|
const replyToMap = fetchReplyToMessages(uniqueRows);
|
||||||
|
|
||||||
const messages: MessageWithUser[] = uniqueRows
|
const messages: MessageWithUser[] = uniqueRows
|
||||||
@@ -413,7 +418,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const reactions = reactionsMap.get(m.id) ?? [];
|
const reactions = reactionsMap.get(m.id) ?? [];
|
||||||
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
||||||
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
|
return buildMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
|
||||||
})
|
})
|
||||||
.filter((m): m is MessageWithUser => m !== null);
|
.filter((m): m is MessageWithUser => m !== null);
|
||||||
|
|
||||||
@@ -498,6 +503,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reactionsMap = fetchDmReactionsForMessages(msgIds);
|
const reactionsMap = fetchDmReactionsForMessages(msgIds);
|
||||||
|
const embedMap = fetchDmEmbedsForMessages(msgIds);
|
||||||
|
|
||||||
const replyToIds = uniqueRows
|
const replyToIds = uniqueRows
|
||||||
.map(m => m.replyToId)
|
.map(m => m.replyToId)
|
||||||
@@ -527,6 +533,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
createdAt: rm.createdAt,
|
createdAt: rm.createdAt,
|
||||||
user: sanitizeUser(rUser),
|
user: sanitizeUser(rUser),
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
reactions: [],
|
reactions: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -538,7 +545,7 @@ export async function searchRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
const reactions = reactionsMap.get(m.id) ?? [];
|
const reactions = reactionsMap.get(m.id) ?? [];
|
||||||
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
const replyTo = m.replyToId ? (replyToMap.get(m.replyToId) ?? null) : null;
|
||||||
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo);
|
return buildDmMessageWithUser(m, user, attachmentMap.get(m.id) ?? [], reactions, replyTo, embedMap.get(m.id) ?? []);
|
||||||
})
|
})
|
||||||
.filter((m): m is DmMessageWithUser => m !== null);
|
.filter((m): m is DmMessageWithUser => m !== null);
|
||||||
|
|
||||||
|
|||||||
@@ -25,15 +25,15 @@ function extractYouTubeId(url: URL): string | null {
|
|||||||
|
|
||||||
// /shorts/ID
|
// /shorts/ID
|
||||||
const shortsMatch = path.match(/^\/shorts\/([A-Za-z0-9_-]+)/);
|
const shortsMatch = path.match(/^\/shorts\/([A-Za-z0-9_-]+)/);
|
||||||
if (shortsMatch) return shortsMatch[1];
|
if (shortsMatch) return shortsMatch[1] ?? null;
|
||||||
|
|
||||||
// /embed/ID
|
// /embed/ID
|
||||||
const embedMatch = path.match(/^\/embed\/([A-Za-z0-9_-]+)/);
|
const embedMatch = path.match(/^\/embed\/([A-Za-z0-9_-]+)/);
|
||||||
if (embedMatch) return embedMatch[1];
|
if (embedMatch) return embedMatch[1] ?? null;
|
||||||
|
|
||||||
// /v/ID (legacy)
|
// /v/ID (legacy)
|
||||||
const vMatch = path.match(/^\/v\/([A-Za-z0-9_-]+)/);
|
const vMatch = path.match(/^\/v\/([A-Za-z0-9_-]+)/);
|
||||||
if (vMatch) return vMatch[1];
|
if (vMatch) return vMatch[1] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { connectionManager } from './handler.js';
|
|||||||
import type { VoiceRoom, DmRoomMeta, SpaceRoomMeta } from './handler.js';
|
import type { VoiceRoom, DmRoomMeta, SpaceRoomMeta } from './handler.js';
|
||||||
import { isMember, getChannelSpaceId, isDmMember, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js';
|
import { isMember, getChannelSpaceId, isDmMember, hasPermission, computePermissions, PermissionBits } from '../utils/permissions.js';
|
||||||
import { broadcastDmMessage, getDmMessageWithUser } from '../routes/dm.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 { sanitizeUser } from '../utils/sanitize.js';
|
||||||
import { deleteAttachmentFiles } from '../utils/fileCleanup.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
|
* Re-evaluate SPEAK permission for all participants in voice channels
|
||||||
@@ -76,6 +77,11 @@ function getMessageWithUser(messageId: string): MessageWithUser | null {
|
|||||||
createdAt: r.createdAt,
|
createdAt: r.createdAt,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const embedRows = db.select()
|
||||||
|
.from(schema.embeds)
|
||||||
|
.where(eq(schema.embeds.messageId, messageId))
|
||||||
|
.all();
|
||||||
|
|
||||||
let replyTo: MessageWithUser | null = null;
|
let replyTo: MessageWithUser | null = null;
|
||||||
if (message.replyToId) {
|
if (message.replyToId) {
|
||||||
// Simple fetch for replyTo (one level deep to avoid recursion loops)
|
// Simple fetch for replyTo (one level deep to avoid recursion loops)
|
||||||
@@ -93,6 +99,7 @@ function getMessageWithUser(messageId: string): MessageWithUser | null {
|
|||||||
createdAt: replyMsg.createdAt,
|
createdAt: replyMsg.createdAt,
|
||||||
user: sanitizeUser(replyUser),
|
user: sanitizeUser(replyUser),
|
||||||
attachments: [], // Don't fetch attachments for replies to save bandwidth
|
attachments: [], // Don't fetch attachments for replies to save bandwidth
|
||||||
|
embeds: [],
|
||||||
reactions: [], // Don't fetch reactions for replies
|
reactions: [], // Don't fetch reactions for replies
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -109,6 +116,7 @@ function getMessageWithUser(messageId: string): MessageWithUser | null {
|
|||||||
createdAt: message.createdAt,
|
createdAt: message.createdAt,
|
||||||
user: sanitizeUser(user),
|
user: sanitizeUser(user),
|
||||||
attachments,
|
attachments,
|
||||||
|
embeds: embedRows.map(e => embedRowToEmbed(e)),
|
||||||
reactions,
|
reactions,
|
||||||
replyTo,
|
replyTo,
|
||||||
};
|
};
|
||||||
@@ -257,6 +265,11 @@ function handleMessageCreate(event: Record<string, unknown>, userId: string): vo
|
|||||||
type: 'message_created',
|
type: 'message_created',
|
||||||
message: messageWithUser,
|
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',
|
type: 'message_updated',
|
||||||
message: updatedMessage,
|
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)
|
// Broadcast to all DM members (including those who closed the channel)
|
||||||
broadcastDmMessage(dmChannelId, dmMessage);
|
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 {
|
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,
|
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 {
|
function handleDmMessageDelete(event: Record<string, unknown>, userId: string): void {
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
|||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
user: currentUser,
|
user: currentUser,
|
||||||
attachments: [],
|
attachments: [],
|
||||||
|
embeds: [],
|
||||||
reactions: [],
|
reactions: [],
|
||||||
replyTo: get().replyTo ?? undefined,
|
replyTo: get().replyTo ?? undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user