source: Klonkt/src/services/guardianship/follows.js@ 3a0ca0f

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

De volgpoort zat alleen in de app, en volgverzoeken beslissen bij meerderheid

Barts twee meldingen (8-8), en de eerste is een echte vondst.

  1. WAAROM ESMEE'S VOLGVERZOEKEN NOOIT AANKWAMEN

Niet wat Bart vreesde -- het was niet zo dat alleen een intern account ze kon
beantwoorden. Die weg werkt: gateOutgoingFollow levert een Offer af bij een
guardian elders, en dat is precies wat dev als guardian van @mee had moeten
krijgen.

Er kwam alleen nooit iets om af te leveren. De poort stond in case 'Follow' van
de C2S-outbox, dus alleen als je via Shaer volgt. Volgde het kind vanuit Klonkts
eigen webinterface, dan werd er geen verzoek aangemaakt, ging er niets naar de
guardians, en was er dus ook niets te beantwoorden. Op dev staat geen enkele rij
in ap_follow_reviews en nergens een follow-approval-regel in de log.

Exact dezelfde deur-naast-de-poort als bij de antwoordpoort vanmiddag: de gate in
C2S, het webpad eromheen. De controle staat nu in followActor zelf, NA het
oplossen van de handle -- zou hij alleen naar de ruwe invoer kijken, dan is elke
@naam@server een sluiproute. approved is de enige doorlaat, voor
performApprovedFollow; zonder dat stuit een goedgekeurd verzoek opnieuw op de
poort en wacht het voor eeuwig.

Drie webroutes zeiden "Je volgt nu X" terwijl het verzoek bij de guardians lag.
Dat is de leugen die de poort waardeloos maakt: het kind denkt dat het gebeurd
is. Er is nu een derde uitkomst.

DE MUTATIE GAF EERST NUL FOUTEN -- er stond niets op deze poort. Nu vier toetsen,
inclusief het handle-geval en het goedgekeurde pad.

  1. VOLGVERZOEKEN BIJ EENVOUDIGE MEERDERHEID

Barts besluit: 1 van 2 is voldoende. followThreshold = ceil(n/2), voor beide
richtingen, want "mag dit kind met deze persoon te maken hebben" is dezelfde
vraag en twee drempels zou een guardian nooit kunnen uitleggen.

BEWUST SOEPELER DAN DE POORTDREMPEL (2 van 2): een gate opent een deur voor alles
wat daarna komt, een volgverzoek gaat over een persoon en is met ontvolgen terug
te draaien. En het is geen versoepeling maar een AANSCHERPING -- dit stond op
'any', een enkele ja hoeveel guardians er ook waren.

De 'all'-stand is weg: hij werd nergens gezet, dus was het een keuze die niemand
kon maken. De toets die hem bewaakte is herschreven, niet verwijderd.

Race naar de drempel zoals de poorttelling, met dezelfde TODO erbij (shaer-8vt):
wie antwoordt weet niet dat hij de doorslag geeft.

Suite 742/742; beide mutaties rood.

  • Property mode set to 100644
File size: 8.4 KB
Line 
1/**
2 * Guardianship (FEP-633c §5.3) — follow-gating for wards.
3 *
4 * A `Follow` targeting a ward is NOT auto-accepted. It is held pending and
5 * routed to the ward's guardians, who approve or deny. A committed guardian's
6 * own Follow is auto-accepted (it needs no gate). Quorum policy per ward:
7 * 'any' (one guardian suffices, default), 'all', or 'none' (open).
8 *
9 * This module is the store + the decision; the AP plumbing (sending the
10 * Accept, inserting the follower) stays in ActivityPubService.
11 */
12import db from '../../config/database.js';
13
14let _s = null;
15function stmts() {
16 if (!_s) {
17 _s = {
18 ins: db.prepare(`INSERT OR IGNORE INTO ap_pending_follows
19 (id, ward_slug, follower_uri, follower_inbox, follower_shared_inbox, follower_name, follower_handle, follower_icon, activity_json, quorum, created_at)
20 VALUES (?,?,?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`),
21 get: db.prepare('SELECT * FROM ap_pending_follows WHERE id = ?'),
22 byWard: db.prepare("SELECT * FROM ap_pending_follows WHERE ward_slug = ? AND status = 'pending' ORDER BY created_at DESC"),
23 approvers: db.prepare('SELECT guardian_uri FROM ap_pending_follow_approvals WHERE follow_id = ?'),
24 approve: db.prepare('INSERT OR IGNORE INTO ap_pending_follow_approvals (follow_id, guardian_uri, decision, created_at) VALUES (?,?,?,CURRENT_TIMESTAMP)'),
25 setStatus: db.prepare('UPDATE ap_pending_follows SET status = ? WHERE id = ?'),
26 del: db.prepare('DELETE FROM ap_pending_follows WHERE id = ?'),
27 };
28 }
29 return _s;
30}
31
32/** Record a gated follow awaiting guardian approval. */
33export function recordPending(wardSlug, f) {
34 stmts().ins.run(
35 f.id, wardSlug, f.follower, f.inbox, f.sharedInbox || null,
36 f.name || null, f.handle || null, f.icon || null,
37 JSON.stringify(f.activity || null), f.quorum || 'any',
38 );
39 return stmts().get.get(f.id);
40}
41
42export function getPending(id) { return stmts().get.get(id); }
43
44/** Pending follows for a local ward (its guardians decide). */
45export function listForWard(wardSlug) { return stmts().byWard.all(wardSlug); }
46
47/**
48 * Record a guardian's decision on a pending follow. Returns
49 * { outcome: 'approved'|'rejected'|'waiting', follow } so the caller can
50 * send the Accept/Reject. A single reject denies; approvals meet the quorum.
51 */
52/**
53 * Hoeveel guardians moeten ja zeggen voor een volgverzoek (Barts besluit, 8-8).
54 *
55 * EENVOUDIGE MEERDERHEID: 1 van 1, 1 van 2, 2 van 3, 2 van 4. Bart: "1/2 is
56 * voldoende."
57 *
58 * BEWUST SOEPELER DAN DE POORTDREMPEL, en dat verschil hoort uitgelegd. Een gate
59 * opent een deur voor alles wat daarna komt; die vraagt om een STRIKTE
60 * meerderheid (thresholdFor in gated.js: 2 van 2, 3 van 4). Een volgverzoek gaat
61 * over een persoon, is met ontvolgen terug te draaien, en stond hier tot vandaag
62 * op 'any' -- een enkele ja, hoeveel guardians er ook waren. Dit is dus geen
63 * versoepeling maar een AANSCHERPING voor iedereen met drie of meer guardians.
64 *
65 * De 'all'-stand die hier stond is weg. Hij werd nergens gezet -- elke schrijver
66 * gaf 'any' mee -- dus het was een keuze die niemand kon maken en die alleen in
67 * de weg stond bij het lezen van deze regel.
68 */
69export function followThreshold(setSize) {
70 return Math.max(1, Math.ceil(setSize / 2));
71}
72
73/**
74 * Een race naar de drempel, net als de poorttelling: zodra het aantal gehaald
75 * is, is het besluit gevallen.
76 *
77 * TODO (shaer-8vt): wie antwoordt weet niet dat hij de doorslag geeft. Bij 1 van
78 * 2 is de eerste ja meteen de beslissing, en het scherm zegt dat nergens. Dat is
79 * hetzelfde gat als bij de gate-voorstellen en het hoort daar samen opgelost.
80 */
81export function decide(id, guardianUri, decision, guardiansOfWard) {
82 const follow = stmts().get.get(id);
83 if (!follow || follow.status !== 'pending') return { outcome: 'gone', follow };
84 stmts().approve.run(id, guardianUri, decision === 'reject' ? 'reject' : 'approve');
85 const rows = db.prepare('SELECT guardian_uri, decision FROM ap_pending_follow_approvals WHERE follow_id = ?').all(id);
86 if (rows.some((r) => r.decision === 'reject')) {
87 stmts().setStatus.run('denied', id);
88 return { outcome: 'rejected', follow };
89 }
90 const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri));
91 const guardians = (guardiansOfWard || []).filter(Boolean);
92 const enough = approvers.size >= followThreshold(guardians.length);
93 if (enough) {
94 stmts().setStatus.run('accepted', id);
95 return { outcome: 'approved', follow };
96 }
97 return { outcome: 'waiting', follow };
98}
99
100export function remove(id) { stmts().del.run(id); }
101
102// ── Guardian-side copy (cross-instance, modelled on the guardian offer): a
103// gated follow on a REMOTE ward this account guards, forwarded here as an
104// Offer(Follow). The decision is Accept/Reject sent back to ward_inbox. ──
105let _r = null;
106function rstmts() {
107 if (!_r) {
108 _r = {
109 ins: db.prepare(`INSERT OR IGNORE INTO ap_follow_reviews
110 (id, guardian_slug, ward_uri, ward_inbox, follower_uri, follower_handle, follower_icon, follow_json,
111 direction, target_uri, target_handle, created_at)
112 VALUES (?,?,?,?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`),
113 get: db.prepare('SELECT * FROM ap_follow_reviews WHERE guardian_slug = ? AND id = ?'),
114 bySlug: db.prepare("SELECT * FROM ap_follow_reviews WHERE guardian_slug = ? AND status = 'pending' ORDER BY created_at DESC"),
115 del: db.prepare('DELETE FROM ap_follow_reviews WHERE guardian_slug = ? AND id = ?'),
116 };
117 }
118 return _r;
119}
120
121/**
122 * De guardian-zijdige kopie van een gate-verzoek op een REMOTE ward.
123 *
124 * `direction` is niet cosmetisch (shaer-jdb). Bij een INKOMENDE is de follower
125 * iemand anders en de ward het doel. Bij een UITGAANDE is de ward zelf de
126 * follower en staat het doel in het Follow-object -- die werd hiervoor
127 * opgeslagen als "deze ward wil deze ward volgen", met het doel weggegooid.
128 */
129export function recordReview(guardianSlug, r) {
130 const richting = r.direction === 'outgoing' ? 'outgoing' : 'incoming';
131 rstmts().ins.run(r.id, guardianSlug, r.wardUri, r.wardInbox || null, r.follower, r.followerHandle || null,
132 r.followerIcon || null, r.followJson || null, richting, r.target || null, r.targetHandle || null);
133 return rstmts().get.get(guardianSlug, r.id);
134}
135
136/**
137 * Een openstaande review als wachtrij-item, in dezelfde vorm die de clients al
138 * lezen (offers en outgoing-follows doen het net zo).
139 */
140export function reviewQueueItem(r, me, guardianCount) {
141 // guardianCount blijft WEG als we hem niet kennen. Bij een remote ward wordt
142 // de guardian-set op diens eigen server bijgehouden, en 0 sturen zou lezen als
143 // "dit kind heeft geen guardians" -- het tegenovergestelde van onbekend.
144 const stemmen = (() => {
145 try { return db.prepare('SELECT guardian_uri, decision FROM ap_pending_follow_approvals WHERE follow_id = ?').all(r.id); }
146 catch { return []; }
147 })();
148 const uitgaand = r.direction === 'outgoing';
149 return {
150 id: r.id,
151 type: 'Follow',
152 // Bij een uitgaande is de WARD de volger; bij een inkomende is dat de vreemde.
153 actor: uitgaand ? r.ward_uri : r.follower_uri,
154 object: uitgaand ? (r.target_uri || '') : r.ward_uri,
155 'shaer:direction': uitgaand ? 'outgoing' : 'incoming',
156 'shaer:ward': r.ward_uri,
157 'shaer:target': uitgaand ? (r.target_uri || undefined) : undefined,
158 'shaer:targetHandle': uitgaand ? (r.target_handle || undefined) : undefined,
159 'shaer:follower': uitgaand ? undefined : r.follower_uri,
160 'shaer:followerHandle': uitgaand ? undefined : (r.follower_handle || undefined),
161 'shaer:quorum': 'all',
162 'shaer:approvals': stemmen.filter((x) => x.decision === 'approve').length,
163 'shaer:guardianCount': guardianCount || undefined,
164 'shaer:myVote': stemmen.some((x) => x.guardian_uri === me),
165 published: r.created_at,
166 };
167}
168
169/** De openstaande reviews van een guardian, per richting. */
170export function listReviewsByDirection(guardianSlug, direction) {
171 return listReviews(guardianSlug).filter((r) => (r.direction === 'outgoing' ? 'outgoing' : 'incoming') === direction);
172}
173export function getReview(guardianSlug, id) { return rstmts().get.get(guardianSlug, id); }
174export function listReviews(guardianSlug) { return rstmts().bySlug.all(guardianSlug); }
175export function removeReview(guardianSlug, id) { rstmts().del.run(guardianSlug, id); }
176
177export default {
178 recordPending, getPending, listForWard, decide, remove,
179 recordReview, getReview, listReviews, removeReview,
180 listReviewsByDirection, reviewQueueItem,
181};
Note: See TracBrowser for help on using the repository browser.