Changeset aed0092 in Klonkt


Ignore:
Timestamp:
08/11/2026 06:35:19 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
960f015
Parents:
cb3001e
Message:

Elke collectie pagineert nu, via een helper in plaats van tien keer dezelfde regels

Vervolg op cb3001e, waar alleen de outbox eraan geloofde omdat Funkwhale daar
over viel. Robins opdracht: de rest ook, om compleetheid te garanderen. Dat is
de goede volgorde -- een foutmelding repareren waar hij valt laat de volgende
lezer op de volgende collectie stuklopen.

pagedCollection in ap-core, en daar hangen ze nu allemaal aan:

outbox followers following featured
tracks playlists playlist post-tracks
replies + de zeven guardianship-wachtrijen

De items blijven overal INLINE op de wortel; first en last wijzen naar dezelfde
pagina, want onze collecties zijn gekapt en er is er precies een. Wie ze vandaag
zonder pagineren leest -- Shaer doet dat -- merkt er niets van.

DRIE PLEKKEN DIE BEWUST AFWIJKEN, want een sleepnet is geen zorgvuldigheid:

  • de guardianship-wachtrijen krijgen hun @context van de route (queueRoute), dus daar staan de velden er met de hand bij. pagedCollection zou de context een tweede keer toevoegen.
  • de thread-collectie draagt al een ?object= in zijn id. Daar ?page= achteraan plakken pagineert niets, het herhaalt de vraag. Owner-only en door Shaer gelezen, dus geen federatiebelang.
  • followers en following geven publiek alleen een AANTAL. Die krijgen de velden juist wel: anders is "ik mag de lijst niet zien" niet te onderscheiden van een kapot antwoord -- dezelfde stille dubbelzinnigheid die we vandaag bij een ander aantroffen.

Vier tests erbij, waaronder een die bewaakt dat de helper attributedTo niet
opeet en een die bewaakt dat de wachtrijen GEEN eigen @context krijgen.

Co-Authored-By: Claude Opus 5 <noreply@…>

Files:
6 edited

Legend:

Unmodified
Added
Removed
  • src/routes/activitypub.js

    rcb3001e raed0092  
    12041204  // zijn -- anders springt het hartje terug zodra de reader opnieuw opent.
    12051205  const reacties = AP.getReactionsFor(auth.site.slug, uit.notes.map((n) => n.id));
     1206  // De thread heeft al een ?object= in zijn id, dus geen ?page= erachter: die
     1207  // collectie is niet te pagineren zonder de vraag zelf te herhalen. Hij is
     1208  // owner-only en wordt door Shaer gelezen, niet door de federatie.
    12061209  AP.sendAP(res, {
    12071210    '@context': AP.AP_CONTEXT,
     
    12211224  const base = baseUrl(req);
    12221225  const items = AP.getReplyUris(base, req.params.id);
    1223   AP.sendAP(res, {
    1224     '@context': AP.AP_CONTEXT,
    1225     id: `${base}/ap/notes/${req.params.id}/replies`,
    1226     type: 'OrderedCollection',
    1227     totalItems: items.length,
    1228     orderedItems: items,
    1229   });
     1226  AP.sendAP(res, AP.pagedCollection(`${base}/ap/notes/${req.params.id}/replies`, items));
    12301227});
    12311228
  • src/services/ActivityPubService.js

    rcb3001e raed0092  
    2929import Blocklist from './BlocklistService.js';
    3030import * as Guardianship from './guardianship/index.js';
    31 import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType, normalizeTags, tagParts, hashtagTags, buildHashtagList } from './ap-core.js';
     31import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType, normalizeTags, tagParts, hashtagTags, buildHashtagList, pagedCollection } from './ap-core.js';
    3232// Doorgeven wat hier altijd vandaan kwam, zodat elke bestaande aanroep blijft werken.
    3333export { AP_CONTEXT, actorId, noteId, guessMediaType };
     
    10431043  // wie hem vandaag leest hoort er morgen niet voor te hoeven pagineren. Er is
    10441044  // precies een pagina, dus first en last wijzen naar dezelfde.
    1045   const eerste = `${id}?page=1`;
    1046   if (page) {
    1047     return {
    1048       '@context': AP_CONTEXT,
    1049       id: eerste,
    1050       type: 'OrderedCollectionPage',
    1051       partOf: id,
    1052       totalItems: items.length,
    1053       orderedItems: items,
    1054     };
    1055   }
    1056   return {
    1057     '@context': AP_CONTEXT,
    1058     id,
    1059     type: 'OrderedCollection',
    1060     totalItems: items.length,
    1061     first: eerste,
    1062     last: eerste,
    1063     orderedItems: items,
    1064   };
     1045  return pagedCollection(id, items, { page });
    10651046}
    10661047
     
    10701051export function buildFollowers(base, site, count, items = null) {
    10711052  const id = `${actorId(base, site.slug)}/followers`;
    1072   return {
    1073     '@context': AP_CONTEXT,
    1074     id,
    1075     type: 'OrderedCollection',
    1076     totalItems: items ? items.length : (count || 0),
    1077     orderedItems: items || [], // count-only for the public; full for the owner
    1078   };
     1053  // count-only for the public; full for the owner
     1054  return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0) });
    10791055}
    10801056
     
    10831059export function buildFollowing(base, site, count, items = null) {
    10841060  const id = `${actorId(base, site.slug)}/following`;
    1085   return {
    1086     '@context': AP_CONTEXT,
    1087     id,
    1088     type: 'OrderedCollection',
    1089     totalItems: items ? items.length : (count || 0),
    1090     orderedItems: items || [], // count-only for the public; full for the owner
    1091   };
     1061  // count-only for the public; full for the owner
     1062  return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0) });
    10921063}
    10931064
     
    10981069  const id = `${actorId(base, site.slug)}/featured`;
    10991070  const items = (posts || []).map((p) => buildNote(base, site, p));
    1100   return {
    1101     '@context': AP_CONTEXT,
    1102     id,
    1103     type: 'OrderedCollection',
    1104     totalItems: items.length,
    1105     orderedItems: items,
    1106   };
     1071  return pagedCollection(id, items);
    11071072}
    11081073
     
    65556520
    65566521export default {
    6557   AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions,
     6522  AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, pagedCollection,
    65586523  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
    65596524  channelUrls, channelCategory, timelineFields, guessMediaType,
  • src/services/ap-core.js

    rcb3001e raed0092  
    187187  return out;
    188188}
     189
     190/**
     191 * Een AS2-collectie MET de paginavelden erbij (shaer-0nh, 11-8).
     192 *
     193 * WAAROM DIT EEN HELPER IS EN GEEN REGELS. Funkwhale weigerde onze outbox met
     194 * "first: This field is required" en "last: This field is required" -- de eerste
     195 * concrete reden die we hoorden waarom er niets van ons binnenkwam. AS2 EIST die
     196 * velden niet, maar bijna iedereen pagineert, en een lezer die de paginaweg
     197 * volgt liep dood. Toen dat voor de outbox gerepareerd was misten alle andere
     198 * collecties ze nog steeds. Een helper zorgt dat de volgende collectie ze niet
     199 * opnieuw vergeet.
     200 *
     201 * DE ITEMS BLIJVEN INLINE op de wortel. Shaer bouwt zijn feed daaruit, en wie
     202 * hem vandaag leest hoort er morgen niet voor te hoeven pagineren. Onze
     203 * collecties zijn gekapt, dus er is precies EEN pagina en wijzen first en last
     204 * naar dezelfde.
     205 *
     206 * @param {string} id        de collectie-uri, zonder query
     207 * @param {Array}  items     wat erin zit (mag leeg)
     208 * @param {object} opts
     209 *   totalItems  als de telling niet items.length is (followers geeft publiek
     210 *               alleen een AANTAL en houdt de lijst dicht)
     211 *   page        true -> een OrderedCollectionPage met partOf in plaats van de wortel
     212 *   extra       velden die op de wortel horen (attributedTo, shaer:*)
     213 */
     214export function pagedCollection(id, items, { totalItems, page = false, extra = {} } = {}) {
     215  const lijst = items || [];
     216  const telling = totalItems === undefined ? lijst.length : totalItems;
     217  const eerste = `${id}?page=1`;
     218  if (page) {
     219    return {
     220      '@context': AP_CONTEXT,
     221      id: eerste,
     222      type: 'OrderedCollectionPage',
     223      partOf: id,
     224      totalItems: telling,
     225      orderedItems: lijst,
     226    };
     227  }
     228  return {
     229    '@context': AP_CONTEXT,
     230    id,
     231    type: 'OrderedCollection',
     232    ...extra,
     233    totalItems: telling,
     234    first: eerste,
     235    last: eerste,
     236    orderedItems: lijst,
     237  };
     238}
  • src/services/guardianship/queues.js

    rcb3001e raed0092  
    2020import * as handshake from './handshake.js';
    2121
     22// De @context zet de route erop (queueRoute), dus hier bewust niet
     23// pagedCollection uit ap-core -- die voegt hem toe en dan staat hij er twee
     24// keer. Wel dezelfde paginavelden, om dezelfde reden: een lezer die de
     25// paginaweg volgt hoort niet dood te lopen (Funkwhale, 11-8).
    2226const collection = (id, items) => ({
    23   id, type: 'OrderedCollection', totalItems: items.length, orderedItems: items,
     27  id, type: 'OrderedCollection', totalItems: items.length,
     28  first: `${id}?page=1`, last: `${id}?page=1`,
     29  orderedItems: items,
    2430});
    2531
  • src/services/music/index.js

    rcb3001e raed0092  
    1414
    1515import db from '../../config/database.js';
    16 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList } from '../ap-core.js';
     16import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection } from '../ap-core.js';
    1717import { afleidenUitInsluitingen, ingeslotenPlaylists } from '../../assets/js/shared/post-music-type.js';
    1818
     
    201201/** De collectie van alle open tracks van een site (shaer-0nh, stap 3). */
    202202export function buildTrackCollection(base, site, rows) {
    203   return {
    204     '@context': AP_CONTEXT,
    205     id: `${actorId(base, site.slug)}/tracks`,
    206     type: 'OrderedCollection',
    207     attributedTo: actorId(base, site.slug),
    208     totalItems: (rows || []).length,
    209     // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts.
    210     orderedItems: (() => {
    211       const posts = site.id ? trackHostPosts(site.id) : null;
    212       return (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts }));
    213     })(),
    214   };
     203  // Eén zoekopdracht voor alle rijen samen; zie trackHostPosts.
     204  const posts = site.id ? trackHostPosts(site.id) : null;
     205  const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { hostPosts: posts }));
     206  return pagedCollection(`${actorId(base, site.slug)}/tracks`, items, { extra: { attributedTo: actorId(base, site.slug) } });
    215207}
    216208
     
    270262    return stub;
    271263  });
    272   return {
    273     '@context': AP_CONTEXT,
    274     id: colId,
    275     type: 'OrderedCollection',
    276     attributedTo: actorId(base, site.slug),
    277     totalItems: items.length,
    278     orderedItems: items,
    279   };
     264  return pagedCollection(colId, items, { extra: { attributedTo: actorId(base, site.slug) } });
    280265}
    281266
     
    289274  const hostPosts = site.id ? trackHostPosts(site.id) : null;
    290275  const items = (rows || []).map((r) => buildTrackAudio(base, site, r, { coverFallback: playlist.cover_url || null, hostPosts }));
    291   const out = {
    292     '@context': AP_CONTEXT,
    293     id: `${actorId(base, site.slug)}/playlists/${playlist.id}`,
    294     type: 'OrderedCollection',
    295     name: playlist.title,
    296     attributedTo: actorId(base, site.slug),
    297     totalItems: items.length,
    298     orderedItems: items,
    299   };
     276  const out = pagedCollection(`${actorId(base, site.slug)}/playlists/${playlist.id}`, items, {
     277    extra: { name: playlist.title, attributedTo: actorId(base, site.slug) },
     278  });
    300279  // Album of playlist is presentatie; op de draad is het één samenvattingsveld.
    301280  const parts = [];
     
    449428
    450429  const hostPosts = new Map(rows.map((r) => [r.id, { id: post.id, slug: post.slug }]));
    451   const out = {
    452     '@context': AP_CONTEXT,
    453     id: postTracksId(base, site, post.id),
    454     type: 'OrderedCollection',
    455     attributedTo: actorId(base, site.slug),
    456     totalItems: rows.length,
    457     orderedItems: rows.map((r) => buildTrackAudio(base, site, r, { hostPosts })),
    458   };
     430  const out = pagedCollection(postTracksId(base, site, post.id),
     431    rows.map((r) => buildTrackAudio(base, site, r, { hostPosts })),
     432    { extra: { attributedTo: actorId(base, site.slug) } });
    459433  return leenVanPost(base, site, out, post);
    460434}
  • test/ap-outbox-paging.test.js

    rcb3001e raed0092  
    6262  assert.equal(leeg.last, `${OUT}?page=1`);
    6363});
     64
     65// ── En de andere collecties, want compleetheid was de opdracht ────────────
     66
     67test('followers en following dragen ze ook, ook count-only', () => {
     68  // Publiek geven die alleen een AANTAL en houden de lijst dicht. Juist dan is
     69  // een geldige collectie belangrijk: anders is "ik mag de lijst niet zien"
     70  // niet te onderscheiden van een kapot antwoord.
     71  const f = AP.buildFollowers(BASE, site, 42, null);
     72  assert.equal(f.totalItems, 42);
     73  assert.deepEqual(f.orderedItems, []);
     74  assert.equal(f.first, `${BASE}/ap/users/dev/followers?page=1`);
     75  const g = AP.buildFollowing(BASE, site, 3, null);
     76  assert.equal(g.first, `${BASE}/ap/users/dev/following?page=1`);
     77  assert.equal(g.last, `${BASE}/ap/users/dev/following?page=1`);
     78});
     79
     80test('featured ook', () => {
     81  const ft = AP.buildFeatured(BASE, site, [POST]);
     82  assert.equal(ft.first, `${BASE}/ap/users/dev/featured?page=1`);
     83  assert.equal(ft.orderedItems.length, 1, 'en de items blijven inline');
     84});
     85
     86test('de muziekcollecties ook, met hun eigen velden erbij', () => {
     87  const tc = AP.buildTrackCollection(BASE, site, []);
     88  assert.equal(tc.first, `${BASE}/ap/users/dev/tracks?page=1`);
     89  assert.equal(tc.attributedTo, `${BASE}/ap/users/dev`,
     90    'de helper mag attributedTo niet opeten');
     91});
     92
     93test('de guardianship-wachtrijen ook', async () => {
     94  const q = await import('../src/services/guardianship/queues.js');
     95  const c = q.helpCollection('https://ons.test/q/help', 'dev');
     96  assert.equal(c.first, 'https://ons.test/q/help?page=1');
     97  assert.equal(c['@context'], undefined,
     98    'de context zet de route erop -- twee keer zou hem ongeldig maken');
     99});
Note: See TracChangeset for help on using the changeset viewer.