import { all, chunks, placeholders } from "./db"; /** Same handle grammar as the markdown reference linker. */ const MENTION = /(?:^|[^\w@/])@([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/g; const MAX_MENTIONS = 200; export type MentionTarget = { id: string; name: string }; /** Lowercased handle as written (current or former name) to the account it names today. */ export type MentionMap = Map; export function mentionHandles(texts: string[]): string[] { const names = new Set(); for (const text of texts) { for (const match of text.matchAll(MENTION)) { names.add(match[1].toLowerCase()); if (names.size >= MAX_MENTIONS) return [...names]; } } return [...names]; } /** * Resolves `@handle`s to accounts. Former names stay reserved for the renamed account forever * (account_redirects; deleted accounts' names are retired), so an old handle names exactly one account * and mentions keep pointing at the same person across renames, rendered under their current name. */ export async function resolveMentions(texts: string[]): Promise { const out: MentionMap = new Map(); const handles = mentionHandles(texts); for (const group of chunks(handles)) { const rows = await all<{ handle: string; id: string; name: string }>( `SELECT LOWER(name) AS handle, id, name FROM accounts WHERE name IN (${placeholders(group.length)})`, ...group, ); for (const row of rows) out.set(row.handle, { id: row.id, name: row.name }); } const former = handles.filter((handle) => !out.has(handle)); for (const group of chunks(former)) { const rows = await all<{ handle: string; id: string; name: string }>( `SELECT LOWER(r.old_name) AS handle, a.id, a.name FROM account_redirects r JOIN accounts a ON a.id = r.account_id WHERE r.old_name IN (${placeholders(group.length)})`, ...group, ); for (const row of rows) out.set(row.handle, { id: row.id, name: row.name }); } return out; }