Changeset 0db3c72 in Klonkt
- Timestamp:
- 08/10/2026 06:54:54 AM (4 weeks ago)
- Branches:
- main
- Children:
- 75f2897
- Parents:
- 7e53594
- Files:
-
- 1 added
- 2 edited
-
src/routes/activitypub.js (modified) (10 diffs)
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
test/standard-shapes.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
r7e53594 r0db3c72 214 214 if (it && it.object && typeof it.object === 'object') { 215 215 it.object['shaer:author'] = me; 216 // En de standaardvorm ernaast (shaer-nmw): de ingesloten actor, zodat 217 // ook een generieke lezer de byline heeft. 218 it.object.attributedTo = AP.actorObject( 219 (typeof it.object.attributedTo === 'string' ? it.object.attributedTo : undefined) || AP.actorId(baseUrl(req), site.slug), 220 me, 221 ); 216 222 const row = byNote.get(it.object.id); 217 223 if (row) { 218 224 it.object['shaer:quote'] = AP.timelineQuote(row.quote_json); 219 if (bearerEmbeds) it.object['shaer:embed'] = AP.timelineEmbed(row.embed_json, { playback: bearerEmbeds.playback }); 225 it.object.quote = AP.quoteObject(row.quote_json); 226 if (bearerEmbeds) { 227 it.object['shaer:embed'] = AP.timelineEmbed(row.embed_json, { playback: bearerEmbeds.playback }); 228 it.object.preview = AP.previewObject(row.embed_json, { playback: bearerEmbeds.playback }); 229 } 220 230 } 221 231 } … … 438 448 // goed van een vreemde server. De naam zelf blijft, met :shortcode: als tekst. 439 449 const gateAuthor = (a) => (a && !emojiAllowed ? { ...a, emojis: undefined } : a); 450 // ── Standaardvormen naast het dialect (shaer-nmw) ──────────────── 451 // 452 // Een lezer die AS2 kent heeft nu genoeg aan attributedTo (ingesloten 453 // actor), quote (FEP-044f als object), preview (AS2 core) en de 454 // Announce-wrapper. De shaer:-velden blijven er nog naast staan voor apps 455 // in het veld; die gaan eruit als de clients om zijn. 456 const authorInfo = (r, p) => { 457 const info = { 458 name: r[`${p}name`] || undefined, handle: r[`${p}handle`] || undefined, 459 icon: r[`${p}icon`] || undefined, url: r[`${p}url`] || undefined, 460 emojis: (() => { try { return r[`${p}emoji_json`] ? JSON.parse(r[`${p}emoji_json`]) : undefined; } catch { return undefined; } })(), 461 }; 462 return (info.name || info.handle || info.icon) ? gateAuthor(info) : undefined; 463 }; 440 464 const rows = AP.getTimeline(auth.site.slug, 60); 441 465 // Eén query voor de hele pagina (shaer-9e9 fase 2): shaer:liked komt uit de … … 443 467 // ap_timeline. Per rij vragen zou hier een N+1 opleveren. 444 468 const reacties = AP.getReactionsFor(auth.site.slug, rows.map((t) => t.id)); 445 const posts = rows.map((t) => ({ 469 const posts = rows.map((t) => { 470 const auteur = authorInfo(t, 'author_'); 471 const booster = authorInfo(t, 'reblog_'); 472 const boosterUri = t.reblog_url || t.reblog_handle || undefined; 473 return { 446 474 id: `${t.id}#create`, 447 type: 'Create', 448 actor: t.author_uri, 475 // EEN BOOST IS EEN ANNOUNCE (shaer-nmw): een Create met een 476 // zijkanaal-property was onze uitvinding; de wrapper is de standaard, en 477 // elke AP-client leest hem al. 478 type: booster ? 'Announce' : 'Create', 479 actor: booster ? (AP.actorObject(boosterUri || t.author_uri, booster)) : t.author_uri, 449 480 published: t.published || t.created_at || undefined, 450 481 object: { 451 482 id: t.id, 452 483 type: 'Note', 453 attributedTo: t.author_uri, 484 // AS2 staat een INGESLOTEN actor toe; dan heeft elke client de byline, 485 // niet alleen de onze (shaer-nmw). 486 attributedTo: AP.actorObject(t.author_uri, auteur), 454 487 content: t.content, 455 488 url: t.url || undefined, … … 497 530 // Carries shaer:playerUrl only when the playback gate is open too. 498 531 'shaer:embed': embedsAllowed ? AP.timelineEmbed(t.embed_json, { playback: playbackAllowed }) : undefined, 532 // De standaardvormen (shaer-nmw): FEP-044f quote als object, AS2 preview 533 // als kaart. Dezelfde poorten als hun shaer:-tegenhangers hierboven. 534 quote: quotesAllowed ? AP.quoteObject(t.quote_json) : undefined, 535 preview: embedsAllowed ? AP.previewObject(t.embed_json, { playback: playbackAllowed }) : undefined, 499 536 }, 500 })); 537 }; 538 }); 501 539 // The direct notes addressed to this account: a plain DM, a guardian's wave 502 540 // (§5), a ward's 🛟 help request (§5.2.1). Those are messages, not posts, so … … 520 558 id: m.object_uri, 521 559 type: 'Note', 522 attributedTo: m.actor_uri, 560 attributedTo: AP.actorObject(m.actor_uri, (m.actor_name || m.actor_handle || m.actor_icon) ? gateAuthor({ 561 name: m.actor_name || undefined, handle: m.actor_handle || undefined, 562 icon: m.actor_icon || undefined, url: m.actor_url || undefined, 563 emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(), 564 }) : undefined), 523 565 content: AP.stripLeadingMentions(m.content), 524 566 url: m.note_url || undefined, … … 543 585 } : undefined), 544 586 'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined, 587 quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined, 588 preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined, 545 589 }, 546 590 })); … … 557 601 id: m.object_uri, 558 602 type: 'Note', 559 attributedTo: m.actor_uri, 603 attributedTo: AP.actorObject(m.actor_uri, (m.actor_name || m.actor_handle || m.actor_icon) ? gateAuthor({ 604 name: m.actor_name || undefined, handle: m.actor_handle || undefined, 605 icon: m.actor_icon || undefined, url: m.actor_url || undefined, 606 emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(), 607 }) : undefined), 560 608 content: AP.stripLeadingMentions(m.content), 561 609 inReplyTo: m.parent_uri || `${base}/ap/notes/${m.post_id}`, … … 571 619 } : undefined, 572 620 'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined, 621 quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined, 622 preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined, 573 623 }, 574 624 })); … … 585 635 // The leading mention anchor is addressing, not prose (the DM leg strips 586 636 // it the same way); the Mention tags built from the full content stay. 587 object: { ...n, content: AP.stripLeadingMentions(n.content), 'shaer:author': mine }, 637 object: { 638 ...n, content: AP.stripLeadingMentions(n.content), 639 'shaer:author': mine, 640 attributedTo: AP.actorObject(typeof n.attributedTo === 'string' ? n.attributedTo : me, mine), 641 }, 588 642 })); 589 643 // Newest first over all legs, so the app can keep treating this as one feed. … … 905 959 'shaer:quote': AP.timelineQuote(uit.quoteJson), 906 960 'shaer:embed': embedsAllowed ? AP.timelineEmbed(uit.embedJson, { playback }) : undefined, 961 quote: AP.quoteObject(uit.quoteJson), 962 preview: embedsAllowed ? AP.previewObject(uit.embedJson, { playback }) : undefined, 907 963 }, 'private, no-store'); 908 964 }); -
src/services/ActivityPubService.js
r7e53594 r0db3c72 4808 4808 4809 4809 /** The stored external-embed card, for the C2S read. */ 4810 // ── Standaardvormen in plaats van eigen dialect (shaer-nmw) ─────── 4811 // 4812 // Robins waarschuwing: geen Klonkt/Shaer-dialect schrijven waar AS2 of een FEP 4813 // het al regelt. Vier eigen properties hadden een standaard naast zich staan, 4814 // en deze helpers zijn die standaard -- een definitie per vorm, zodat de tien 4815 // plekken die ze emitten niet elk hun eigen variant krijgen. 4816 // 4817 // De oude shaer:-velden blijven er voorlopig NAAST staan. Een app in het veld 4818 // leest ze nog, en een leeg scherm is een duurdere fout dan een dubbel veld; 4819 // ze gaan eruit als de clients om zijn (tweede helft van shaer-nmw). 4820 4821 /** FEP-9098 Emoji-tags uit een {shortcode: url}-kaart. */ 4822 function emojiTagsFromMap(emojis) { 4823 const uit = Object.entries(emojis || {}) 4824 .filter(([naam, url]) => naam && url) 4825 .map(([naam, url]) => ({ type: 'Emoji', name: naam, icon: { type: 'Image', url } })); 4826 return uit.length ? uit : undefined; 4827 } 4828 4829 /** 4830 * Een actor als INGESLOTEN OBJECT voor `attributedTo` / `actor`. 4831 * 4832 * AS2 staat toe dat attributedTo een object is in plaats van een URI, en dan 4833 * heeft ELKE client er wat aan -- niet alleen de onze, die er shaer:author 4834 * naast kreeg. `preferredUsername` is de lokale naam; een lezer leidt de handle 4835 * af uit die naam plus de host van de id, precies zoals wij serverkant ook 4836 * doen. Weten we niets van de persoon, dan blijft het de kale URI: een leeg 4837 * object zou beweren dat we hem kennen. 4838 */ 4839 export function actorObject(uri, info) { 4840 if (!uri) return undefined; 4841 const iets = info && (info.name || info.handle || info.icon || info.url); 4842 if (!iets) return uri; 4843 const o = { id: uri, type: 'Person' }; 4844 if (info.name) o.name = info.name; 4845 const lokaal = String(info.handle || '').replace(/^@/, '').split('@')[0]; 4846 if (lokaal) o.preferredUsername = lokaal; 4847 if (info.icon) o.icon = { type: 'Image', url: info.icon }; 4848 if (info.url) o.url = info.url; 4849 const tags = emojiTagsFromMap(info.emojis); 4850 if (tags) o.tag = tags; 4851 return o; 4852 } 4853 4854 /** 4855 * De linkkaart als AS2 `preview` (core: "identifies an entity that provides a 4856 * preview of this object"). Een Page met url, name en image IS een kaart; daar 4857 * hoefde shaer:embed nooit voor te bestaan. 4858 * 4859 * Wat WEL van ons blijft is de spelerpagina: dat die alleen meegaat als de 4860 * guardians de poort openden is FEP-633c-gedrag en heeft geen AS2-tegenhanger. 4861 */ 4862 export function previewObject(embedJson, { playback = false } = {}) { 4863 const e = timelineEmbed(embedJson, { playback }); 4864 if (!e) return undefined; 4865 const thumb = (e.media || []).find((m) => m && m.url); 4866 const p = { type: 'Page', url: e.url }; 4867 if (e.title) p.name = e.title; 4868 if (thumb) p.image = { type: 'Image', url: thumb.url }; 4869 if (e.author && (e.author.name || e.author.handle)) { 4870 p.attributedTo = { type: 'Person', name: e.author.name || e.author.handle }; 4871 } 4872 if (e['shaer:playerUrl']) p['shaer:playerUrl'] = e['shaer:playerUrl']; 4873 if (e['shaer:playable']) p['shaer:playable'] = e['shaer:playable']; 4874 return p; 4875 } 4876 4877 /** 4878 * De geciteerde post als OBJECT in `quote` (FEP-044f staat toe dat quote het 4879 * object zelf is, niet alleen een URI). De opgeslagen momentopname wordt hier 4880 * een echte Note, met de auteur als ingesloten actor -- dus geen tweede eigen 4881 * property voor iets dat de FEP al kan. 4882 */ 4883 export function quoteObject(quoteJson) { 4884 const q = timelineQuote(quoteJson); 4885 if (!q) return undefined; 4886 const note = { type: 'Note', id: q.url, url: q.url }; 4887 if (q.content) note.content = q.content; 4888 if (q.published) note.published = q.published; 4889 if (q.author) { 4890 note.attributedTo = actorObject(q.author.url || q.url, { 4891 name: q.author.name, handle: q.author.handle, icon: q.author.icon, 4892 }); 4893 } 4894 const media = (q.media || []).filter((m) => m && m.url) 4895 .map((m) => ({ type: 'Document', mediaType: m.type || undefined, url: m.url })); 4896 if (media.length) note.attachment = media; 4897 const tags = emojiTagsFromMap(q.emojis); 4898 if (tags) note.tag = tags; 4899 return note; 4900 } 4901 4810 4902 export function timelineEmbed(embedJson, { playback = false } = {}) { 4811 4903 try { … … 6188 6280 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock, 6189 6281 deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker, 6190 sendMaybe304, etagFor, onGuardian, wakeGuardian, onGuardianshipEvent, proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,6282 sendMaybe304, etagFor, onGuardian, wakeGuardian, onGuardianshipEvent, proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, actorObject, previewObject, quoteObject, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio, 6191 6283 linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections, 6192 6284 noteVisibility, belongsInTimeline, playerUrlFor, isRejectedObject, rejectInteraction, interactionReportTarget,
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)