source: Klonkt/src/services/BlocklistService.js@ e311202

main
Last change on this file since e311202 was e311202, checked in by Robin <roboburr@…>, 3 weeks ago

A block now leaves the house: Block out, Undo(Block) back

Blocking was purely local — row in ap_blocks, purge the cached content,
drop the follower — and the other side never heard about it. The hub
kept the channel and every post, because nothing told it and its
unsigned crawl kept reading the public outbox (seen on dev.klonkt.com,
21-8).

Now blocking delivers a Block to the blocked actor's inbox and
unblocking an Undo(Block), so the way back stays open. Delivery can
never hold up the block itself: the row is written first and a
failed delivery is swallowed. Domain blocks send nothing — no inbox to
address.

Second door: a signed reader we block gets the same 404 on /ap/notes/:id
that a stranger gets, matching what the outbox already did. Unsigned
callers stay anonymous to us and keep the public view; that is what the
Block delivery is for.

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

  • Property mode set to 100644
File size: 5.0 KB
Line 
1/**
2 * The instance blocklist (ap_blocks): actors and whole domains a site has
3 * blocked. Lives NEXT TO the guardianship module, not inside it, because it
4 * is shared: Klonkt's own Block tab uses it, and Shaer's "in Orbit" reads it
5 * as the source of truth (AP §5.6 blocked collection, owner-only).
6 *
7 * Extracted from ActivityPubService (guardianship refactor); behavior is
8 * unchanged. ActivityPubService re-exports these under the old names so
9 * existing callers keep working.
10 */
11import db from '../config/database.js';
12
13let _insBl, _delBl, _listBl;
14function blStmts() {
15 if (!_insBl) {
16 _insBl = db.prepare('INSERT OR IGNORE INTO ap_blocks (slug, target, kind, label, created_at) VALUES (?,?,?,?,CURRENT_TIMESTAMP)');
17 _delBl = db.prepare('DELETE FROM ap_blocks WHERE slug = ? AND target = ?');
18 _listBl = db.prepare('SELECT * FROM ap_blocks WHERE slug = ? ORDER BY created_at DESC');
19 }
20 return { ins: _insBl, del: _delBl, list: _listBl };
21}
22
23export function listBlocks(slug) { return blStmts().list.all(slug); }
24
25// True if an actor (or its whole domain) is blocked anywhere on this instance.
26export function isBlockedAny(actorUri) {
27 if (!actorUri) return false;
28 let domain = ''; try { domain = new URL(actorUri).host; } catch { /* ignore */ }
29 try { return !!db.prepare("SELECT 1 FROM ap_blocks WHERE (kind='actor' AND target=?) OR (kind='domain' AND target=?) LIMIT 1").get(actorUri, domain); }
30 catch { return false; }
31}
32
33function purgeBlocked(kind, target) {
34 try {
35 if (kind === 'domain') {
36 // Exact host match (a URL LIKE over-/under-matches: it misses bare-domain or :port
37 // actor URIs and can catch look-alikes). Filter by parsed host, same as isBlockedAny.
38 const purge = (table, col) => {
39 let rows = [];
40 try { rows = db.prepare(`SELECT DISTINCT ${col} AS u FROM ${table} WHERE ${col} IS NOT NULL AND ${col} != ''`).all(); } catch { return; }
41 const del = db.prepare(`DELETE FROM ${table} WHERE ${col} = ?`);
42 for (const r of rows) { let h = ''; try { h = new URL(r.u).host; } catch { /* skip */ } if (h === target) { try { del.run(r.u); } catch { /* ignore */ } } }
43 };
44 purge('ap_interactions', 'actor_uri');
45 purge('ap_timeline', 'author_uri');
46 purge('ap_followers', 'actor_uri');
47 } else {
48 db.prepare('DELETE FROM ap_interactions WHERE actor_uri = ?').run(target);
49 db.prepare('DELETE FROM ap_timeline WHERE author_uri = ?').run(target);
50 db.prepare('DELETE FROM ap_followers WHERE actor_uri = ?').run(target);
51 }
52 } catch { /* best-effort */ }
53}
54
55// Block an actor (@handle or actor URL) or a whole domain; purges their content.
56// `resolveHandle` (async handle → actor URL) is injected by the caller so this
57// service needs nothing from ActivityPubService (no circular import).
58/**
59 * Een blokkade is pas een blokkade als de ander het merkt (Robin, 21-8).
60 *
61 * Tot vandaag bleef hij binnenshuis: rij in ap_blocks, inhoud opruimen, volger
62 * eruit -- en verder niets. De andere kant volgde je dan nog steeds in zijn
63 * eigen boeken en bleef je publieke outbox lezen. Precies wat de hub deed:
64 * kanaal en berichten stonden er gewoon nog. Vandaar dat we het nu ook
65 * VERSTUREN, zoals Mastodon dat doet: een Block naar de inbox van wie je
66 * blokkeert, en bij opheffen een Undo(Block) zodat de weg terug openligt.
67 *
68 * Alleen voor een actor-blokkade: een heel domein heeft geen inbox om aan te
69 * schrijven. En bezorgen mag nooit de blokkade zelf tegenhouden -- die staat
70 * al vast in de database voordat we ook maar iets proberen te versturen.
71 */
72async function meldBlokkade(site, target, kind, bezorg, undo = false) {
73 if (kind !== 'actor' || typeof bezorg !== 'function') return;
74 try { await bezorg(site, target, undo); } catch { /* de blokkade staat, de melding is een gunst */ }
75}
76
77export async function blockTarget(site, input, resolveHandle, bezorg) {
78 const raw = String(input || '').trim();
79 if (!site || !site.slug || !raw) return { error: 'empty' };
80 let kind, target, label;
81 if (/^https?:\/\//i.test(raw)) { kind = 'actor'; target = raw; label = raw; }
82 else if (raw.includes('@')) {
83 const actorUrl = resolveHandle ? await resolveHandle(raw) : null;
84 if (!actorUrl) return { error: 'not_found' };
85 kind = 'actor'; target = actorUrl; label = raw.startsWith('@') ? raw : ('@' + raw);
86 } else { kind = 'domain'; target = raw.toLowerCase(); label = raw.toLowerCase(); }
87 blStmts().ins.run(site.slug, target, kind, label);
88 purgeBlocked(kind, target);
89 console.log('[AP] block', site.slug, kind, target);
90 await meldBlokkade(site, target, kind, bezorg);
91 return { ok: true, label };
92}
93
94export async function unblock(site, target, bezorg) {
95 const rij = blStmts().list.all(site.slug).find((b) => b.target === target);
96 blStmts().del.run(site.slug, target);
97 // Undo(Block) zodat de ander weet dat de deur weer open is; zonder dit blijft
98 // hij bij zichzelf geblokkeerd staan en komt hij nooit terug.
99 await meldBlokkade(site, target, (rij && rij.kind) || 'actor', bezorg, true);
100 return { ok: true };
101}
102
103export default { listBlocks, isBlockedAny, blockTarget, unblock };
Note: See TracBrowser for help on using the repository browser.