Ignore:
Timestamp:
08/08/2026 07:00:17 AM (5 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
0b0cd54
Parents:
f3a2556
git-author:
Robin <roboburr@…> (08/08/2026 07:00:15 AM)
git-committer:
roboburr <roboburr@…> (08/08/2026 07:00:17 AM)
Message:

Voorstellen vanuit de app, en een kind dat zelf kan vragen (shaer-8ru)

Barts opdracht: bouw shaer-8ru zodat de apps ook kunnen voorstellen, en maak
daarnaast het pad voor de ward om zelf een voorstel aan te zwengelen als hij
tegen een gated feature aanloopt.

DEEL 1 -- DE APPS KUNNEN VOORSTELLEN. De outbox kent nu een Offer van een
shaer:GatedSetting, precies de vorm die 5.6 al beschrijft en die de inbox al las.
De afweging zelf is verhuisd naar AP.proposeGate, waar de PWA-route nu ook
doorheen loopt. Twee implementaties naast elkaar zou een tweede weg naar
hetzelfde besluit zijn -- exact de fout die we vanmiddag bij de antwoordpoort
rechtzetten, toen de innamepoort alleen in C2S bleek te zitten.

DEEL 2 -- HET KIND KRIJGT WOORDEN. Tot nu toe liep alles over de guardians: zij
zien de catalogus, zij stellen voor, zij tellen. Het kind liep tegen een dichte
deur en kon niets. Nu stuurt het een shaer:gateRequest, en die landt bij zijn
guardians BIJ DE POORT waar hij over gaat -- niet in een aparte lijst die je
apart moet openen en dus vergeet.

EEN VRAAG IS GEEN STEM, en dat is de hele grens. Het verzoek is geen voorstel en
telt nergens mee; pas als een guardian het oppakt wordt het een voorstel dat
langs de gewone tally gaat. Anders opent een kind zijn eigen poort door hard
genoeg te vragen. Er staat een toets op die kijkt of er echt geen voorstel en
geen open poort uit ontstaat.

NIET DE REDDINGSBOEI. Een hulpvraag is een noodgeval en gaat door elke dichte
deur; dit is een wens. Ze door elkaar laten lopen zou de boei devalueren tot "het
kind wil iets", en dan kijkt er op een dag niemand meer op als hij afgaat.

GEEN VRIJE TEKST, en dat is geen gierigheid maar de reden dat het verzoek langs
de messages-poort MAG. Een kind met berichten dicht kan nog steeds om iets
vragen, zonder dat er een kanaal ontstaat om omheen die poort te praten. Wil het
uitleggen waarom, dan is dat een gesprek, en gesprekken hebben hun eigen poort.

Alleen van een eigen ward: een verzoek van een vreemde is geen vraag maar een
onbekende die iets over jouw instellingen wil zeggen.

Twaalf toetsen. Twee mutaties gecontroleerd (1 en 6 rood). Suite 712/712.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    rf3a2556 r2bfe26c  
    26322632            Guardianship.availability.declareAway(slug, actorUri, until);
    26332633            console.log('[AP] guardian declared away (3.6.1):', actorUri, '→', slug, 'until', new Date(until).toISOString());
     2634          }
     2635        }
     2636        // Een kind dat zelf om een poort vraagt (shaer-8ru). Zelfde weg als de
     2637        // afwezigheidsmelding: een gewone directe note met een shaer:-markering,
     2638        // per genoemde ontvanger afgehandeld.
     2639        //
     2640        // ALLEEN VAN EEN EIGEN WARD. Een verzoek van een vreemde is geen vraag
     2641        // maar een onbekende die iets over jouw instellingen wil zeggen -- dat
     2642        // hoort in geen enkele lijst te belanden waar een guardian op afgaat.
     2643        {
     2644          const req = Guardianship.gatereq.parseRequest(o);
     2645          if (req) {
     2646            for (const slug of slugs) {
     2647              const mijn = (() => { try { return Guardianship.listWards(slug).some((w) => w.other_uri === actorUri); } catch { return false; } })();
     2648              if (!mijn) { console.warn('[AP] gate request from someone who is not our ward, ignored:', actorUri, '→', slug); continue; }
     2649              Guardianship.gatereq.record(slug, actorUri, req.feature, o.id);
     2650              console.log('[AP] gate request', req.feature, actorUri, '→', slug);
     2651            }
    26342652          }
    26352653        }
     
    32373255  try {
    32383256    switch (type) {
     3257      // Een gate-voorstel uit de app (5.6, shaer-8ru). De PWA deed dit al over
     3258      // een eigen route; hier komt het binnen als wat het in AP IS -- een Offer
     3259      // van een shaer:GatedSetting. Dezelfde functie erachter, want twee wegen
     3260      // naar hetzelfde besluit is precies wat we vandaag hebben rechtgezet.
     3261      case 'Offer': {
     3262        const gs = Guardianship.gated.parseGatedSetting(object);
     3263        if (!gs) return { status: 400, error: 'unsupported_offer' };
     3264        const uit = proposeGate(site, gs.ward, gs.feature, gs.value);
     3265        if (uit.status !== 200) return uit;
     3266        return { ...uit, status: 201, id: uit.offerId };
     3267      }
    32393268      case 'Create': {
    32403269        if (!object || typeof object !== 'object') return { status: 400, error: 'missing_object' };
     
    32483277          const isWard = (() => { try { return Guardianship.listGuardians(site.slug).length > 0; } catch { return false; } })();
    32493278          const isHelp = object['shaer:helpRequest'] === true || object.helpRequest === true;
     3279          // Een poortverzoek van het kind zelf (shaer-8ru) gaat langs de
     3280          // messages-poort. Dat lijkt een gat en is het niet: het verzoek draagt
     3281          // ALLEEN de naam van de feature, geen vrije tekst, dus er ontstaat geen
     3282          // kanaal om omheen die poort te praten. Zonder deze uitzondering kan
     3283          // een kind met berichten dicht nergens meer om vragen -- en dan is de
     3284          // hele weg dood op precies het moment dat hij nodig is.
     3285          const isGateReq = !!Guardianship.gatereq.parseRequest(object);
    32503286          const direct = c2sVisibility(object) === 'direct';
    3251           if (!isHelp) {
     3287          if (!isHelp && !isGateReq) {
    32523288            if (direct && !Guardianship.wardGateAllowed(site.gate_messages, isWard)) {
    32533289              return { status: 403, error: 'gated_messages' };
     
    33073343            // absence like it does for a ward anywhere else. One path.
    33083344          }
    3309           const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil });
     3345          const gateReq = Guardianship.gatereq.parseRequest(object);
     3346          const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil, gateRequest: gateReq && gateReq.feature });
    33103347          if (!r || !r.id) return { status: 502, error: 'direct_failed' };
    33113348          return { status: 201, id: r.id, url: `${base}/ap/notes/${r.id}` };
     
    35383575// Send a reply FROM this site to a remote actor (in reply to their inbound reply).
    35393576// `parent` = an ap_interactions row (actor_uri, actor_url, actor_handle, object_uri).
     3577/**
     3578 * Een gate-voorstel de deur uit (FEP-633c 5.6, shaer-8ru).
     3579 *
     3580 * STOND IN routes/guardian.js en kon daar alleen door de PWA aangeroepen worden.
     3581 * De apps moeten hetzelfde kunnen, en een tweede implementatie ernaast zou een
     3582 * tweede weg naar hetzelfde besluit zijn -- precies de fout die we vandaag bij
     3583 * de antwoordpoort hebben rechtgezet, toen de innamepoort alleen in C2S bleek te
     3584 * zitten en het webpad eromheen liep. Een pad dus.
     3585 *
     3586 * EEN WEG, waar de ward ook woont (Robins regel, 29-7): voorstellen over de
     3587 * lijn en de server van de ward laat tellen. Co-locatie verandert alleen het
     3588 * transport -- deliverToActor lust een lokale ontvanger terug door dezelfde
     3589 * inbox. De oude kortsluiting boekte de stem hier meteen, en zo bleef het
     3590 * remote-pad een maand stuk zonder dat iemand het merkte.
     3591 */
     3592export function proposeGate(site, wardUri, feature, allow) {
     3593  const uri = String(wardUri || '').trim();
     3594  if (!uri) return { status: 400, error: 'empty_uri' };
     3595  if (!Guardianship.gated.featureColumn(feature)) return { status: 400, error: 'unknown_feature' };
     3596  // Alleen een guardian van dit kind. Zonder deze regel zou iedereen met een
     3597  // token een instelling van een vreemd kind kunnen aanvragen.
     3598  if (!Guardianship.listWards(site.slug).some((w) => w.other_uri === uri)) {
     3599    return { status: 403, error: 'not_your_ward' };
     3600  }
     3601  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     3602  const me = actorId(base, site.slug);
     3603  const offerId = `${me}/gated/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
     3604  const offer = Guardianship.gated.buildGatedOffer(offerId, me, uri, feature, allow);
     3605  // Ons eigen spoor van wat we stuurden: de server van de ward antwoordt op deze
     3606  // Offer zodra het besluit valt, en dat antwoord heeft een rij nodig om in te
     3607  // landen. Het is ook het enige waardoor het scherm van de voorsteller meer kan
     3608  // zeggen dan een knoptekst.
     3609  Guardianship.gated.recordSent(offerId, site.slug, uri, feature, allow);
     3610  deliverToActor(site, uri, offer).catch(() => { /* queued, best-effort */ });
     3611  const localSlug = (base && uri.startsWith(`${base}/`)) ? uri.replace(/\/+$/, '').split('/').pop() : null;
     3612  const progress = localSlug ? Guardianship.gated.gatedProgress(localSlug, feature) : null;
     3613  return { status: 200, ok: true, allow, state: 'open', offerId, ...(progress || { federated: true }) };
     3614}
     3615
    35403616export async function deliverReply(site, { postId, postSlug, parent, text, html, language, attachments, mentions, visibility }) {
    35413617  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     
    61136189  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    61146190  deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
    6115   getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
     6191  proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,
    61166192  linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
    61176193  noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget,
Note: See TracChangeset for help on using the changeset viewer.