source: Klonkt/src/services/guardianship/delivery.js@ 6b5d7da

main
Last change on this file since 6b5d7da was 6b5d7da, checked in by Robin Genis <roboburr@…>, 7 weeks ago

Guardianship als eigen module + gedeelde BlocklistService

FEP-633c (Guardians) lag verspreid door ActivityPubService; nu is het één
cohesief onderdeel in src/services/guardianship/ met submodules. De
blocklist staat er bewust NAAST (BlocklistService): die wordt gedeeld met
Klonkt zelf (Block-tab) en is niet guardianship-specifiek.

De module importeert ActivityPubService nooit terug: de AP-helpers gaan er
één keer in via wireDelivery/wireHandshake, en de service delegeert met
dunne wrappers zodat elke bestaande aanroep blijft werken.

Naast de verhuizing ook de serverkant die nog miste (shaer-bh1): de
ap_guardianships-relaties, shaer:guardians/isGuardian/queues op het
actor-doc, en de adoptie-handshake Offer/Accept/Reject over C2S en S2S,
met het contract van de Shaer test-daemon zodat de iOS/Android-clients het
ongewijzigd spreken. Een inkomend hulpverzoek (shaer:helpRequest op een
directe mention) krijgt een eigen vlag in ap_mentions en pusht als
'help'-type richting de Guardian-PWA (volgende commit).

Changed files:
src/services/ActivityPubService.js

  • shaer-context, actor-props en helpRequest uit de module gespread
  • blocklist-functies zijn delegaties naar BlocklistService
  • c2sVisibility/deliverDirectNote re-export uit guardianship/delivery
  • C2S: Offer/Accept/Reject eerst langs de handshake-module
  • S2S: Offer aan GATED (signature-eis) + handshake-routering
  • inbound mention: help_request-vlag + 'help'/'guardian'-push-events

src/config/database.js

  • tabel ap_guardianships (slug, role, other_uri, status, offer_id)
  • kolom ap_mentions.help_request

test/activitypub-as2.test.js

  • shaer:queues/offers/follows/wards in de AS2-allowlist

New file:
src/services/BlocklistService.js

  • ap_blocks-opslag, blockTarget/unblock/listBlocks/isBlockedAny, purge; handle-resolver via injectie (geen circulaire import)

src/services/guardianship/index.js

  • de publieke API van het onderdeel

src/services/guardianship/context.js

  • shaer-namespace + Relationship-vocabulaire

src/services/guardianship/relations.js

  • ap_guardianships-API + actor-props (FEP-633c paragraaf 2)

src/services/guardianship/handshake.js

src/services/guardianship/queues.js

src/services/guardianship/notes.js

  • shaer:helpRequest lezen/schrijven

src/services/guardianship/delivery.js

  • de directe-note-route (call-for-help), gedrag ongewijzigd

remarks: alle 158 tests groen. push-teksten (push.n_help_*, push.n_guard_*)
en de queue-routes + Guardian-PWA volgen in de volgende commits.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 5.2 KB
Line 
1/**
2 * Guardianship (FEP-633c) — the direct-note delivery leg.
3 *
4 * A direct note (private mention, shaer-tqc) is the ward's call-for-help
5 * carrier: addressed to specific actors only, no Public, no followers
6 * fan-out. Moved here from ActivityPubService (guardianship refactor);
7 * behavior is unchanged.
8 *
9 * This module has NO import back into ActivityPubService: the AP helpers it
10 * needs (actor fetch, key material, delivery, note building) are provided
11 * once via wireDelivery(deps) at ActivityPubService load time.
12 */
13import crypto from 'crypto';
14import db from '../../config/database.js';
15
16const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
17
18let deps = null;
19/** Called once by ActivityPubService with the shared AP helpers. */
20export function wireDelivery(d) { deps = d; }
21
22// Addressing → visibility. Arrays or bare strings; unknown shapes read as the
23// safest bucket they match.
24export function c2sVisibility(object) {
25 const arr = (v) => (Array.isArray(v) ? v : (v ? [v] : [])).filter((x) => typeof x === 'string');
26 const to = arr(object.to), cc = arr(object.cc);
27 const isPublic = (x) => x === PUBLIC || x === 'as:Public' || x === 'Public';
28 const isFollowers = (x) => /\/followers\/?$/.test(x);
29 if (to.some(isPublic)) return 'public';
30 if (cc.some(isPublic)) return 'quiet';
31 if (to.some(isFollowers) || cc.some(isFollowers)) return 'friends';
32 if (!to.length && !cc.length) return 'public'; // no addressing at all: legacy client, keep old behavior
33 return 'direct';
34}
35
36// A direct note: a NEW conversation (or a direct reply) addressed to specific
37// actors only. Stored in ap_outbox with visibility 'direct' + the recipient
38// list, delivered to exactly those inboxes: no followers fan-out, no Public,
39// so no boosts and no timelines. The same S2S leg a Mastodon DM takes, so a
40// guardian on any instance receives it as a private mention (the ward
41// call-for-help path).
42export async function deliverDirectNote(site, { recipients, text, language, inReplyTo, attachments, helpRequest }) {
43 const { actorId, fetchActor, deriveHandle, escHtml, linkUrls, linkHashtags,
44 getOutboxRow, buildReplyNote, AP_CONTEXT, getOrCreateKeys, deliver, enqueueDelivery } = deps;
45 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
46 const list = [...new Set((recipients || []).filter((u) => /^https?:\/\//i.test(String(u || ''))))].slice(0, 8);
47 if (!base || !site || !site.slug || !list.length || !String(text || '').trim()) return null;
48 const me = actorId(base, site.slug);
49 // Resolve every recipient for a mention anchor + a delivery inbox.
50 const resolved = [];
51 for (const uri of list) {
52 const a = await fetchActor(uri).catch(() => null);
53 if (!a || !(a.inbox || (a.endpoints && a.endpoints.sharedInbox))) continue;
54 resolved.push({ uri, inbox: (a.endpoints && a.endpoints.sharedInbox) || a.inbox, handle: deriveHandle(uri), url: a.url || uri });
55 }
56 if (!resolved.length) return null;
57 const mention = resolved.map((r) => {
58 const disp = r.handle && r.handle[0] === '@' ? r.handle : '@' + (r.handle || '');
59 return `<a href="${escHtml(r.url)}" class="u-url mention" data-actor="${escHtml(r.uri)}">${escHtml(disp)}</a> `;
60 }).join('');
61 const body = escHtml(String(text).trim()).replace(/\r?\n/g, '<br>');
62 const content = `<p>${mention}${linkUrls(linkHashtags(base, body))}</p>`;
63 const lang = /^[a-z]{2,3}(-[A-Za-z0-9-]+)?$/.test(String(language || '')) ? language : null;
64 // Attachments: same rules as deliverReply (own /media/ uploads only,
65 // image/audio/video, max 4) — the help-buoy capture rides this.
66 const media = (Array.isArray(attachments) ? attachments : [])
67 .filter((a) => a && typeof a.url === 'string' && /^\/media\/[\w./-]+$/.test(a.url)
68 && /^(image|audio|video)\//.test(String(a.mediaType || '')))
69 .slice(0, 4)
70 .map((a) => ({ url: a.url, mediaType: String(a.mediaType), name: String(a.name || '').slice(0, 120) }));
71 const id = crypto.randomUUID();
72 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, language, attachments, visibility, to_actors, help_request, created_at)
73 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)`)
74 .run(id, site.slug, '', null, inReplyTo || null, resolved[0].uri, resolved[0].handle, content, lang, media.length ? JSON.stringify(media) : null, 'direct', JSON.stringify(resolved.map((r) => r.uri)), helpRequest ? 1 : 0);
75 const row = getOutboxRow(id);
76 const note = buildReplyNote(base, site, row);
77 const create = {
78 '@context': AP_CONTEXT,
79 id: note.id + '#create', type: 'Create', actor: me,
80 published: note.published, to: note.to, cc: note.cc, object: note,
81 };
82 const keys = getOrCreateKeys(site.slug);
83 const keyId = `${me}#main-key`;
84 let delivered = 0;
85 for (const inbox of [...new Set(resolved.map((r) => r.inbox))]) {
86 let ok = false;
87 try { const st = await deliver(inbox, create, keyId, keys.private_pem); ok = st >= 200 && st < 300; } catch { ok = false; }
88 if (ok) delivered++;
89 else enqueueDelivery(site.slug, inbox, create);
90 }
91 console.log('[AP] direct note', site.slug, '→', resolved.length, 'recipient(s), delivered', delivered);
92 return { id, content, delivered };
93}
94
95export default { wireDelivery, c2sVisibility, deliverDirectNote };
Note: See TracBrowser for help on using the repository browser.