Ignore:
Timestamp:
07/01/2026 07:26:13 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
9e1b1bb
Parents:
1c2dcba
Message:

feat(fediverse): receive reports (inbound Flag) → owner's notifications

Completes the Klonkt↔Klonkt report loop: a Flag delivered to a Klonkt inbox was
previously ignored. It's now signature-enforced, stored, and surfaced to the site
owner (who moderates their own instance) in the fediverse notifications.

  • src/config/database.js — ap_reports table (per-site incoming reports).
  • src/services/ActivityPubService.js — handleInbox handles inbound Flag (resolves which local site it targets from the reported object URIs, stores reporter + reason); Flag added to the signature-GATED list; getNotifications includes reports.
  • src/views/pages/fedi-notifications.ejs — a report item (flag icon, reason).
  • src/services/i18n.js — notif.reported (nl/en/de).
  • CHANGELOG(.nl/.de).md — note that reports about your site show in notifications.

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r1c2dcba r737ea05  
    936936  // Blocked actor/domain → silently drop (202, don't reveal the block).
    937937  if (claimedActor && isBlockedAny(claimedActor)) { console.log('[AP] inbox dropped (blocked)', claimedActor, 'from', ip); return 202; }
    938   const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject', 'Add', 'Remove', 'Update'];
     938  const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject', 'Add', 'Remove', 'Update', 'Flag'];
    939939  if (GATED.includes(type)) {
    940940    if (!verified || !claimedActor || verified.id !== claimedActor) {
     
    942942      return 401;
    943943    }
     944  }
     945
     946  // A moderation report (Flag) about our content — store it for the targeted site's owner
     947  // (each Klonkt site is moderated by its own owner). Signature is enforced (GATED).
     948  if (type === 'Flag') {
     949    const objs = Array.isArray(act.object) ? act.object : (act.object ? [act.object] : []);
     950    const objectUris = objs.map((o) => (typeof o === 'string' ? o : (o && o.id))).filter(Boolean);
     951    let targetSlug = null;
     952    const noteIds = [];
     953    for (const u of objectUris) {
     954      const s = slugFromActorUrl(u);        // one of our actors?
     955      if (s) { targetSlug = targetSlug || s; continue; }
     956      const pid = postIdFromNoteUrl(u, base); // one of our notes?
     957      if (pid) noteIds.push(pid);
     958    }
     959    if (!targetSlug && noteIds.length) {
     960      try { const r = db.prepare('SELECT s.slug FROM posts p JOIN sites s ON s.id = p.site_id WHERE p.id = ? LIMIT 1').get(noteIds[0]); if (r) targetSlug = r.slug; } catch { /* ignore */ }
     961    }
     962    if (!targetSlug) return 202; // not about us / can't tell → drop
     963    const ai = actorInfo(await resolveActor(claimedActor).catch(() => null), claimedActor);
     964    try {
     965      db.prepare('INSERT INTO ap_reports (slug, actor_uri, actor_name, actor_handle, actor_icon, content, objects, created_at) VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP)')
     966        .run(targetSlug, claimedActor || null, ai.name, ai.handle, ai.icon, HtmlSanitizerService.toPlainText(act.content || '').slice(0, 3000), JSON.stringify(objectUris.slice(0, 20)));
     967      console.log('[AP] report received for', targetSlug, 'from', claimedActor);
     968    } catch { /* ignore */ }
     969    return 202;
    944970  }
    945971
     
    21102136    });
    21112137  } catch { /* ignore */ }
     2138  try {
     2139    for (const r of db.prepare('SELECT actor_uri, actor_name, actor_handle, actor_icon, content, created_at FROM ap_reports WHERE slug = ? ORDER BY created_at DESC LIMIT 50').all(slug)) {
     2140      out.push({ type: 'report', name: r.actor_name, handle: r.actor_handle, url: r.actor_uri, icon: r.actor_icon, content: r.content, created_at: r.created_at });
     2141    }
     2142  } catch { /* ignore */ }
    21122143  out.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
    21132144  return out.slice(0, limit || 60);
Note: See TracChangeset for help on using the changeset viewer.