Changeset dd7daef in Klonkt


Ignore:
Timestamp:
06/27/2026 12:15:56 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
04c70fc
Parents:
bddbfe0
Message:

feat(fedi): inline @mentions in replies federate (resolve + link + notify)

  • services/ActivityPubService.js — replies now resolve inline @user@domain mentions (resolveMentionsInText via WebFinger), link them (class="u-url mention", href = actor URI) and deliver to the mentioned actors' inboxes so they get notified. buildReplyNote derives Mention tags from the content (mentionTags) — covering the parent mention + inline ones. deliverOutboxUpdate (edit) links inline mentions too.
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    rbddbfe0 rdd7daef  
    931931}
    932932
     933// Extract Mention tag objects from already-linked content (class="u-url mention").
     934function mentionTags(content) {
     935  const tags = [], seen = new Set();
     936  const re = /<a href="([^"]+)" class="u-url mention">@([^<]+)<\/a>/gi;
     937  let m;
     938  while ((m = re.exec(content || ''))) {
     939    const href = m[1];
     940    if (seen.has(href)) continue; seen.add(href);
     941    tags.push({ type: 'Mention', href, name: '@' + m[2] });
     942  }
     943  return tags;
     944}
     945// Resolve inline @user@domain mentions in reply/post text → link them (href = actor URI)
     946// and collect the mentioned actors' inboxes so they get notified. Best-effort per mention.
     947async function resolveMentionsInText(base, html) {
     948  const inboxes = [];
     949  const handles = new Set();
     950  const re = /(^|[\s>])@([A-Za-z0-9_.-]+@[A-Za-z0-9.-]+)/g;
     951  let m;
     952  while ((m = re.exec(html || ''))) handles.add(m[2]);
     953  let out = String(html || '');
     954  for (const h of handles) {
     955    let actorUri = null;
     956    try { actorUri = await webfingerResolve('@' + h); } catch { actorUri = null; }
     957    if (!actorUri) continue;
     958    const actor = await fetchActor(actorUri).catch(() => null);
     959    const inbox = actor && ((actor.endpoints && actor.endpoints.sharedInbox) || actor.inbox);
     960    if (inbox) inboxes.push(inbox);
     961    const esc = h.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
     962    out = out.replace(new RegExp('(^|[\\s>])@' + esc + '(?![A-Za-z0-9_.-])', 'g'),
     963      (full, pre) => `${pre}<a href="${actorUri}" class="u-url mention">@${h}</a>`);
     964  }
     965  return { html: out, inboxes };
     966}
     967
    933968export function buildReplyNote(base, site, row) {
    934969  const me = actorId(base, site.slug);
     
    944979    cc: [PUBLIC, `${me}/followers`],
    945980    tag: [
    946       ...(row.to_actor ? [{ type: 'Mention', href: row.to_actor, name: row.to_handle }] : []),
     981      ...mentionTags(row.content),
    947982      ...hashtagTags(base, row.content),
    948983    ],
     
    9661001  const me = actorId(base, site.slug);
    9671002  const handle = parent.actor_handle || deriveHandle(parent.actor_uri);
     1003  const dispHandle = handle && handle[0] === '@' ? handle : '@' + (handle || '');
    9681004  const body = escHtml(String(text).trim()).replace(/\r?\n/g, '<br>');
     1005  const mres = await resolveMentionsInText(base, body); // link inline @mentions + collect their inboxes
    9691006  const mention = parent.actor_uri
    970     ? `<a href="${escHtml(parent.actor_url || parent.actor_uri)}" class="u-url mention">${escHtml(handle)}</a> ` : '';
    971   const content = `<p>${mention}${linkHashtags(base, body)}</p>`;
     1007    ? `<a href="${escHtml(parent.actor_uri || parent.actor_url)}" class="u-url mention">${escHtml(dispHandle)}</a> ` : '';
     1008  const content = `<p>${mention}${linkHashtags(base, mres.html)}</p>`;
    9721009  // Dedup: skip if the exact same reply was already sent (double-submit guard).
    9731010  const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? LIMIT 1')
     
    9931030  (parent.threadInboxes || []).forEach((i) => inboxes.add(i)); // whole ancestor chain
    9941031  for (const f of fStmts().list.all(site.slug)) inboxes.add(f.shared_inbox || f.inbox);
     1032  mres.inboxes.forEach((i) => inboxes.add(i)); // people @mentioned inline in the reply
    9951033  inboxes.delete(`${me}/inbox`);       // never deliver to ourselves (already in ap_outbox)
    9961034  inboxes.delete(`${base}/ap/inbox`);  // (our own shared inbox) → avoids a self-duplicate
     
    11221160  const mention = row.to_actor
    11231161    ? `<a href="${escHtml(row.to_actor)}" class="u-url mention">${escHtml(row.to_handle || deriveHandle(row.to_actor))}</a> ` : '';
    1124   const body = escHtml(text).replace(/\r?\n/g, '<br>');
    1125   const content = `<p>${mention}${linkHashtags(base, body)}</p>`;
     1162  const mres = await resolveMentionsInText(base, escHtml(text).replace(/\r?\n/g, '<br>'));
     1163  const content = `<p>${mention}${linkHashtags(base, mres.html)}</p>`;
    11261164  db.prepare('UPDATE ap_outbox SET content = ? WHERE id = ?').run(content, outboxId);
    11271165  const note = buildReplyNote(base, site, iStmts().getO.get(outboxId));
Note: See TracChangeset for help on using the changeset viewer.