Changeset bac4bf3 in Klonkt
- Timestamp:
- 08/13/2026 11:55:04 AM (4 weeks ago)
- Branches:
- main
- Children:
- 783b9ff
- Parents:
- 7e9d0ea
- git-author:
- Robin <roboburr@…> (08/13/2026 11:54:36 AM)
- git-committer:
- Robin <roboburr@…> (08/13/2026 11:55:04 AM)
- Files:
-
- 1 added
- 3 edited
-
src/routes/activitypub.js (modified) (5 diffs)
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
src/services/ap-core.js (modified) (2 diffs)
-
test/collectie-paginering.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
r7e9d0ea rbac4bf3 28 28 29 29 const router = express.Router(); 30 31 /** 32 * Welke pagina vraagt de lezer? (shaer-sk4) 33 * 34 * Hier stond `!!req.query.page` -- of de parameter er STAAT, niet welke. Daardoor 35 * gaf ?page=2 en ?page=99 allemaal pagina 1, en noemde het antwoord zichzelf ook 36 * nog pagina 1. Onleesbaar getal of geen parameter: dan de wortel. 37 */ 38 function paginaNr(req) { 39 if (req.query.page === undefined) return false; 40 const n = Math.floor(Number(req.query.page)); 41 return Number.isFinite(n) && n > 0 ? n : 1; 42 } 30 43 // The whole fediverse layer can be turned off (solo "no federation" mode): 31 44 // then /ap/*, WebFinger and NodeInfo are simply gone — the site is undiscoverable … … 182 195 }); 183 196 if (audience === 'blocked') { 184 return AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, [], [], { page: !!req.query.page}), 'private, no-store');197 return AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, [], [], { page: paginaNr(req) }), 'private, no-store'); 185 198 } 186 199 const fanClause = audience === 'friend' ? '' : "AND (fan_only IS NULL OR fan_only = 0)"; … … 192 205 // De tracks gaan mee voor iedereen die de deur door mag; de blocked-tak 193 206 // hierboven levert bewust een outbox ZONDER posts en zonder tracks. 194 const ob = AP.buildOutbox(baseUrl(req), site, posts, AP.siteOpenTracks(site.id), { page: !!req.query.page});207 const ob = AP.buildOutbox(baseUrl(req), site, posts, AP.siteOpenTracks(site.id), { page: paginaNr(req) }); 195 208 if (audience === 'friend') { 196 209 // The owner's app builds its feed from this leg, and every note here is … … 986 999 // Default = bare references; enrich only when the client asks (FEP-9876). 987 1000 const items = wantsEnriched(req, res) ? uris.map((u) => AP.buildActorRef(site.slug, u)) : uris; 988 return AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, items.length, items ));1001 return AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, items.length, items, { page: paginaNr(req) })); 989 1002 } 990 1003 const n = db.prepare('SELECT COUNT(*) n FROM ap_followers WHERE slug = ?').get(site.slug).n; 991 AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, n ));1004 AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, n, null, { page: paginaNr(req) })); 992 1005 }); 993 1006 … … 1005 1018 items = enrich ? uris.map((u) => AP.buildActorRef(site.slug, u)) : uris; 1006 1019 } catch { /* table may not exist */ } 1007 return AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, items.length, items ));1020 return AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, items.length, items, { page: paginaNr(req) })); 1008 1021 } 1009 1022 let n = 0; 1010 1023 try { n = db.prepare("SELECT COUNT(*) n FROM ap_following WHERE slug = ? AND status = 'accepted'").get(site.slug).n; } catch { /* table may not exist */ } 1011 AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, n ));1024 AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, n, null, { page: paginaNr(req) })); 1012 1025 }); 1013 1026 -
src/services/ActivityPubService.js
r7e9d0ea rbac4bf3 1032 1032 .sort((a, b) => wanneer(b) - wanneer(a)) 1033 1033 .slice(0, MAX_OUTBOX); 1034 // WAT HIER NOG NIET GEPAGINEERD IS, en dat hoort genoemd (shaer-sk4): deze 1035 // lijst is al door de route op twintig rijen afgekapt, dus pagina 2 is leeg. 1036 // Echt doorbladeren vraagt een LIMIT/OFFSET in SQL -- en dat is hier lastiger 1037 // dan bij volgers, want posts en tracks worden op DATUM door elkaar gevlochten 1038 // en komen uit twee tabellen. Dat vraagt een UNION met een offset erover, geen 1039 // tweede slice. De vorm klopt nu wel: pagina 2 zegt eerlijk dat hij leeg is en 1040 // biedt geen `next` aan, in plaats van pagina 1 nog eens te geven. 1034 1041 // GEPAGINEERD, ook al past alles op een pagina (Funkwhale, 11-8). 1035 1042 // … … 1049 1056 // account owner (a C2S bearer scoped to this site) gets the real actor URIs via 1050 1057 // `items`, so their own client can build a friends list. 1051 export function buildFollowers(base, site, count, items = null ) {1058 export function buildFollowers(base, site, count, items = null, { page = false } = {}) { 1052 1059 const id = `${actorId(base, site.slug)}/followers`; 1053 1060 // count-only for the public; full for the owner 1054 return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0) });1061 return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0), page }); 1055 1062 } 1056 1063 1057 1064 // The accounts this site follows — count only, mirroring buildFollowers. The spec lists 1058 1065 // `following` as a standard actor property; Hubzilla/Friendica + crawlers expect it. 1059 export function buildFollowing(base, site, count, items = null ) {1066 export function buildFollowing(base, site, count, items = null, { page = false } = {}) { 1060 1067 const id = `${actorId(base, site.slug)}/following`; 1061 1068 // count-only for the public; full for the owner 1062 return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0) });1069 return pagedCollection(id, items || [], { totalItems: items ? items.length : (count || 0), page }); 1063 1070 } 1064 1071 -
src/services/ap-core.js
r7e9d0ea rbac4bf3 212 212 * extra velden die op de wortel horen (attributedTo, shaer:*) 213 213 */ 214 export function pagedCollection(id, items, { totalItems, page = false, extra = {} } = {}) { 214 /** Hoeveel items op een pagina. Gelijk aan wat de outbox vroeger als KAP had. */ 215 export const PAGINA_GROOTTE = 20; 216 217 /** 218 * Een collectie, met ECHTE paginering (shaer-sk4). 219 * 220 * Wat hier stond was een omhulsel: `page` veranderde alleen de VORM en er werd 221 * nooit gesneden. `first` en `last` wezen allebei naar ?page=1, elke ?page=N gaf 222 * dezelfde items, en pagina 99 noemde zichzelf pagina 1. Robin zag dat de 223 * pagina's identiek bleven; dit is waarom. 224 * 225 * DE WORTEL BLIJFT ZIJN ITEMS INLINE DRAGEN, en dat is geen slordigheid maar de 226 * hele reden dat dit veilig is. Shaer leest één document en volgt `next` niet; 227 * zou de wortel nu leeg worden, dan kreeg elke draaiende app nul items en geen 228 * foutmelding. Eerst de clients leren pagineren, dan pas de wortel afslanken. 229 * 230 * Een pagina VOORBIJ het einde is leeg en zegt dat ook -- met zijn eigen nummer 231 * en zonder `next`. Hem naar de laatste pagina terugbuigen zou opnieuw een 232 * antwoord zijn dat over zichzelf liegt. 233 */ 234 export function pagedCollection(id, items, { totalItems, page = false, perPage = PAGINA_GROOTTE, extra = {} } = {}) { 215 235 const lijst = items || []; 216 236 const telling = totalItems === undefined ? lijst.length : totalItems; 217 const eerste = `${id}?page=1`; 237 const grootte = Math.max(1, Number(perPage) || PAGINA_GROOTTE); 238 const paginas = Math.max(1, Math.ceil(lijst.length / grootte)); 239 const url = (n) => `${id}?page=${n}`; 240 218 241 if (page) { 242 const n = Math.max(1, Math.floor(Number(page)) || 1); 243 const deel = lijst.slice((n - 1) * grootte, n * grootte); 219 244 return { 220 245 '@context': AP_CONTEXT, 221 id: eerste,246 id: url(n), 222 247 type: 'OrderedCollectionPage', 223 248 partOf: id, 224 249 totalItems: telling, 225 orderedItems: lijst, 250 ...(n > 1 ? { prev: url(n - 1) } : {}), 251 ...(n < paginas ? { next: url(n + 1) } : {}), 252 orderedItems: deel, 226 253 }; 227 254 } … … 232 259 ...extra, 233 260 totalItems: telling, 234 first: eerste,235 last: eerste,261 first: url(1), 262 last: url(paginas), 236 263 orderedItems: lijst, 237 264 };
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)