Ignore:
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    r09fc5fb raae5881  
    283283});
    284284
    285 // ── De poorten van een lezer, op EEN plek (FEP-633c) ─────────────
    286 //
    287 // De inbox-lezing rekende ze inline uit. Nu er meer lezingen zijn die
    288 // dezelfde poorten moeten eerbiedigen (de gesprekken, de geschiedenis), zou
    289 // dat evenveel kopieen worden -- en een poort die op een van die plekken
    290 // vergeten wordt, levert stil iets uit dat dicht hoorde te staan.
    291 function leesPoorten(site) {
    292   const isWard = (() => { try { return Guardianship.listGuardians(site.slug).length > 0; } catch { return false; } })();
    293   const embeds = Guardianship.externalEmbedsAllowed(site.external_embeds, isWard);
    294   const gate = (col) => Guardianship.wardGateAllowed(site[col], isWard);
    295   const emoji = gate('gate_custom_emoji');
    296   return {
    297     isWard,
    298     embedsAllowed: embeds,
    299     playbackAllowed: embeds && Guardianship.externalPlaybackAllowed(site.external_playback, isWard),
    300     imagesAllowed: gate('gate_images'),
    301     musicAllowed: gate('gate_music'),
    302     quotesAllowed: gate('gate_quote_cards'),
    303     emojiAllowed: emoji,
    304     messagesAllowed: gate('gate_messages'),
    305     composeAllowed: gate('gate_compose'),
    306     repliesAllowed: gate('gate_replies'),
    307     threadsAllowed: gate('external_threads'),
    308     followingAllowed: gate('gate_following'),
    309     // Emoji dicht raakt ook de bylines: de plaatjes in een naam komen net zo
    310     // goed van een vreemde server. De naam zelf blijft, met :shortcode: als tekst.
    311     gateAuthor: (a) => (a && !emoji ? { ...a, emojis: undefined } : a),
    312   };
    313 }
    314 
    315 /** De naam waaronder deze lezer zichzelf herkent in een Mention. */
    316 function eigenHandle(base, slug) {
    317   try { return `@${slug}@${new URL(base).host}`; } catch { return `@${slug}`; }
    318 }
    319 
    320 // ── Een bericht als AS2-item: EEN beschrijving van de kaartvorm ──
    321 //
    322 // Gebruikt door de inbox-lezing en door de gesprekslezingen. Twee keer
    323 // opschrijven is twee vormen die uit de pas kunnen lopen, en dat merk je pas
    324 // als een kaart ergens anders rendert dan waar je keek.
    325 function berichtItem(m, { base, me, myHandle, p }) {
    326   return {
    327     id: `${m.object_uri}#create`,
    328     type: 'Create',
    329     actor: m.actor_uri,
    330     published: AP.isoStamp(m.published || m.created_at),
    331     object: {
    332       id: m.object_uri,
    333       type: 'Note',
    334       attributedTo: AP.actorObject(m.actor_uri, (m.actor_name || m.actor_handle || m.actor_icon) ? p.gateAuthor({
    335         name: m.actor_name || undefined, handle: m.actor_handle || undefined,
    336         icon: m.actor_icon || undefined, url: m.actor_url || undefined,
    337         emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
    338       }) : undefined),
    339       content: AP.stripLeadingMentions(m.content),
    340       url: m.note_url || undefined,
    341       published: AP.isoStamp(m.published || m.created_at),
    342       // Addressed to us and to nobody we know of: the other recipients of a
    343       // note to several people are not ours to see, so we serve what we know.
    344       to: [me],
    345       // The Mention is how the client recognises itself as the addressee and
    346       // groups the note into a conversation. No FEP-e232 link tags here: a
    347       // mention row keeps the resolved quote, not the raw tags.
    348       tag: [{ type: 'Mention', href: me, name: myHandle }, ...(p.emojiAllowed ? (AP.timelineEmojis(m.emoji_json) || []) : [])],
    349       attachment: AP.gateAttachments(AP.timelineAttachments(m.media_json), { images: p.imagesAllowed, audio: p.musicAllowed }),
    350       // FEP-633c: what kind of message this is. The wave is a gentle nudge from
    351       // a guardian; the help request is the buoy. Both render differently.
    352       'shaer:wave': m.wave ? true : undefined,
    353       'shaer:helpRequest': m.help_request ? true : undefined,
    354       quote: p.quotesAllowed ? AP.quoteObject(m.quote_json) : undefined,
    355       preview: p.embedsAllowed ? AP.previewObject(m.embed_json, { playback: p.playbackAllowed }) : undefined,
    356     },
    357   };
    358 }
    359 
    360 /** Een eigen verzonden note als AS2-item, zelfde vorm als de inbox-leg. */
    361 function verzondenItem(n, { me, mine }) {
    362   return {
    363     id: `${n.id}#create`,
    364     type: 'Create',
    365     actor: me,
    366     published: n.published,
    367     // The leading mention anchor is addressing, not prose (the DM leg strips
    368     // it the same way); the Mention tags built from the full content stay.
    369     object: {
    370       ...n, content: AP.stripLeadingMentions(n.content),
    371       attributedTo: AP.actorObject(typeof n.attributedTo === 'string' ? n.attributedTo : me, mine),
    372     },
    373   };
    374 }
    375 
    376 // ── Gesprekken: eerst wie, dan pas wat (shaer-frontend-yso) ──────
    377 //
    378 // Twee lezingen naast de bestaande inbox-lezing, niet in de plaats ervan: de
    379 // apps in het veld lezen die nog. /conversations geeft EEN rij per tegenpartij
    380 // -- compleet van vorm, dus de avatarhemel kan niemand kwijtraken doordat een
    381 // ander druk was -- en /messages geeft een gesprek met een cursor, zodat een
    382 // 'load more' eerlijk kan verschijnen in plaats van dat de geschiedenis stil
    383 // ophoudt.
    384 //
    385 // Beide lopen langs dezelfde poorten als de inbox-lezing (leesPoorten) en
    386 // dezelfde kaartvorm (berichtItem/verzondenItem). Messages dicht sluit ook
    387 // hier vreemden en vrienden, maar nooit het guardian-kanaal en nooit de boei.
    388 function gesprekItems(req, auth, refs) {
    389   const base = baseUrl(req);
    390   const P = leesPoorten(auth.site);
    391   const me = AP.actorId(base, auth.site.slug);
    392   const ctx = { base, me, myHandle: eigenHandle(base, auth.site.slug), p: P };
    393   const mine = AP.selfAuthor(base, auth.site);
    394   const guardianUris = (() => { try { return new Set(Guardianship.listGuardians(auth.site.slug).map((g) => g.other_uri)); } catch { return new Set(); } })();
    395 
    396   const binnen = new Map(AP.messageRowsByUri(auth.site.slug, refs.filter((r) => r.richting === 'in').map((r) => r.ref))
    397     .map((m) => [m.object_uri, m]));
    398   const uit = [];
    399   for (const r of refs) {
    400     if (r.richting === 'in') {
    401       const m = binnen.get(r.ref);
    402       if (!m) continue;
    403       if (!(P.messagesAllowed || m.help_request || guardianUris.has(m.actor_uri))) continue;
    404       uit.push(berichtItem(m, ctx));
    405     } else {
    406       const n = AP.getOutboxNote(base, r.ref);
    407       // Je eigen woorden blijven van jou: een dichte messages-poort verbergt
    408       // niet wat je zelf gezegd hebt.
    409       if (n) uit.push(verzondenItem(n, { me, mine }));
    410     }
    411   }
    412   return uit;
    413 }
    414 
    415 router.get('/ap/users/:slug/conversations', (req, res) => {
    416   const auth = OAuth.verifyBearer(req.headers.authorization);
    417   if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
    418   const koppen = AP.conversationHeads(auth.site.slug);
    419   const items = gesprekItems(req, auth, koppen);
    420   AP.sendAP(res, {
    421     '@context': AP.AP_CONTEXT,
    422     id: `${baseUrl(req)}/ap/users/${encodeURIComponent(auth.site.slug)}/conversations`,
    423     type: 'OrderedCollection',
    424     totalItems: items.length,
    425     orderedItems: items,
    426     'shaer:cursor': AP.feedCursor(auth.site.slug),
    427   }, 'private, no-store');
    428 });
    429 
    430 router.get('/ap/users/:slug/messages', (req, res) => {
    431   const auth = OAuth.verifyBearer(req.headers.authorization);
    432   if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end();
    433   const other = String(req.query.with || '');
    434   if (!/^https?:\/\//i.test(other)) return res.status(400).json({ error: 'with must be an actor URI' });
    435   const uit = AP.conversationHistory(auth.site.slug, other, {
    436     before: req.query.before ? String(req.query.before) : null,
    437     limit: req.query.limit,
    438   });
    439   const items = gesprekItems(req, auth, uit.rijen);
    440   // De paginagrootte reist mee in next: vroeg je om 30, dan hoort de volgende
    441   // pagina er ook 30 te zijn. Zonder dit wordt hij stilletjes de standaard, en
    442   // dan klopt het ritme van een 'load more' niet meer met wat de gebruiker ziet.
    443   const maat = req.query.limit ? `&limit=${encodeURIComponent(String(req.query.limit))}` : '';
    444   const zelf = `${baseUrl(req)}/ap/users/${encodeURIComponent(auth.site.slug)}/messages?with=${encodeURIComponent(other)}`;
    445   AP.sendAP(res, {
    446     '@context': AP.AP_CONTEXT,
    447     id: req.query.before ? `${zelf}${maat}&before=${encodeURIComponent(String(req.query.before))}` : `${zelf}${maat}`,
    448     type: 'OrderedCollectionPage',
    449     partOf: zelf,
    450     orderedItems: items,
    451     // De volgende pagina is de standaardvorm van 'er is meer' (AS2). Ontbreekt
    452     // hij, dan is het gesprek op -- en dat mag de client weten zonder gokken,
    453     // want anders kan een 'load more' niet eerlijk verschijnen.
    454     next: uit.meer && uit.oudste ? `${zelf}${maat}&before=${encodeURIComponent(uit.oudste)}` : undefined,
    455   }, 'private, no-store');
    456 });
    457 
    458285// ── Long-poll (owner only, Robins verzoek 31-7) ───────────────────
    459286// Hold the request until something push-worthy lands for this account, then
     
    587414  // here, at serialisation: a blocked embed is never sent, because an embed the
    588415  // client merely hides has still been delivered to the device.
    589   // De poorten van deze lezer (leesPoorten): een plek waar ze berekend worden,
    590   // zodat de gesprekslezingen dezelfde stand eerbiedigen en niet hun eigen
    591   // kopie krijgen die kan gaan afwijken.
    592   const P = leesPoorten(auth.site);
    593   const {
    594     embedsAllowed, playbackAllowed, imagesAllowed, musicAllowed, quotesAllowed,
    595     emojiAllowed, messagesAllowed, composeAllowed, repliesAllowed, threadsAllowed,
    596     followingAllowed, gateAuthor,
    597   } = P;
    598   // De rechten-lijst hieronder vraagt er nog een paar rechtstreeks op.
    599   const gate = (col) => Guardianship.wardGateAllowed(auth.site[col], P.isWard);
     416  const isWard = (() => { try { return Guardianship.listGuardians(auth.site.slug).length > 0; } catch { return false; } })();
     417  const embedsAllowed = Guardianship.externalEmbedsAllowed(auth.site.external_embeds, isWard);
     418  // The heavier sibling (5.6): may a third party's PLAYER run inside the app,
     419  // and may a link hand the child over to a browser? Both are the guardians'
     420  // call, both default to off for a ward, and both need the preview gate open
     421  // first: you cannot play, or follow, what you may not see. Served here so
     422  // the app knows what it may offer instead of guessing.
     423  const playbackAllowed = embedsAllowed
     424    && Guardianship.externalPlaybackAllowed(auth.site.external_playback, isWard);
     425  // De rest van de familie (shaer-ahy.1, 8-8): zelfde regel, zelfde plek --
     426  // de poort zit bij de serialisatie, wat dicht is wordt nooit geleverd.
     427  const gate = (col) => Guardianship.wardGateAllowed(auth.site[col], isWard);
     428  const imagesAllowed = gate('gate_images');
     429  const musicAllowed = gate('gate_music');
     430  const quotesAllowed = gate('gate_quote_cards');
     431  const emojiAllowed = gate('gate_custom_emoji');
     432  const messagesAllowed = gate('gate_messages');
     433  const composeAllowed = gate('gate_compose');
     434  const repliesAllowed = gate('gate_replies');
     435  const threadsAllowed = gate('external_threads');
     436  // Zelf iemand volgen (shaer-p729). Anders dan de rest betekent dicht hier niet
     437  // "kan niet" maar "moet eerst gevraagd worden": het verzoek gaat naar de
     438  // guardians. Juist dat hoort de app VOORAF te weten, zodat de knop kan zeggen
     439  // dat je het gaat vragen in plaats van te doen alsof het al gelukt is en het
     440  // kind het pas bij het antwoord te laten ontdekken.
     441  const followingAllowed = gate('gate_following');
     442  // Emoji dicht raakt ook de bylines: de plaatjes in een naam komen net zo
     443  // goed van een vreemde server. De naam zelf blijft, met :shortcode: als tekst.
     444  const gateAuthor = (a) => (a && !emojiAllowed ? { ...a, emojis: undefined } : a);
    600445  // ── Standaardvormen naast het dialect (shaer-nmw) ────────────────
    601446  //
     
    676521  // beschermt niemand. De hulpvraag zelf gaat aan de innamekant al altijd voor.
    677522  const guardianUris = (() => { try { return new Set(Guardianship.listGuardians(auth.site.slug).map((g) => g.other_uri)); } catch { return new Set(); } })();
    678   const berichtCtx = { base, me, myHandle, p: P };
    679523  const messages = AP.getDirectMessages(auth.site.slug, 60)
    680524    .filter((m) => messagesAllowed || m.help_request || guardianUris.has(m.actor_uri))
    681     .map((m) => berichtItem(m, berichtCtx));
     525    .map((m) => ({
     526    id: `${m.object_uri}#create`,
     527    type: 'Create',
     528    actor: m.actor_uri,
     529    published: AP.isoStamp(m.published || m.created_at),
     530    object: {
     531      id: m.object_uri,
     532      type: 'Note',
     533      attributedTo: AP.actorObject(m.actor_uri, (m.actor_name || m.actor_handle || m.actor_icon) ? gateAuthor({
     534        name: m.actor_name || undefined, handle: m.actor_handle || undefined,
     535        icon: m.actor_icon || undefined, url: m.actor_url || undefined,
     536        emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),
     537      }) : undefined),
     538      content: AP.stripLeadingMentions(m.content),
     539      url: m.note_url || undefined,
     540      published: AP.isoStamp(m.published || m.created_at),
     541      // Addressed to us and to nobody we know of: the other recipients of a
     542      // note to several people are not ours to see, so we serve what we know.
     543      to: [me],
     544      // The Mention is how the client recognises itself as the addressee and
     545      // groups the note into a conversation. No FEP-e232 link tags here: a
     546      // mention row keeps the resolved quote, not the raw tags.
     547      tag: [{ type: 'Mention', href: me, name: myHandle }, ...(emojiAllowed ? (AP.timelineEmojis(m.emoji_json) || []) : [])],
     548      attachment: AP.gateAttachments(AP.timelineAttachments(m.media_json), { images: imagesAllowed, audio: musicAllowed }),
     549      // FEP-633c: what kind of message this is. The wave is a gentle nudge from
     550      // a guardian; the help request is the buoy. Both render differently.
     551      'shaer:wave': m.wave ? true : undefined,
     552      'shaer:helpRequest': m.help_request ? true : undefined,
     553      quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined,
     554      preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined,
     555    },
     556  }));
    682557  // Inbound REPLIES on your own posts: stored as interactions (the web's
    683558  // comment machinery), never as mentions, so this read missed them and a
Note: See TracChangeset for help on using the changeset viewer.