Changeset 2b4252c in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 07/25/2026 09:15:34 AM (7 weeks ago)
- Branches:
- main
- Children:
- 4f15f67
- Parents:
- af5b79b
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
raf5b79b r2b4252c 1323 1323 } 1324 1324 1325 // FEP-633c §5.3 (modelled on the adoption offer): a gated follow forwarded to 1326 // the guardians as an Offer(Follow), their Accept/Reject back to the ward. 1327 if ((type === 'Offer' || type === 'Accept' || type === 'Reject') && act['shaer:followApproval'] === true) { 1328 if (await handleFollowApprovalInbox(act, slugParam)) { console.log('[AP] follow-approval', type, 'from', claimedActor); return 202; } 1329 } 1330 1325 1331 // FEP-633c: the adoption handshake. An Offer lands at the local ward; an 1326 1332 // Accept/Reject answers an offer a local guardian sent. Anything the … … 1393 1399 name: fi.name, handle: fi.handle, icon: fi.icon, activity: act, 1394 1400 }); 1395 // The ward and its guardians live together (the family Klonkt); a push 1396 // tells each local guardian to decide in /guardian2. Over the wire the 1397 // follow stays a normal pending Follow until they accept (Robins besluit: 1398 // keep the flow simple, no cross-instance forwarding). 1401 // FEP-633c §5.3, modelled on the guardian offer: the ward forwards the 1402 // gated follow to its guardians for approval. A LOCAL guardian gets a 1403 // push and reads /guardian2 directly; a REMOTE guardian gets an 1404 // Offer(Follow) delivered so its instance stores a copy (same distributed 1405 // pattern as the adoption offer). On quorum the ward returns Accept(Follow). 1406 const wardActor = actorId(base, slug); 1407 const wardKeys = getOrCreateKeys(slug); 1408 const followObj = { id: followId, type: 'Follow', actor: who, object: wardActor }; 1399 1409 for (const g of wardGuardians) { 1400 1410 const gslug = slugFromActorUrl(g); 1401 if (!gslug) continue; 1402 const L = pushLang(gslug); 1403 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: fi.name || fi.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian2` }); 1411 if (gslug) { 1412 const L = pushLang(gslug); 1413 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: fi.name || fi.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian2` }); 1414 } else { 1415 fetchActor(g).then((ga) => { 1416 const inbox = ga && ((ga.endpoints && ga.endpoints.sharedInbox) || ga.inbox); 1417 if (!inbox) return; 1418 const offer = { '@context': AP_CONTEXT, id: `${wardActor}#followoffer-${Date.now()}-${rid()}`, type: 'Offer', actor: wardActor, to: [g], object: followObj, 'shaer:followApproval': true }; 1419 deliverWithRetry(slug, inbox, offer, `${wardActor}#main-key`, wardKeys.private_pem).catch(() => {}); 1420 }).catch(() => {}); 1421 } 1404 1422 } 1405 1423 console.log('[AP] Follow', who, '→ ward', slug, '(gated, awaiting guardians)'); … … 2954 2972 } 2955 2973 2974 // ── Cross-instance follow-approval (FEP-633c §5.3, modelled on the guardian 2975 // offer). Inbound: an Offer(Follow) forwarded by a ward to a guardian (leg 2976 // 2), or a guardian's Accept/Reject coming back to the ward (leg 4). ────── 2977 async function handleFollowApprovalInbox(act, slugParam) { 2978 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 2979 const type = Array.isArray(act.type) ? act.type[0] : act.type; 2980 const actorUri = typeof act.actor === 'string' ? act.actor : (act.actor && act.actor.id); 2981 2982 // Leg 2: I am a guardian; the object is the Follow to approve. The Offer is 2983 // signed by the ward, so act.actor is the ward. 2984 if (type === 'Offer') { 2985 const fo = (act.object && typeof act.object === 'object') ? act.object : null; 2986 const foType = fo && (Array.isArray(fo.type) ? fo.type[0] : fo.type); 2987 if (!fo || foType !== 'Follow') return false; 2988 const followId = fo.id; 2989 const follower = typeof fo.actor === 'string' ? fo.actor : (fo.actor && fo.actor.id); 2990 const wardUri = actorUri; 2991 if (!followId || !follower || !wardUri) return false; 2992 const recips = (Array.isArray(act.to) ? act.to : (act.to ? [act.to] : [])).filter((x) => typeof x === 'string'); 2993 if (slugParam) recips.push(actorId(base, slugParam)); 2994 let stored = false; 2995 for (const r of new Set(recips)) { 2996 const gslug = slugFromActorUrl(r); 2997 if (!gslug) continue; 2998 if (!Guardianship.getRelation(gslug, 'guardian', wardUri)) continue; // must actually guard this ward 2999 const wardDoc = await fetchActor(wardUri).catch(() => null); 3000 const fai = actorInfo(await fetchActor(follower).catch(() => null), follower); 3001 Guardianship.follows.recordReview(gslug, { id: followId, wardUri, wardInbox: wardDoc && wardDoc.inbox, follower, followerHandle: fai.handle, followerIcon: fai.icon, followJson: JSON.stringify(fo) }); 3002 const L = pushLang(gslug); 3003 pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: fai.name || fai.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian2` }); 3004 stored = true; 3005 } 3006 return stored; 3007 } 3008 3009 // Leg 4: I am the ward; a guardian decided. object is the Follow (id). 3010 const fo = act.object; 3011 const followId = typeof fo === 'string' ? fo : (fo && fo.id); 3012 if (!followId) return false; 3013 const pending = Guardianship.follows.getPending(followId); 3014 if (!pending) return false; 3015 const guardians = Guardianship.listGuardians(pending.ward_slug).map((g) => g.other_uri); 3016 if (!guardians.includes(actorUri)) return false; // only a real guardian of this ward decides 3017 const decision = type === 'Reject' ? 'reject' : 'approve'; 3018 const r = Guardianship.follows.decide(followId, actorUri, decision, guardians); 3019 try { 3020 if (r.outcome === 'approved') { await acceptGatedFollow(r.follow); Guardianship.follows.remove(followId); } 3021 else if (r.outcome === 'rejected') { await rejectGatedFollow(r.follow); Guardianship.follows.remove(followId); } 3022 } catch { /* delivery is retried */ } 3023 return true; 3024 } 3025 3026 // Leg 3: a guardian in /guardian2 decides on a forwarded follow; send the 3027 // Accept/Reject back to the ward's inbox (signed by the guardian). 3028 export async function sendFollowDecision(guardianSite, review, decision) { 3029 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 3030 const me = actorId(base, guardianSite.slug); 3031 const keys = getOrCreateKeys(guardianSite.slug); 3032 const fo = review.follow_json ? JSON.parse(review.follow_json) : { id: review.id, type: 'Follow', actor: review.follower_uri, object: review.ward_uri }; 3033 const activity = { '@context': AP_CONTEXT, id: `${me}#followdec-${Date.now()}-${rid()}`, type: decision === 'reject' ? 'Reject' : 'Accept', actor: me, to: [review.ward_uri], object: fo, 'shaer:followApproval': true }; 3034 if (review.ward_inbox) await deliverWithRetry(guardianSite.slug, review.ward_inbox, activity, `${me}#main-key`, keys.private_pem); 3035 return { ok: true }; 3036 } 3037 2956 3038 // Send a Like or Announce (boost) on a remote note FROM this site. 2957 3039 export async function sendInteraction(site, kind, targetNoteId, authorUri) { … … 3268 3350 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote, 3269 3351 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, sendInteraction, voteOnPoll, voteOnRemotePoll, 3270 acceptGatedFollow, rejectGatedFollow, isWardGuardian, 3352 acceptGatedFollow, rejectGatedFollow, isWardGuardian, sendFollowDecision, 3271 3353 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs, 3272 3354 autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
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)