Changeset 09fc5fb in Klonkt for src/routes/activitypub.js
- Timestamp:
- 08/10/2026 02:03:09 PM (4 weeks ago)
- Branches:
- main
- Children:
- f549cf5
- Parents:
- 86ec01b
- File:
-
- 1 edited
-
src/routes/activitypub.js (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
r86ec01b r09fc5fb 283 283 }); 284 284 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 285 458 // ── Long-poll (owner only, Robins verzoek 31-7) ─────────────────── 286 459 // Hold the request until something push-worthy lands for this account, then … … 414 587 // here, at serialisation: a blocked embed is never sent, because an embed the 415 588 // 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); 445 600 // ── Standaardvormen naast het dialect (shaer-nmw) ──────────────── 446 601 // … … 521 676 // beschermt niemand. De hulpvraag zelf gaat aan de innamekant al altijd voor. 522 677 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 }; 523 679 const messages = AP.getDirectMessages(auth.site.slug, 60) 524 680 .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)); 557 682 // Inbound REPLIES on your own posts: stored as interactions (the web's 558 683 // comment machinery), never as mentions, so this read missed them and a
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)