Changeset f5c3870 in Klonkt for src/services


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

Location:
src/services
Files:
2 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};
  • src/services/i18n.js

    rd0cab9d rf5c3870  
    2626    'nav.language': 'Taal',
    2727    'nav.notifications': 'Meldingen',
    28     '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',
     28    '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',
    2929    'notif.reply': '{actor} reageerde op je reactie', 'notif.comment': '{actor} reageerde op je post', 'notif.like': '{actor} vindt je post leuk',
    3030    'switch.agenda': 'Agenda',
     
    10291029    'nav.language': 'Language',
    10301030    'nav.notifications': 'Notifications',
    1031     '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',
     1031    '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',
    10321032    'notif.reply': '{actor} replied to your comment', 'notif.comment': '{actor} commented on your post', 'notif.like': '{actor} liked your post',
    10331033    'switch.agenda': 'Agenda',
     
    20232023    'nav.language': 'Sprache',
    20242024    'nav.notifications': 'Benachrichtigungen',
    2025     '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',
     2025    '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',
    20262026    'notif.reply': '{actor} hat auf deinen Kommentar geantwortet', 'notif.comment': '{actor} hat deinen Beitrag kommentiert', 'notif.like': '{actor} gefΓ€llt dein Beitrag',
    20272027    'switch.agenda': 'Termine',
Note: See TracChangeset for help on using the changeset viewer.