Changeset aed0092 in Klonkt
- Timestamp:
- 08/11/2026 06:35:19 PM (4 weeks ago)
- Branches:
- main
- Children:
- 960f015
- Parents:
- cb3001e
- Files:
-
- 6 edited
-
src/routes/activitypub.js (modified) (2 diffs)
-
src/services/ActivityPubService.js (modified) (6 diffs)
-
src/services/ap-core.js (modified) (1 diff)
-
src/services/guardianship/queues.js (modified) (1 diff)
-
src/services/music/index.js (modified) (5 diffs)
-
test/ap-outbox-paging.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
rcb3001e raed0092 1204 1204 // zijn -- anders springt het hartje terug zodra de reader opnieuw opent. 1205 1205 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. 1206 1209 AP.sendAP(res, { 1207 1210 '@context': AP.AP_CONTEXT, … … 1221 1224 const base = baseUrl(req); 1222 1225 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)); 1230 1227 }); 1231 1228 -
src/services/ActivityPubService.js
rcb3001e raed0092 29 29 import Blocklist from './BlocklistService.js'; 30 30 import * as Guardianship from './guardianship/index.js'; 31 import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType, normalizeTags, tagParts, hashtagTags, buildHashtagList } from './ap-core.js';31 import { PUBLIC, AP_CONTEXT, safeUrl, actorId, noteId, guessMediaType, normalizeTags, tagParts, hashtagTags, buildHashtagList, pagedCollection } from './ap-core.js'; 32 32 // Doorgeven wat hier altijd vandaan kwam, zodat elke bestaande aanroep blijft werken. 33 33 export { AP_CONTEXT, actorId, noteId, guessMediaType }; … … 1043 1043 // wie hem vandaag leest hoort er morgen niet voor te hoeven pagineren. Er is 1044 1044 // 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 }); 1065 1046 } 1066 1047 … … 1070 1051 export function buildFollowers(base, site, count, items = null) { 1071 1052 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) }); 1079 1055 } 1080 1056 … … 1083 1059 export function buildFollowing(base, site, count, items = null) { 1084 1060 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) }); 1092 1063 } 1093 1064 … … 1098 1069 const id = `${actorId(base, site.slug)}/featured`; 1099 1070 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); 1107 1072 } 1108 1073 … … 6555 6520 6556 6521 export default { 6557 AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, 6522 AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId, stripLeadingMentions, pagedCollection, 6558 6523 buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured, 6559 6524 channelUrls, channelCategory, timelineFields, guessMediaType, -
src/services/ap-core.js
rcb3001e raed0092 187 187 return out; 188 188 } 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 */ 214 export 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 20 20 import * as handshake from './handshake.js'; 21 21 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). 22 26 const 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, 24 30 }); 25 31 -
src/services/music/index.js
rcb3001e raed0092 14 14 15 15 import db from '../../config/database.js'; 16 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList } from '../ap-core.js';16 import { AP_CONTEXT, PUBLIC, actorId, noteId, safeUrl, guessMediaType, buildHashtagList, pagedCollection } from '../ap-core.js'; 17 17 import { afleidenUitInsluitingen, ingeslotenPlaylists } from '../../assets/js/shared/post-music-type.js'; 18 18 … … 201 201 /** De collectie van alle open tracks van een site (shaer-0nh, stap 3). */ 202 202 export 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) } }); 215 207 } 216 208 … … 270 262 return stub; 271 263 }); 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) } }); 280 265 } 281 266 … … 289 274 const hostPosts = site.id ? trackHostPosts(site.id) : null; 290 275 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 }); 300 279 // Album of playlist is presentatie; op de draad is het één samenvattingsveld. 301 280 const parts = []; … … 449 428 450 429 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) } }); 459 433 return leenVanPost(base, site, out, post); 460 434 } -
test/ap-outbox-paging.test.js
rcb3001e raed0092 62 62 assert.equal(leeg.last, `${OUT}?page=1`); 63 63 }); 64 65 // ── En de andere collecties, want compleetheid was de opdracht ──────────── 66 67 test('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 80 test('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 86 test('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 93 test('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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)