Ignore:
Timestamp:
06/24/2026 04:08:34 PM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
19a72df
Parents:
d0cab9d
Message:

feat(fediverse-client): block/defederate accounts & domains

Block an @handle or a whole domain (/blokkeren + 🚫 on timeline items): their
inbound activities are silently dropped (202) and their stored content is purged.
New ap_blocks table + isBlockedAny gate in the inbox. Top-bar bell now points to
the fediverse /meldingen (gated to site managers via canManageFedi) instead of the
dead native notifications.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    rd0cab9d rf5c3870  
    364364  // forged replies/likes/follows/timeline posts). GET/discovery stays open.
    365365  const claimedActor = typeof act.actor === 'string' ? act.actor : (act.actor && act.actor.id);
     366  // Blocked actor/domain → silently drop (202, don't reveal the block).
     367  if (claimedActor && isBlockedAny(claimedActor)) { console.log('[AP] inbox dropped (blocked)', claimedActor); return 202; }
    366368  const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject'];
    367369  if (GATED.includes(type)) {
     
    774776}
    775777
     778// ── Blocking / defederation ───────────────────────────────────────
     779let _insBl, _delBl, _listBl;
     780function blStmts() {
     781  if (!_insBl) {
     782    _insBl = db.prepare('INSERT OR IGNORE INTO ap_blocks (slug, target, kind, label, created_at) VALUES (?,?,?,?,CURRENT_TIMESTAMP)');
     783    _delBl = db.prepare('DELETE FROM ap_blocks WHERE slug = ? AND target = ?');
     784    _listBl = db.prepare('SELECT * FROM ap_blocks WHERE slug = ? ORDER BY created_at DESC');
     785  }
     786  return { ins: _insBl, del: _delBl, list: _listBl };
     787}
     788export function listBlocks(slug) { return blStmts().list.all(slug); }
     789
     790// True if an actor (or its whole domain) is blocked anywhere on this instance.
     791export function isBlockedAny(actorUri) {
     792  if (!actorUri) return false;
     793  let domain = ''; try { domain = new URL(actorUri).host; } catch { /* ignore */ }
     794  try { return !!db.prepare("SELECT 1 FROM ap_blocks WHERE (kind='actor' AND target=?) OR (kind='domain' AND target=?) LIMIT 1").get(actorUri, domain); }
     795  catch { return false; }
     796}
     797
     798function purgeBlocked(kind, target) {
     799  try {
     800    if (kind === 'domain') {
     801      const like = `%//${target}/%`;
     802      db.prepare('DELETE FROM ap_interactions WHERE actor_uri LIKE ?').run(like);
     803      db.prepare('DELETE FROM ap_timeline WHERE author_uri LIKE ?').run(like);
     804      db.prepare('DELETE FROM ap_followers WHERE actor_uri LIKE ?').run(like);
     805    } else {
     806      db.prepare('DELETE FROM ap_interactions WHERE actor_uri = ?').run(target);
     807      db.prepare('DELETE FROM ap_timeline WHERE author_uri = ?').run(target);
     808      db.prepare('DELETE FROM ap_followers WHERE actor_uri = ?').run(target);
     809    }
     810  } catch { /* best-effort */ }
     811}
     812
     813// Block an actor (@handle or actor URL) or a whole domain; purges their content.
     814export async function blockTarget(site, input) {
     815  const raw = String(input || '').trim();
     816  if (!site || !site.slug || !raw) return { error: 'empty' };
     817  let kind, target, label;
     818  if (/^https?:\/\//i.test(raw)) { kind = 'actor'; target = raw; label = raw; }
     819  else if (raw.includes('@')) {
     820    const actorUrl = await webfingerResolve(raw);
     821    if (!actorUrl) return { error: 'not_found' };
     822    kind = 'actor'; target = actorUrl; label = raw.startsWith('@') ? raw : ('@' + raw);
     823  } else { kind = 'domain'; target = raw.toLowerCase(); label = raw.toLowerCase(); }
     824  blStmts().ins.run(site.slug, target, kind, label);
     825  purgeBlocked(kind, target);
     826  console.log('[AP] block', site.slug, kind, target);
     827  return { ok: true, label };
     828}
     829
     830export function unblock(site, target) { blStmts().del.run(site.slug, target); return { ok: true }; }
     831
    776832export default {
    777833  getOrCreateKeys, apWants, sendAP, actorId, noteId,
     
    781837  listOutbox, deliverOutboxDelete,
    782838  webfingerResolve, followActor, unfollowActor, listFollowing, getTimeline, sendInteraction,
    783   getNotifications,
     839  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    784840};
Note: See TracChangeset for help on using the changeset viewer.