48 lines · 1.9 KB
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<string, MentionTarget>;

export function mentionHandles(texts: string[]): string[] {
  const names = new Set<string>();
  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<MentionMap> {
  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