import { eq, inArray } from 'drizzle-orm'; import type { Embed } from '@backspace/shared'; import { getDb, schema } from '../db/index.js'; import { generateSnowflake } from './snowflake.js'; import { classifyUrl } from './embedClassifier.js'; import { fetchUrlMetadata } from './metadataFetcher.js'; import { connectionManager } from '../ws/handler.js'; const MAX_EMBEDS_PER_MESSAGE = 5; // ─── URL Extraction ──────────────────────────────────────────────────────── export function extractUrls(content: string | null): string[] { if (!content) return []; const matches = content.match(/https?:\/\/[^\s<>"{}|\\^`[\]]+/g); if (!matches) return []; // Deduplicate while preserving order const seen = new Set(); const unique: string[] = []; for (const url of matches) { if (!seen.has(url)) { seen.add(url); unique.push(url); } } return unique.slice(0, MAX_EMBEDS_PER_MESSAGE); } // ─── Row → Embed Conversion ──────────────────────────────────────────────── export function embedRowToEmbed(row: typeof schema.embeds.$inferSelect): Embed { return { id: row.id, messageId: row.messageId, dmMessageId: row.dmMessageId, url: row.url, embedType: row.embedType as Embed['embedType'], provider: (row.provider ?? null) as Embed['provider'], title: row.title ?? null, description: row.description ?? null, image: row.image ?? null, embedUrl: row.embedUrl ?? null, width: row.width ?? null, height: row.height ?? null, color: row.color ?? null, createdAt: row.createdAt, }; } // ─── Embed Resolution ───────────────────────────────────────────────────── export async function resolveEmbeds( messageId: string, content: string | null, channelId: string, isDm: boolean, spaceId: string | null, ): Promise { const urls = extractUrls(content); if (urls.length === 0) return; const db = getDb(); const now = Date.now(); const resolvedEmbeds: Embed[] = []; for (const url of urls) { try { const classification = classifyUrl(url); let title: string | null = null; let description: string | null = null; let image: string | null = null; let siteName: string | null = null; if (classification.embedType === 'image') { // Direct image URL — no fetch needed, use the URL as the image source image = url; } else if (classification.needsMetadataFetch) { const metadata = await fetchUrlMetadata(url); if (metadata) { title = metadata.title; description = metadata.description; image = metadata.image; siteName = metadata.siteName; } // For generic embeds, skip if we couldn't extract a title if (classification.embedType === 'generic' && !title) { continue; } } const embedId = generateSnowflake(); const row: typeof schema.embeds.$inferInsert = { id: embedId, messageId: isDm ? null : messageId, dmMessageId: isDm ? messageId : null, url, embedType: classification.embedType, provider: classification.provider, title, description, image, embedUrl: classification.embedUrl, width: null, height: null, color: null, createdAt: now, }; db.insert(schema.embeds).values(row).run(); resolvedEmbeds.push(embedRowToEmbed(row as typeof schema.embeds.$inferSelect)); } catch { // One URL failure must not block the rest continue; } } if (resolvedEmbeds.length === 0) return; if (isDm) { connectionManager.sendToDmMembers(channelId, { type: 'dm_embeds_resolved', messageId, dmChannelId: channelId, embeds: resolvedEmbeds, }); } else { if (!spaceId) return; connectionManager.sendToChannel(spaceId, channelId, { type: 'embeds_resolved', messageId, channelId, embeds: resolvedEmbeds, }); } } // ─── Re-resolution (on message edit) ────────────────────────────────────── export async function reResolveEmbeds( messageId: string, content: string | null, channelId: string, isDm: boolean, spaceId: string | null, ): Promise { const db = getDb(); if (isDm) { db.delete(schema.embeds) .where(eq(schema.embeds.dmMessageId, messageId)) .run(); } else { db.delete(schema.embeds) .where(eq(schema.embeds.messageId, messageId)) .run(); } await resolveEmbeds(messageId, content, channelId, isDm, spaceId); } // ─── Batch Fetching ──────────────────────────────────────────────────────── export function fetchEmbedsForMessages( messageIds: string[], ): Map { const result = new Map(); if (messageIds.length === 0) return result; const db = getDb(); const rows = db.select() .from(schema.embeds) .where(inArray(schema.embeds.messageId, messageIds)) .all(); for (const row of rows) { if (!row.messageId) continue; const existing = result.get(row.messageId); if (existing) { existing.push(row); } else { result.set(row.messageId, [row]); } } return result; } export function fetchDmEmbedsForMessages( dmMessageIds: string[], ): Map { const result = new Map(); if (dmMessageIds.length === 0) return result; const db = getDb(); const rows = db.select() .from(schema.embeds) .where(inArray(schema.embeds.dmMessageId, dmMessageIds)) .all(); for (const row of rows) { if (!row.dmMessageId) continue; const existing = result.get(row.dmMessageId); if (existing) { existing.push(row); } else { result.set(row.dmMessageId, [row]); } } return result; }