atagen

malice

public
Star0
Watch0
dffd63b58af439639de9f8e155d0365d83605a8b
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/urls.ts

973f8fbdaf80atagen2026-09-28 09:10 UTC1
export interface UrlPreview {
973f8fbdaf80atagen2026-09-28 09:10 UTC2
  href: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC3
  display: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC4
  hostname: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC5
  path: string;
973f8fbdaf80atagen2026-09-28 09:10 UTC6
}
973f8fbdaf80atagen2026-09-28 09:10 UTC7
973f8fbdaf80atagen2026-09-28 09:10 UTC8
const MAX_LINKS = 4;
973f8fbdaf80atagen2026-09-28 09:10 UTC9
const MAX_URL_LENGTH = 2048;
973f8fbdaf80atagen2026-09-28 09:10 UTC10
const TRAILING_PUNCT = /[.,;:!?'"~…—–-]+$/u;
973f8fbdaf80atagen2026-09-28 09:10 UTC11
973f8fbdaf80atagen2026-09-28 09:10 UTC12
function countChar(text: string, char: string): number {
973f8fbdaf80atagen2026-09-28 09:10 UTC13
  let count = 0;
973f8fbdaf80atagen2026-09-28 09:10 UTC14
  for (const c of text) if (c === char) count += 1;
973f8fbdaf80atagen2026-09-28 09:10 UTC15
  return count;
973f8fbdaf80atagen2026-09-28 09:10 UTC16
}
973f8fbdaf80atagen2026-09-28 09:10 UTC17
973f8fbdaf80atagen2026-09-28 09:10 UTC18
function trimUrl(raw: string): string {
973f8fbdaf80atagen2026-09-28 09:10 UTC19
  let out = raw;
973f8fbdaf80atagen2026-09-28 09:10 UTC20
  let changed = true;
973f8fbdaf80atagen2026-09-28 09:10 UTC21
  while (changed && out.length > 0) {
973f8fbdaf80atagen2026-09-28 09:10 UTC22
    changed = false;
973f8fbdaf80atagen2026-09-28 09:10 UTC23
    const stripped = out.replace(TRAILING_PUNCT, '');
973f8fbdaf80atagen2026-09-28 09:10 UTC24
    if (stripped !== out) {
973f8fbdaf80atagen2026-09-28 09:10 UTC25
      out = stripped;
973f8fbdaf80atagen2026-09-28 09:10 UTC26
      changed = true;
973f8fbdaf80atagen2026-09-28 09:10 UTC27
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC28
    // Strip a trailing closer only when it has no matching opener left in
973f8fbdaf80atagen2026-09-28 09:10 UTC29
    // the URL itself, so "(https://x/a_(b))" keeps the inner pair and drops
973f8fbdaf80atagen2026-09-28 09:10 UTC30
    // the outer markdown/paren closer.
973f8fbdaf80atagen2026-09-28 09:10 UTC31
    const pairs: Array<[string, string]> = [
973f8fbdaf80atagen2026-09-28 09:10 UTC32
      ['(', ')'],
973f8fbdaf80atagen2026-09-28 09:10 UTC33
      ['[', ']'],
973f8fbdaf80atagen2026-09-28 09:10 UTC34
      ['{', '}']
973f8fbdaf80atagen2026-09-28 09:10 UTC35
    ];
973f8fbdaf80atagen2026-09-28 09:10 UTC36
    for (const [open, close] of pairs) {
973f8fbdaf80atagen2026-09-28 09:10 UTC37
      if (out.endsWith(close) && countChar(out, close) > countChar(out, open)) {
973f8fbdaf80atagen2026-09-28 09:10 UTC38
        out = out.slice(0, -1);
973f8fbdaf80atagen2026-09-28 09:10 UTC39
        changed = true;
973f8fbdaf80atagen2026-09-28 09:10 UTC40
      }
973f8fbdaf80atagen2026-09-28 09:10 UTC41
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC42
    if (out.endsWith('>')) {
973f8fbdaf80atagen2026-09-28 09:10 UTC43
      out = out.slice(0, -1);
973f8fbdaf80atagen2026-09-28 09:10 UTC44
      changed = true;
973f8fbdaf80atagen2026-09-28 09:10 UTC45
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC46
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC47
  return out;
973f8fbdaf80atagen2026-09-28 09:10 UTC48
}
973f8fbdaf80atagen2026-09-28 09:10 UTC49
973f8fbdaf80atagen2026-09-28 09:10 UTC50
/**
973f8fbdaf80atagen2026-09-28 09:10 UTC51
 * Find http(s) URLs in message text without fetching anything: rendering is
973f8fbdaf80atagen2026-09-28 09:10 UTC52
 * purely local (hostname/path/link card) so no third-party request can leak
973f8fbdaf80atagen2026-09-28 09:10 UTC53
 * reader IPs. Callers MUST NOT auto-fetch link targets.
973f8fbdaf80atagen2026-09-28 09:10 UTC54
 */
973f8fbdaf80atagen2026-09-28 09:10 UTC55
export function findUrls(text: string): UrlPreview[] {
973f8fbdaf80atagen2026-09-28 09:10 UTC56
  if (!text || !text.includes('http')) return [];
973f8fbdaf80atagen2026-09-28 09:10 UTC57
  const out: UrlPreview[] = [];
973f8fbdaf80atagen2026-09-28 09:10 UTC58
  const seen = new Set<string>();
973f8fbdaf80atagen2026-09-28 09:10 UTC59
  const pattern = /https?:\/\/[^\s<>"'`\]]+/gi;
973f8fbdaf80atagen2026-09-28 09:10 UTC60
  let match: RegExpExecArray | null;
973f8fbdaf80atagen2026-09-28 09:10 UTC61
  while (out.length < MAX_LINKS && (match = pattern.exec(text)) !== null) {
973f8fbdaf80atagen2026-09-28 09:10 UTC62
    let candidate = match[0];
973f8fbdaf80atagen2026-09-28 09:10 UTC63
    if (candidate.length > MAX_URL_LENGTH) candidate = candidate.slice(0, MAX_URL_LENGTH);
973f8fbdaf80atagen2026-09-28 09:10 UTC64
    const trimmed = trimUrl(candidate);
973f8fbdaf80atagen2026-09-28 09:10 UTC65
    if (trimmed.length < 8) continue;
973f8fbdaf80atagen2026-09-28 09:10 UTC66
    let parsed: URL | null = null;
973f8fbdaf80atagen2026-09-28 09:10 UTC67
    try {
973f8fbdaf80atagen2026-09-28 09:10 UTC68
      parsed = new URL(trimmed);
973f8fbdaf80atagen2026-09-28 09:10 UTC69
    } catch {
973f8fbdaf80atagen2026-09-28 09:10 UTC70
      continue;
973f8fbdaf80atagen2026-09-28 09:10 UTC71
    }
973f8fbdaf80atagen2026-09-28 09:10 UTC72
    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue;
973f8fbdaf80atagen2026-09-28 09:10 UTC73
    const href = parsed.toString();
973f8fbdaf80atagen2026-09-28 09:10 UTC74
    if (seen.has(href)) continue;
973f8fbdaf80atagen2026-09-28 09:10 UTC75
    seen.add(href);
973f8fbdaf80atagen2026-09-28 09:10 UTC76
    const rest = `${parsed.pathname}${parsed.search}${parsed.hash}`;
973f8fbdaf80atagen2026-09-28 09:10 UTC77
    out.push({
973f8fbdaf80atagen2026-09-28 09:10 UTC78
      href,
973f8fbdaf80atagen2026-09-28 09:10 UTC79
      display: trimmed.length > 72 ? `${trimmed.slice(0, 69)}…` : trimmed,
973f8fbdaf80atagen2026-09-28 09:10 UTC80
      hostname: parsed.hostname,
973f8fbdaf80atagen2026-09-28 09:10 UTC81
      path: rest && rest !== '/' ? (rest.length > 48 ? `${rest.slice(0, 45)}…` : rest) : ''
973f8fbdaf80atagen2026-09-28 09:10 UTC82
    });
973f8fbdaf80atagen2026-09-28 09:10 UTC83
  }
973f8fbdaf80atagen2026-09-28 09:10 UTC84
  return out;
973f8fbdaf80atagen2026-09-28 09:10 UTC85
}