Changeset f549cf5 in Klonkt for src/routes/activitypub.js


Ignore:
Timestamp:
08/10/2026 02:03:21 PM (4 weeks ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
3bbf73d
Parents:
c34ef26 (diff), 09fc5fb (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.
Message:

Merge branch 'incoming'

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    rc34ef26 rf549cf5  
    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.
     291function 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. */
     316function 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.
     325function 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. */
     361function 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.
     388function 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
     415router.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
     430router.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
    285458// ── Long-poll (owner only, Robins verzoek 31-7) ───────────────────
    286459// Hold the request until something push-worthy lands for this account, then
     
    414587  // here, at serialisation: a blocked embed is never sent, because an embed the
    415588  // client merely hides has still been delivered to the device.
    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);
     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);
    445600  // ── Standaardvormen naast het dialect (shaer-nmw) ────────────────
    446601  //
     
    521676  // beschermt niemand. De hulpvraag zelf gaat aan de innamekant al altijd voor.
    522677  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 };
    523679  const messages = AP.getDirectMessages(auth.site.slug, 60)
    524680    .filter((m) => messagesAllowed || m.help_request || guardianUris.has(m.actor_uri))
    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   }));
     681    .map((m) => berichtItem(m, berichtCtx));
    557682  // Inbound REPLIES on your own posts: stored as interactions (the web's
    558683  // comment machinery), never as mentions, so this read missed them and a
Note: See TracChangeset for help on using the changeset viewer.