Changeset 952baf3 in Klonkt for src/services/guardianship
- Timestamp:
- 08/07/2026 05:15:52 PM (5 weeks ago)
- Branches:
- main
- Children:
- ba76bf5
- Parents:
- f85b2c3 (diff), 0d5bd2c (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the(diff)links above to see all the changes relative to each parent. - Location:
- src/services/guardianship
- Files:
-
- 1 added
- 5 edited
Legend:
- Unmodified
- Added
- Removed
-
src/services/guardianship/delivery.js
rf85b2c3 r952baf3 41 41 // guardian on any instance receives it as a private mention (the ward 42 42 // call-for-help path). 43 export async function deliverDirectNote(site, { recipients, text, language, inReplyTo, attachments, helpRequest, wave, awayUntil}) {43 export async function deliverDirectNote(site, { recipients, text, html, language, inReplyTo, attachments, helpRequest, wave, awayUntil, helpMark }) { 44 44 const { actorId, fetchActor, localActor, deliverTo, deriveHandle, escHtml, linkUrls, linkHashtags, 45 45 getOutboxRow, buildReplyNote, AP_CONTEXT, getOrCreateKeys, deliver, enqueueDelivery } = deps; … … 83 83 return `<a href="${escHtml(r.url)}" class="u-url mention" data-actor="${escHtml(r.uri)}">${escHtml(disp)}</a> `; 84 84 }).join(''); 85 // Rijk antwoord: `html` is de HTML uit de reply-editor, hier gesaneerd; `text` 86 // blijft de platte versie (het `source`-veld en de no-JS-fallback). Levert de 87 // sanitizer niets bruikbaars op, dan valt hij terug op de escaped tekst -- 88 // een leeggepoetste editor mag geen leeg bericht versturen. 89 const richClean = html ? deps.sanitizeHtml(String(html)) : ''; 90 const rich = richClean && deps.htmlToPlainText(richClean).trim() ? richClean : ''; 85 91 const body = escHtml(String(text).trim()).replace(/\r?\n/g, '<br>'); 86 const content = `<p>${mention}${linkUrls(linkHashtags(base, body))}</p>`; 92 // De mention-anker blijft een eigen alinea vooraan: de ontvanger moet in het 93 // bericht genoemd staan, ook als de rijke inhoud met een kop of lijst begint. 94 const content = rich 95 ? `<p>${mention}</p>${linkUrls(linkHashtags(base, rich))}` 96 : `<p>${mention}${linkUrls(linkHashtags(base, body))}</p>`; 87 97 const lang = /^[a-z]{2,3}(-[A-Za-z0-9-]+)?$/.test(String(language || '')) ? language : null; 88 98 // Attachments: same rules as deliverReply (own /media/ uploads only, … … 99 109 const row = getOutboxRow(id); 100 110 const note = buildReplyNote(base, site, row); 111 // Markering op een hulpvraag (shaer-lgo): een gewone directe note die er een 112 // shaer:-eigenschap bij draagt, net als de zwaai. Zo reist het over dezelfde 113 // bezorging, ziet de ward het als bericht ("er komt iemand"), en houden de 114 // mede-guardians er staat aan over. 115 if (helpMark && helpMark.noteUri) { 116 note[helpMark.kind === 'handled' ? 'shaer:helpHandled' : 'shaer:helpPickup'] = helpMark.noteUri; 117 } 101 118 const create = { 102 119 '@context': AP_CONTEXT, -
src/services/guardianship/follows.js
rf85b2c3 r952baf3 81 81 _r = { 82 82 ins: db.prepare(`INSERT OR IGNORE INTO ap_follow_reviews 83 (id, guardian_slug, ward_uri, ward_inbox, follower_uri, follower_handle, follower_icon, follow_json, created_at) 84 VALUES (?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`), 83 (id, guardian_slug, ward_uri, ward_inbox, follower_uri, follower_handle, follower_icon, follow_json, 84 direction, target_uri, target_handle, created_at) 85 VALUES (?,?,?,?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`), 85 86 get: db.prepare('SELECT * FROM ap_follow_reviews WHERE guardian_slug = ? AND id = ?'), 86 87 bySlug: db.prepare("SELECT * FROM ap_follow_reviews WHERE guardian_slug = ? AND status = 'pending' ORDER BY created_at DESC"), … … 91 92 } 92 93 94 /** 95 * De guardian-zijdige kopie van een gate-verzoek op een REMOTE ward. 96 * 97 * `direction` is niet cosmetisch (shaer-jdb). Bij een INKOMENDE is de follower 98 * iemand anders en de ward het doel. Bij een UITGAANDE is de ward zelf de 99 * follower en staat het doel in het Follow-object -- die werd hiervoor 100 * opgeslagen als "deze ward wil deze ward volgen", met het doel weggegooid. 101 */ 93 102 export function recordReview(guardianSlug, r) { 94 rstmts().ins.run(r.id, guardianSlug, r.wardUri, r.wardInbox || null, r.follower, r.followerHandle || null, r.followerIcon || null, r.followJson || null); 103 const richting = r.direction === 'outgoing' ? 'outgoing' : 'incoming'; 104 rstmts().ins.run(r.id, guardianSlug, r.wardUri, r.wardInbox || null, r.follower, r.followerHandle || null, 105 r.followerIcon || null, r.followJson || null, richting, r.target || null, r.targetHandle || null); 95 106 return rstmts().get.get(guardianSlug, r.id); 107 } 108 109 /** 110 * Een openstaande review als wachtrij-item, in dezelfde vorm die de clients al 111 * lezen (offers en outgoing-follows doen het net zo). 112 */ 113 export function reviewQueueItem(r, me, guardianCount) { 114 // guardianCount blijft WEG als we hem niet kennen. Bij een remote ward wordt 115 // de guardian-set op diens eigen server bijgehouden, en 0 sturen zou lezen als 116 // "dit kind heeft geen guardians" -- het tegenovergestelde van onbekend. 117 const stemmen = (() => { 118 try { return db.prepare('SELECT guardian_uri, decision FROM ap_pending_follow_approvals WHERE follow_id = ?').all(r.id); } 119 catch { return []; } 120 })(); 121 const uitgaand = r.direction === 'outgoing'; 122 return { 123 id: r.id, 124 type: 'Follow', 125 // Bij een uitgaande is de WARD de volger; bij een inkomende is dat de vreemde. 126 actor: uitgaand ? r.ward_uri : r.follower_uri, 127 object: uitgaand ? (r.target_uri || '') : r.ward_uri, 128 'shaer:direction': uitgaand ? 'outgoing' : 'incoming', 129 'shaer:ward': r.ward_uri, 130 'shaer:target': uitgaand ? (r.target_uri || undefined) : undefined, 131 'shaer:targetHandle': uitgaand ? (r.target_handle || undefined) : undefined, 132 'shaer:follower': uitgaand ? undefined : r.follower_uri, 133 'shaer:followerHandle': uitgaand ? undefined : (r.follower_handle || undefined), 134 'shaer:quorum': 'all', 135 'shaer:approvals': stemmen.filter((x) => x.decision === 'approve').length, 136 'shaer:guardianCount': guardianCount || undefined, 137 'shaer:myVote': stemmen.some((x) => x.guardian_uri === me), 138 published: r.created_at, 139 }; 140 } 141 142 /** De openstaande reviews van een guardian, per richting. */ 143 export function listReviewsByDirection(guardianSlug, direction) { 144 return listReviews(guardianSlug).filter((r) => (r.direction === 'outgoing' ? 'outgoing' : 'incoming') === direction); 96 145 } 97 146 export function getReview(guardianSlug, id) { return rstmts().get.get(guardianSlug, id); } … … 102 151 recordPending, getPending, listForWard, decide, remove, 103 152 recordReview, getReview, listReviews, removeReview, 153 listReviewsByDirection, reviewQueueItem, 104 154 }; -
src/services/guardianship/gated.js
rf85b2c3 r952baf3 60 60 'shaer:externalPlayback': 'external_playback', 61 61 }; 62 /** 63 * De gates die deze Klonkt kent, met hun SOORT. 64 * 65 * Wat gated wordt is een ontwerpkeuze van de implementatie: de FEP levert het 66 * mechanisme (voorstel, tally, settle) en een paar voorbeelden, niet de lijst. 67 * Deze catalogus is die lijst, op een plek. Een gate erbij hoort een regel data 68 * te zijn en geen nieuw stuk scherm. 69 * 70 * `kind` is niet decoratief. De gates verschillen in hoe ze werken en dat mag 71 * een guardian niet hoeven raden: 72 * 73 * setting een stand, aan of uit, terug te draaien 74 * perRequest geen stand maar een stroom beslissingen (5.3 volgverzoeken) 75 * handover draagt gezag OVER; onomkeerbaar zodra de ward hem gebruikt 76 * 77 * `needs` is de trap uit shaer-ahy: zien < afspelen. Je kunt niet afspelen wat 78 * je niet mag zien, dus dat tweede is pas te bewegen als het eerste openstaat. 79 */ 80 export const GATE_CATALOGUE = [ 81 { feature: 'shaer:externalEmbeds', kind: 'setting', reversible: true }, 82 { feature: 'shaer:externalPlayback', kind: 'setting', reversible: true, needs: 'shaer:externalEmbeds' }, 83 // Altijd aan voor een ward (5.3): niet te verzetten, wel te tonen. Een paneel 84 // dat alleen verstelbare dingen laat zien verzwijgt de helft van wat er geldt. 85 { feature: 'shaer:follows', kind: 'perRequest', reversible: true, fixed: true }, 86 ]; 87 88 /** 89 * De gates van een ward als rijen voor het paneel. Puur, zodat de regels 90 * getoetst kunnen worden zonder database of scherm. 91 * 92 * @param settings {feature: true|false|null} -- null is ONBEKEND, niet uit 93 * @param guardianCount aantal guardians, of null als we het niet weten 94 * @param proposals [{feature, value, status}] lopende voorstellen 95 * @param waiting {feature: aantal} wat er per gate op een besluit wacht 96 */ 97 export function gateRows({ settings = {}, guardianCount = null, proposals = [], waiting = {} } = {}) { 98 return GATE_CATALOGUE.map((g) => { 99 const value = Object.prototype.hasOwnProperty.call(settings, g.feature) ? settings[g.feature] : null; 100 // De trap: het bovenliggende moet OPEN staan. Onbekend telt niet als dicht -- 101 // bij een ward elders kennen we de stand niet, en verbergen betekende daar 102 // ooit dat een voorstel nooit geopend kon worden. 103 const blockedBy = g.needs && settings[g.needs] === false ? g.needs : null; 104 return { 105 feature: g.feature, 106 kind: g.kind, 107 reversible: !!g.reversible, 108 value, 109 // Vast staat vast: tonen mag, verzetten niet. 110 adjustable: !g.fixed && !blockedBy, 111 blockedBy: blockedBy || undefined, 112 // Zonder bekend aantal guardians GEEN drempel verzinnen. Nul of een gok 113 // leest als een feit, en dit is precies waar een guardian op afgaat. 114 threshold: (guardianCount && guardianCount > 0) 115 ? { need: thresholdFor(guardianCount), of: guardianCount } : null, 116 proposal: proposals.find((p) => p.feature === g.feature) || undefined, 117 waiting: waiting[g.feature] || undefined, 118 }; 119 }); 120 } 121 62 122 export function featureColumn(feature) { 63 123 return Object.prototype.hasOwnProperty.call(FEATURES, feature) ? FEATURES[feature] : null; … … 239 299 240 300 export default { 301 GATE_CATALOGUE, gateRows, 241 302 tallyGatedSetting, thresholdFor, featureColumn, recordGatedVote, gatedProgress, GATED_WINDOW_MS, 242 303 parseGatedSetting, buildGatedOffer, rememberGatedOffer, recallGatedOffer, -
src/services/guardianship/index.js
rf85b2c3 r952baf3 32 32 // §5.6 gated settings (decided by the guardians, enforced by the ward's server) 33 33 export * as gated from './gated.js'; 34 // 5.2.1: wie er op een hulpvraag af is en wanneer hij is afgesloten (shaer-lgo) 35 export * as help from './help.js'; -
src/services/guardianship/queues.js
rf85b2c3 r952baf3 6 6 * - offers: pending handshake offers where I am a party (§3), with the full 7 7 * accept tally so the client shows the right action 8 * - follows: pending gated follows for my wards (§5.3) — Fase 2, empty for now8 * - follows: pending gated follows ON my wards (§5.3), Fase 2 (shaer-jdb) 9 9 * - wards: my committed wards 10 10 */ … … 13 13 import * as availability from './availability.js'; 14 14 import * as outgoing from './outgoing.js'; 15 import * as follows from './follows.js'; 15 16 import * as handshake from './handshake.js'; 16 17 … … 35 36 } 36 37 37 /** Gated follows awaiting guardian approval — not built in Klonkt yet (Fase 2). */ 38 export function followsCollection(id) { 39 return collection(id, []); 38 /** 39 * Gate-verzoeken OP mijn wards die op mijn antwoord wachten (Guardianship Fase 2, 40 * shaer-jdb). Dit was een lege stub: de gating zelf werkt sinds shaer-hxg, maar 41 * werd nooit aan een C2S-client doorgegeven omdat de koers toen op de PWA lag. 42 * 43 * Twee bronnen, want een guardian kan wards op andere servers hebben en (nog) 44 * op deze: 45 * - ap_follow_reviews: de doorgestuurde kopie van een REMOTE ward 46 * - ap_pending_follows: een ward op deze instance 47 * Zie shaer-h6u: die tweede hoort op termijn ook over de lijn te gaan. 48 */ 49 export function followsCollection(id, slug, me) { 50 const items = follows.listReviewsByDirection(slug, 'incoming') 51 .map((r) => follows.reviewQueueItem(r, me)); 52 for (const w of relations.listWards(slug)) { 53 const wardSlug = slugOf(w.other_uri); 54 if (!wardSlug) continue; 55 for (const p of follows.listForWard(wardSlug)) { 56 items.push({ 57 id: p.id, type: 'Follow', actor: p.follower_uri, object: w.other_uri, 58 'shaer:direction': 'incoming', 'shaer:ward': w.other_uri, 59 'shaer:follower': p.follower_uri, 'shaer:followerHandle': p.follower_handle || undefined, 60 'shaer:quorum': p.quorum || 'any', published: p.created_at, 61 }); 62 } 63 } 64 return collection(id, items); 40 65 } 41 66 42 /** §5.3 outbound: this ward's own follow requests, waiting for its guardians. */ 67 /** De slug van een actor-uri op DEZE instance, of null als hij elders woont. */ 68 function slugOf(uri) { 69 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 70 if (!base || !String(uri || '').startsWith(`${base}/ap/users/`)) return null; 71 return decodeURIComponent(String(uri).slice(`${base}/ap/users/`.length).split(/[/?#]/)[0]) || null; 72 } 73 74 /** 75 * §5.3 uitgaand. Twee lezers, een wachtrij, en dat kan omdat §1 een ward en een 76 * guardian wederzijds uitsluit: je bent het een of het ander. 77 * 78 * ALS WARD wat IK wil volgen en waar mijn guardians nog over moeten 79 * ALS GUARDIAN wat mijn WARDS willen volgen en waar IK over moet (shaer-jdb) 80 * 81 * Dat tweede ontbrak. De wachtrij serveerde alleen listForWard(slug), en voor 82 * een guardian is dat per definitie leeg -- dus het scherm "Your wards want to 83 * follow" kon nooit iets tonen. 84 */ 43 85 export function outgoingFollowsCollection(id, slug, me) { 44 return collection(id, outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me))); 86 const items = outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me)); 87 for (const r of follows.listReviewsByDirection(slug, 'outgoing')) { 88 items.push(follows.reviewQueueItem(r, me)); 89 } 90 return collection(id, items); 45 91 } 46 92
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)