Changeset 75f2897 in Klonkt
- Timestamp:
- 08/10/2026 07:33:52 AM (4 weeks ago)
- Branches:
- main
- Children:
- aae5881
- Parents:
- 0db3c72
- Files:
-
- 6 edited
-
src/routes/activitypub.js (modified) (9 diffs)
-
src/services/ActivityPubService.js (modified) (3 diffs)
-
test/c2s-messages.test.js (modified) (1 diff)
-
test/own-card.test.js (modified) (1 diff)
-
test/standard-shapes.test.js (modified) (1 diff)
-
test/thread.test.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/routes/activitypub.js
r0db3c72 r75f2897 195 195 if (audience === 'friend') { 196 196 // The owner's app builds its feed from this leg, and every note here is 197 // by the site itself : give it the same `shaer:author` byline the timeline198 // entries carry, so your own cards get a header too (avatar + name).197 // by the site itself, so give it a byline too (avatar + name): de 198 // ingesloten actor in attributedTo, net als de tijdlijn. 199 199 const me = AP.selfAuthor(baseUrl(req), site); 200 // De kaart op je eigen post (shaer-k3f): dezelfde shaer:quote/shaer:embed201 // die detijdlijn voor andermans posts draagt, uit de snapshots die200 // De kaart op je eigen post (shaer-k3f): dezelfde quote/preview die de 201 // tijdlijn voor andermans posts draagt, uit de snapshots die 202 202 // deliverCreate bij het publiceren opsloeg. Op note-id gekoppeld, want 203 203 // buildOutbox sorteert en mengt tracks erdoorheen. De embed alleen voor de … … 213 213 for (const it of ob.orderedItems) { 214 214 if (it && it.object && typeof it.object === 'object') { 215 it.object['shaer:author'] = me;216 // En de standaardvorm ernaast (shaer-nmw): de ingesloten actor, zodat217 // ook een generieke lezer de byline heeft.218 215 it.object.attributedTo = AP.actorObject( 219 216 (typeof it.object.attributedTo === 'string' ? it.object.attributedTo : undefined) || AP.actorId(baseUrl(req), site.slug), … … 222 219 const row = byNote.get(it.object.id); 223 220 if (row) { 224 it.object['shaer:quote'] = AP.timelineQuote(row.quote_json);225 221 it.object.quote = AP.quoteObject(row.quote_json); 226 222 if (bearerEmbeds) { 227 it.object['shaer:embed'] = AP.timelineEmbed(row.embed_json, { playback: bearerEmbeds.playback });228 223 it.object.preview = AP.previewObject(row.embed_json, { playback: bearerEmbeds.playback }); 229 224 } … … 501 496 return tags.length ? tags : undefined; 502 497 })(), 503 // FEP-044f: the resolved quoted post (author + content), so the client504 // renders an embedded quote card instead of a bare link. Omitted when the505 // note has no quote or the quoted post could not be resolved.506 'shaer:quote': quotesAllowed ? AP.timelineQuote(t.quote_json) : undefined,507 // The post author's display info (name / @handle / avatar), so every card508 // gets a byline header like the quote card. attributedTo stays the bare509 // actor URI; this is the resolved presentation Klonkt already stored.510 'shaer:author': gateAuthor((t.author_name || t.author_handle || t.author_icon) ? {511 name: t.author_name || undefined, handle: t.author_handle || undefined,512 icon: t.author_icon || undefined, url: t.author_url || undefined,513 // FEP-9098: emojis in the display name (":shortcode:"), if any.514 emojis: (() => { try { return t.author_emoji_json ? JSON.parse(t.author_emoji_json) : undefined; } catch { return undefined; } })(),515 } : undefined),516 // When a followed account boosted this, who did ("X boosted"). Omitted for517 // ordinary posts.518 'shaer:booster': gateAuthor((t.reblog_name || t.reblog_handle || t.reblog_icon) ? {519 name: t.reblog_name || undefined, handle: t.reblog_handle || undefined,520 icon: t.reblog_icon || undefined,521 // FEP-9098: emojis in the booster's display name (":shortcode:"), if any.522 emojis: (() => { try { return t.reblog_emoji_json ? JSON.parse(t.reblog_emoji_json) : undefined; } catch { return undefined; } })(),523 } : undefined),524 498 // Whether THIS account already liked/boosted the note, so the app's 525 499 // detail-view buttons show the current state (and can toggle/undo). 526 500 'shaer:liked': !!(reacties.get(t.id) || {}).liked, 527 501 'shaer:boosted': !!(reacties.get(t.id) || {}).boosted, 528 // An external (non-fediverse) embed, thumbnail-only and never an iframe. 529 // Omitted entirely when the gate is closed (see above). 530 // Carries shaer:playerUrl only when the playback gate is open too. 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. 502 // FEP-044f: de geciteerde post als object, zodat de client een kaart 503 // rendert in plaats van een kale link. AS2 preview is diezelfde kaart 504 // voor een EXTERNE link: thumbnail, nooit de iframe van de aanbieder. 505 // Allebei weg zodra hun poort dicht staat; de speler in preview hangt 506 // aan de playback-poort. 534 507 quote: quotesAllowed ? AP.quoteObject(t.quote_json) : undefined, 535 508 preview: embedsAllowed ? AP.previewObject(t.embed_json, { playback: playbackAllowed }) : undefined, … … 578 551 'shaer:wave': m.wave ? true : undefined, 579 552 'shaer:helpRequest': m.help_request ? true : undefined, 580 'shaer:quote': quotesAllowed ? AP.timelineQuote(m.quote_json) : undefined,581 'shaer:author': gateAuthor((m.actor_name || m.actor_handle || m.actor_icon) ? {582 name: m.actor_name || undefined, handle: m.actor_handle || undefined,583 icon: m.actor_icon || undefined, url: m.actor_url || undefined,584 emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),585 } : undefined),586 'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined,587 553 quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined, 588 554 preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined, … … 612 578 tag: [{ type: 'Mention', href: me, name: myHandle }, ...(AP.timelineEmojis(m.emoji_json) || [])], 613 579 attachment: AP.timelineAttachments(m.media_json), 614 'shaer:quote': AP.timelineQuote(m.quote_json),615 'shaer:author': (m.actor_name || m.actor_handle || m.actor_icon) ? {616 name: m.actor_name || undefined, handle: m.actor_handle || undefined,617 icon: m.actor_icon || undefined, url: m.actor_url || undefined,618 emojis: (() => { try { return m.actor_emoji_json ? JSON.parse(m.actor_emoji_json) : undefined; } catch { return undefined; } })(),619 } : undefined,620 'shaer:embed': embedsAllowed ? AP.timelineEmbed(m.embed_json, { playback: playbackAllowed }) : undefined,621 580 quote: quotesAllowed ? AP.quoteObject(m.quote_json) : undefined, 622 581 preview: embedsAllowed ? AP.previewObject(m.embed_json, { playback: playbackAllowed }) : undefined, … … 637 596 object: { 638 597 ...n, content: AP.stripLeadingMentions(n.content), 639 'shaer:author': mine,640 598 attributedTo: AP.actorObject(typeof n.attributedTo === 'string' ? n.attributedTo : me, mine), 641 599 }, … … 957 915 AP.sendAP(res, { 958 916 '@context': AP.AP_CONTEXT, 959 'shaer:quote': AP.timelineQuote(uit.quoteJson),960 'shaer:embed': embedsAllowed ? AP.timelineEmbed(uit.embedJson, { playback }) : undefined,961 917 quote: AP.quoteObject(uit.quoteJson), 962 918 preview: embedsAllowed ? AP.previewObject(uit.embedJson, { playback }) : undefined, … … 996 952 attachment: AP.gateAttachments(n.attachment, { images: imagesOk, audio: musicOk }), 997 953 tag: emojiOk ? n.tag : AP.stripEmojiTags(n.tag), 998 'shaer:author': (n['shaer:author'] && !emojiOk) ? { ...n['shaer:author'], emojis: undefined } : n['shaer:author'], 954 // De emoji-poort knipt in de byline zelf: FEP-9098 zit in de tag van de 955 // ingesloten actor, niet meer in een eigen emoji-kaart ernaast. 956 attributedTo: (!emojiOk && n.attributedTo && typeof n.attributedTo === 'object') 957 ? { ...n.attributedTo, tag: undefined } : n.attributedTo, 999 958 })); 1000 959 uit.hidden = kring.hidden; -
src/services/ActivityPubService.js
r0db3c72 r75f2897 3751 3751 id: o.id, 3752 3752 type: 'Note', 3753 attributedTo: actorUri, 3753 // De ingesloten actor (shaer-nmw): de byline hoort in attributedTo, waar 3754 // elke AP-lezer hem zoekt, en niet in een eigen property ernaast. 3755 attributedTo: actorObject(actorUri, actorInfo(authors.get(actorUri), actorUri)), 3754 3756 inReplyTo: (typeof o.inReplyTo === 'string' ? o.inReplyTo : (o.inReplyTo && o.inReplyTo.id)) || objectUri, 3755 3757 content: HtmlSanitizerService.sanitize(String(o.content || '').slice(0, 50_000)), … … 3777 3779 return out.length ? out : undefined; 3778 3780 })(), 3779 'shaer:author': actorInfo(authors.get(actorUri), actorUri),3780 3781 })).sort((a, b) => String(a.published || '').localeCompare(String(b.published || ''))); 3781 3782 … … 3802 3803 const kept = [], out = { hidden: 0 }; 3803 3804 for (const n of notes) { 3804 if (circle.has(n.attributedTo)) kept.push(n); 3805 // actorUriOf, niet n.attributedTo: sinds de byline ingesloten meegaat is 3806 // dat een OBJECT en zou een kale vergelijking hier stil alles wegfilteren 3807 // -- een ward met een lege thread en nergens een foutmelding. 3808 if (circle.has(actorUriOf(n.attributedTo))) kept.push(n); 3805 3809 else out.hidden += 1; 3806 3810 } -
test/c2s-messages.test.js
r0db3c72 r75f2897 94 94 assert.ok((wave.tag || []).some((x) => x.type === 'Mention' && x.href === 'https://test.example/ap/users/kid'), 95 95 'with a Mention the client recognises itself in'); 96 assert.equal(wave ['shaer:author'].name, 'Oma', 'and a byline to show');96 assert.equal(wave.attributedTo.name, 'Oma', 'and a byline to show'); 97 97 assert.ok(!/@kid@test\.example/.test(wave.content), 'the leading @mention is stripped, like Berichten on the web'); 98 98 -
test/own-card.test.js
r0db3c72 r75f2897 120 120 globalThis.fetch = vorige; 121 121 assert.equal(uit.notes.length, 1); 122 const auteur = uit.notes[0] ['shaer:author'];122 const auteur = uit.notes[0].attributedTo; 123 123 assert.equal(auteur.url, `${HOST}/`, 'de html-Link, niet de array gestringd'); 124 124 assert.ok(!String(auteur.url).includes('object'), 'nergens [object Object]'); -
test/standard-shapes.test.js
r0db3c72 r75f2897 83 83 assert.equal(AP.quoteObject('rommel'), undefined); 84 84 }); 85 86 // ── En dan het weghalen (shaer-nmw, tweede helft) ──────────────────── 87 // 88 // Bewijzen dat de standaardvorm ERBIJ staat is de helft; de andere helft is dat 89 // het dialect er niet meer NAAST staat. Zonder deze wacht sluipt een 90 // shaer:author zo weer een nieuwe leg in en spreken we stilletjes weer onze 91 // eigen taal. Daarom door de echte route, want daar wordt de vorm beslist. 92 test('de C2S-leg spreekt geen dialect meer waar de standaard het regelt', async (t) => { 93 const crypto = await import('crypto'); 94 const express = (await import('express')).default; 95 const db = (await import('../src/config/database.js')).default; 96 const routes = (await import('../src/routes/activitypub.js')).default; 97 98 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)') 99 .run('u1', 'u1', 'u1@t', 'x', 'god'); 100 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)') 101 .run('s1', 'kind', 'kind', 'u1'); 102 db.prepare(`INSERT INTO ap_timeline (id, slug, author_uri, author_name, author_handle, author_icon, 103 reblog_name, reblog_handle, content) 104 VALUES ('https://elders/n1', 'kind', 'https://elders/u/til', 'Tante Til', '@til@elders', 105 'https://elders/til.png', 'Esmee', '@es@elders', '<p>hoi</p>')`).run(); 106 107 const bearer = 'test-token-' + 'b'.repeat(24); 108 db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)') 109 .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write'); 110 111 const app = express(); 112 app.use(routes); 113 const server = app.listen(0); 114 t.after(() => server.close()); 115 await new Promise((r) => server.once('listening', r)); 116 const doc = await (await fetch(`http://127.0.0.1:${server.address().port}/ap/users/kind/inbox`, 117 { headers: { Authorization: `Bearer ${bearer}` } })).json(); 118 119 const tekst = JSON.stringify(doc); 120 // Op de SLEUTEL, niet op een stuk tekst: 'shaer:quote' zit ook in 121 // shaer:quoteCards, en dat is een poortnaam die blijft. Een te grove zeef 122 // zou hier rood staan om iets dat klopt -- of erger, groen om iets dat niet 123 // klopt. 124 for (const weg of ['shaer:author', 'shaer:booster', 'shaer:quote', 'shaer:embed']) { 125 assert.ok(!tekst.includes(`"${weg}":`), `${weg} hoort weg te zijn`); 126 } 127 // Wat WEL mag blijven, en waarom: per-lezer-interactiestatus heeft in AS2 128 // geen tegenhanger (Mastodon lost het in zijn eigen REST-API op), en de 129 // FEP-633c-termen zijn bewust van ons en in standaardisatie. 130 assert.ok(tekst.includes('shaer:liked'), 'interactiestatus blijft, die heeft geen standaard'); 131 132 const item = doc.orderedItems.find((i) => i.object.id === 'https://elders/n1'); 133 assert.equal(item.type, 'Announce', 'een boost is de wrapper, niet een zijkanaal'); 134 assert.equal(item.object.attributedTo.name, 'Tante Til', 'en de byline zit in attributedTo'); 135 assert.equal(item.object.attributedTo.preferredUsername, 'til'); 136 }); -
test/thread.test.js
r0db3c72 r75f2897 52 52 ]; 53 53 54 // De echte fetch, voordat de stub hem overneemt: de route-test onderaan doet 55 // een ECHT verzoek aan een testserver op localhost, en dat mag de stub niet 56 // beantwoorden met zijn 404. 57 const echteFetch = globalThis.fetch; 58 54 59 globalThis.fetch = async (url) => { 55 60 const u = String(url); … … 69 74 assert.equal(uit.notes.length, 2); 70 75 // Oudste eerst: een gesprek lees je van boven naar beneden. 71 assert.equal(uit.notes[0] ['shaer:author'].name, 'tante');72 assert.equal(uit.notes[1] ['shaer:author'].name, 'vreemde');76 assert.equal(uit.notes[0].attributedTo.name, 'tante'); 77 assert.equal(uit.notes[1].attributedTo.name, 'vreemde'); 73 78 // De sanitizer heeft het script eruit gehaald, de tekst mag blijven. 74 79 assert.ok(!uit.notes[1].content.includes('<script'), 'script weggeschoond'); … … 96 101 const kring = AP.filterThreadToCircle('pupil', uit.notes); 97 102 assert.equal(kring.notes.length, 1); 98 assert.equal(kring.notes[0]['shaer:author'].name, 'tante'); 103 // Ook de zeef zelf leest de ingesloten actor: een kale vergelijking op 104 // attributedTo zou hier stil ALLES wegfilteren. 105 assert.equal(kring.notes[0].attributedTo.name, 'tante'); 99 106 // De vreemde is er, en dat mag gezegd: geteld, niet stil weggelaten. 100 107 assert.equal(kring.hidden, 1); … … 143 150 globalThis.fetch = vorige; 144 151 }); 152 153 // ── Door de route heen, want daar wordt de vorm beslist ────────────── 154 // 155 // De byline van een antwoord verhuisde naar de ingesloten attributedTo 156 // (shaer-nmw). Deze leg had geen route-dekking, en juist hier zit de 157 // emoji-poort die IN die byline knipt -- knipt hij in het oude veld, dan 158 // gebeurt er stil niets meer. 159 test('de thread-route geeft de byline in attributedTo, en de emoji-poort knipt daarin', async (t) => { 160 const crypto = await import('crypto'); 161 const express = (await import('express')).default; 162 const routes = (await import('../src/routes/activitypub.js')).default; 163 164 const bearer = 'test-token-' + 'c'.repeat(24); 165 db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)') 166 .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write'); 167 168 const app = express(); 169 app.use(routes); 170 const server = app.listen(0); 171 t.after(() => server.close()); 172 await new Promise((r) => server.once('listening', r)); 173 const haal = async () => (await (await echteFetch( 174 `http://127.0.0.1:${server.address().port}/ap/users/kind/thread?object=${encodeURIComponent(BRON)}`, 175 { headers: { Authorization: `Bearer ${bearer}` } })).json()); 176 177 const doc = await haal(); 178 assert.ok(!JSON.stringify(doc).includes('"shaer:author":'), 'geen dialect meer op deze leg'); 179 const vanTante = doc.orderedItems.find((n) => n.id.endsWith('/replies/2')); 180 assert.equal(vanTante.attributedTo.name, 'tante', 'de byline zit in attributedTo'); 181 assert.equal(vanTante.attributedTo.preferredUsername, 'tante'); 182 assert.ok((vanTante.tag || []).some((x) => x.type === 'Emoji'), 'open poort: de emoji van het antwoord blijft'); 183 184 // Poort dicht: FEP-9098 zit in de tag van de ingesloten actor, dus daar 185 // hoort geknipt te worden -- en het antwoord zelf houdt zijn eigen emoji 186 // niet meer over. 187 db.prepare("UPDATE sites SET gate_custom_emoji = 0 WHERE slug = 'kind'").run(); 188 db.prepare("INSERT INTO ap_guardianships (slug, other_uri, role, status) VALUES ('kind', ?, 'guardian', 'accepted')") 189 .run('https://203.0.113.20/users/tante'); 190 const dicht = await haal(); 191 const na = dicht.orderedItems.find((n) => n.id.endsWith('/replies/2')); 192 assert.ok(!(na.tag || []).some((x) => x.type === 'Emoji'), 'dichte poort: geen emoji meer'); 193 assert.ok(!na.attributedTo.tag, 'ook niet in de byline'); 194 });
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)