Changeset 737ea05 in Klonkt


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@…>

Files:
7 edited

Legend:

Unmodified
Added
Removed
  • CHANGELOG.de.md

    r1c2dcba r737ea05  
    88### Hinzugefügt
    99- **Einen Beitrag im Fediverse melden.** Von einem Fediverse-Beitrag aus kannst du ihn jetzt den
    10   Moderatoren seines Heimatservers melden, mit optionaler Begründung.
     10  Moderatoren seines Heimatservers melden, mit optionaler Begründung — und meldet jemand deine Seite,
     11  erscheint die Meldung in deinen Fediverse-Benachrichtigungen.
    1112- **Sprache eines Beitrags festlegen.** Wähle, in welcher Sprache du einen Beitrag geschrieben hast —
    1213  im Fediverse funktionieren damit der Sprachfilter der Timeline und die Übersetzen-Schaltfläche.
  • CHANGELOG.md

    r1c2dcba r737ea05  
    88### Added
    99- **Report a post to the fediverse.** From a fediverse post you can now report it to the moderators
    10   of its own home server, with an optional reason.
     10  of its own home server, with an optional reason — and if someone reports your site, the report
     11  shows up in your fediverse notifications.
    1112- **Set a post's language.** Choose the language you wrote a post in — on the fediverse it enables
    1213  timeline language filtering and the translate button.
  • CHANGELOG.nl.md

    r1c2dcba r737ea05  
    88### Toegevoegd
    99- **Rapporteer een post op de fediverse.** Vanaf een fediverse-post kun je die nu melden bij de
    10   moderators van de eigen server van die post, met een optionele reden.
     10  moderators van de eigen server van die post, met een optionele reden — en meldt iemand jouw site,
     11  dan verschijnt die melding in je fediverse-meldingen.
    1112- **Stel de taal van een post in.** Kies in welke taal je een post schreef — op de fediverse
    1213  werkt daarmee het taalfilter van de tijdlijn en de vertaal-knop.
  • src/config/database.js

    r1c2dcba r737ea05  
    347347    );
    348348    CREATE INDEX IF NOT EXISTS idx_poll_votes_post ON poll_votes(post_id);
     349    CREATE TABLE IF NOT EXISTS ap_reports (
     350      id INTEGER PRIMARY KEY AUTOINCREMENT,
     351      slug TEXT NOT NULL,           -- our site the report is about (its owner moderates)
     352      actor_uri TEXT,               -- the reporter's actor URI
     353      actor_name TEXT, actor_handle TEXT, actor_icon TEXT,
     354      content TEXT,                 -- the reason (plain text)
     355      objects TEXT,                 -- JSON array of reported object URIs (our actor + statuses)
     356      seen INTEGER DEFAULT 0,
     357      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
     358    );
     359    CREATE INDEX IF NOT EXISTS idx_ap_reports_slug ON ap_reports(slug, created_at);
    349360  `);
    350361  // "Feature" a followed account: its posts show in the local Cirkel.
  • 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);
  • src/services/i18n.js

    r1c2dcba r737ea05  
    2828    'nav.language': 'Taal',
    2929    'nav.notifications': 'Meldingen',
    30     'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 'notif.followed': 'volgt je nu', 'notif.liked': 'likete je post', 'notif.boosted': 'boostte je post', 'notif.replied': 'reageerde op', 'blk.title': 'Blokkeren', 'blk.lead': 'Blokkeer een account of een heel domein — hun reacties, likes en posts verdwijnen en nieuwe worden geweigerd.', 'blk.block_btn': 'Blokkeren', 'blk.empty': 'Niks geblokkeerd.', 'blk.unblock': 'Deblokkeren', 'tl.block': 'Blokkeer',
     30    'notif.title': 'Meldingen', 'notif.empty': 'Nog geen meldingen.', 'notif.someone': 'Iemand', 'notif.followed': 'volgt je nu', 'notif.liked': 'likete je post', 'notif.boosted': 'boostte je post', 'notif.replied': 'reageerde op', 'notif.reported': 'rapporteerde je bij hun server', 'blk.title': 'Blokkeren', 'blk.lead': 'Blokkeer een account of een heel domein — hun reacties, likes en posts verdwijnen en nieuwe worden geweigerd.', 'blk.block_btn': 'Blokkeren', 'blk.empty': 'Niks geblokkeerd.', 'blk.unblock': 'Deblokkeren', 'tl.block': 'Blokkeer',
    3131    'notif.reply': '{actor} reageerde op je reactie', 'notif.comment': '{actor} reageerde op je post', 'notif.like': '{actor} vindt je post leuk',
    3232    'switch.agenda': 'Agenda',
     
    959959    'nav.language': 'Language',
    960960    'nav.notifications': 'Notifications',
    961     'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 'notif.followed': 'followed you', 'notif.liked': 'liked your post', 'notif.boosted': 'boosted your post', 'notif.replied': 'replied to', 'blk.title': 'Blocking', 'blk.lead': 'Block an account or a whole domain — their replies, likes and posts disappear and new ones are refused.', 'blk.block_btn': 'Block', 'blk.empty': 'Nothing blocked.', 'blk.unblock': 'Unblock', 'tl.block': 'Block',
     961    'notif.title': 'Notifications', 'notif.empty': 'No notifications yet.', 'notif.someone': 'Someone', 'notif.followed': 'followed you', 'notif.liked': 'liked your post', 'notif.boosted': 'boosted your post', 'notif.replied': 'replied to', 'notif.reported': 'reported you to their server', 'blk.title': 'Blocking', 'blk.lead': 'Block an account or a whole domain — their replies, likes and posts disappear and new ones are refused.', 'blk.block_btn': 'Block', 'blk.empty': 'Nothing blocked.', 'blk.unblock': 'Unblock', 'tl.block': 'Block',
    962962    'notif.reply': '{actor} replied to your comment', 'notif.comment': '{actor} commented on your post', 'notif.like': '{actor} liked your post',
    963963    'switch.agenda': 'Agenda',
     
    18811881    'nav.language': 'Sprache',
    18821882    'nav.notifications': 'Benachrichtigungen',
    1883     'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 'notif.followed': 'folgt dir jetzt', 'notif.liked': 'gefällt dein Beitrag', 'notif.boosted': 'teilte deinen Beitrag', 'notif.replied': 'antwortete auf', 'blk.title': 'Blockieren', 'blk.lead': 'Blockiere ein Konto oder eine ganze Domain — ihre Antworten, Likes und Beiträge verschwinden und neue werden abgelehnt.', 'blk.block_btn': 'Blockieren', 'blk.empty': 'Nichts blockiert.', 'blk.unblock': 'Entsperren', 'tl.block': 'Blockieren',
     1883    'notif.title': 'Benachrichtigungen', 'notif.empty': 'Noch keine Benachrichtigungen.', 'notif.someone': 'Jemand', 'notif.followed': 'folgt dir jetzt', 'notif.liked': 'gefällt dein Beitrag', 'notif.boosted': 'teilte deinen Beitrag', 'notif.replied': 'antwortete auf', 'notif.reported': 'hat dich bei ihrem Server gemeldet', 'blk.title': 'Blockieren', 'blk.lead': 'Blockiere ein Konto oder eine ganze Domain — ihre Antworten, Likes und Beiträge verschwinden und neue werden abgelehnt.', 'blk.block_btn': 'Blockieren', 'blk.empty': 'Nichts blockiert.', 'blk.unblock': 'Entsperren', 'tl.block': 'Blockieren',
    18841884    'notif.reply': '{actor} hat auf deinen Kommentar geantwortet', 'notif.comment': '{actor} hat deinen Beitrag kommentiert', 'notif.like': '{actor} gefällt dein Beitrag',
    18851885    'switch.agenda': 'Termine',
  • src/views/pages/fedi-notifications.ejs

    r1c2dcba r737ea05  
    77    <ul class="nt-list">
    88      <% items.forEach(function(n){
    9            var _ic = n.type === 'follow' ? 'follow' : (n.type === 'like' ? 'like' : (n.type === 'announce' ? 'boost' : 'reply'));
     9           var _ic = n.type === 'follow' ? 'follow' : (n.type === 'like' ? 'like' : (n.type === 'announce' ? 'boost' : (n.type === 'report' ? 'report' : 'reply')));
    1010      %>
    1111        <li class="nt-item nt-<%= _ic %>">
     
    1414            <% } else if (_ic === 'boost') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
    1515            <% } else if (_ic === 'follow') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
     16            <% } else if (_ic === 'report') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>
    1617            <% } else { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg><% } %>
    1718          </span>
     
    2627              <% } else if (n.type === 'like') { %><%= t('notif.liked') %>
    2728              <% } else if (n.type === 'announce') { %><%= t('notif.boosted') %>
     29              <% } else if (n.type === 'report') { %><%= t('notif.reported') %>
    2830              <% } else { %><%= t('notif.replied') %><% } %>
    2931              <% if (n.post_slug) { %><a class="nt-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %>
    3032            </div>
    31             <% if (n.type === 'reply' && n.content) { %><div class="nt-content"><%- n.content %></div><% } %>
     33            <% if (n.type === 'report' && n.content) { %><div class="nt-content"><%= n.content %></div>
     34            <% } else if (n.type === 'reply' && n.content) { %><div class="nt-content"><%- n.content %></div><% } %>
    3235          </div>
    3336        </li>
     
    5558  .nt-boost .nt-icon { background: color-mix(in srgb, #2fa85a 16%, transparent); color: #2fa85a; }
    5659  .nt-follow .nt-icon, .nt-reply .nt-icon { background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); }
     60  .nt-report .nt-icon { background: color-mix(in srgb, #c0392b 16%, transparent); color: #c0392b; }
    5761  .nt-body { flex: 1; min-width: 0; }
    5862  .nt-line { display: flex; align-items: baseline; gap: .4rem; }
Note: See TracChangeset for help on using the changeset viewer.