Changeset 6e9bc70 in Klonkt
- Timestamp:
- 10/12/2026 04:15:08 PM (2 days ago)
- Branches:
- main
- Children:
- 61d52ed
- Parents:
- 746d98a
- Files:
-
- 2 added
- 6 edited
-
src/config/database.js (modified) (1 diff)
-
src/routes/activitypub.js (modified) (4 diffs)
-
src/services/ActivityPubService.js (modified) (1 diff)
-
src/services/ap-inbox.js (modified) (2 diffs)
-
src/services/ap-timeline.js (modified) (2 diffs)
-
src/services/conversation-key.js (added)
-
src/services/guardianship/delivery.js (modified) (3 diffs)
-
test/soft-groups.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r746d98a r6e9bc70 1058 1058 // opnieuw op te halen, en een verzonnen ouder is erger dan geen. 1059 1059 ensureColumn('ap_mentions', 'in_reply_to', 'TEXT'); 1060 // Zachte groepen (shaer-mbji, src/services/conversation-key.js). Een gesprek 1061 // is de SET deelnemers; die werd bij een binnenkomende note nergens bewaard, 1062 // dus een bericht van Anna aan jou en Ben wist niet meer dat Ben erbij was. 1063 // 1064 // `conv` is de sleutel, één keer berekend bij het schrijven: elke lezing 1065 // hoeft hem dan alleen te lezen, en een sleutel die bij elke query opnieuw 1066 // uit JSON gebouwd wordt is een plek om het verschillend te doen. NULL op 1067 // oude rijen, en dan valt de lezing terug op wat hij altijd deed. 1068 ensureColumn('ap_mentions', 'conv', 'TEXT'); 1069 ensureColumn('ap_mentions', 'participants', 'TEXT'); // JSON: de anderen, zonder mij 1070 ensureColumn('ap_mentions', 'context', 'TEXT'); // FEP-7888 1071 ensureColumn('ap_outbox', 'conv', 'TEXT'); 1072 ensureColumn('ap_outbox', 'context', 'TEXT'); 1060 1073 ensureColumn('ap_interactions', 'media_json', 'TEXT'); 1061 1074 ensureColumn('ap_interactions', 'quote_json', 'TEXT'); -
src/routes/activitypub.js
r746d98a r6e9bc70 20 20 import OAuth from '../services/OAuthService.js'; 21 21 import * as Guardianship from '../services/guardianship/index.js'; 22 import * as ConversationKey from '../services/conversation-key.js'; 22 23 import * as Migration from '../services/MigrationService.js'; 23 24 import { getPrimarySite } from '../middleware/site.js'; … … 626 627 } 627 628 } 629 // WELK GESPREK, EN MET WIE (shaer-mbji). De client kon dat afleiden uit de 630 // adressering van de note -- maar een binnenkomer draagt die niet mee in zijn 631 // kaart, dus een bericht van Anna aan jou en Ben las als iets van Anna alleen. 632 // De server weet het: de sleutel staat in de kop. Twee termen, zodat een 633 // client die alleen de deelnemers wil niet een sleutel hoeft te ontleden. 634 for (const { head, item } of pairs) { 635 const obj = item && item.object; 636 if (!obj || !head.other) continue; 637 obj['shaer:conversation'] = head.other; 638 obj['shaer:participants'] = ConversationKey.participantsOfKey(head.other); 639 } 628 640 return pairs; 629 641 } … … 673 685 const auth = OAuth.verifyBearer(req.headers.authorization); 674 686 if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end(); 675 const other = String(req.query.with || ''); 676 if (!/^https?:\/\//i.test(other)) return res.status(400).json({ error: 'with must be an actor URI' }); 687 // EÉN OF MEER `with` (shaer-mbji). Eén is het gesprek met die persoon, zoals 688 // altijd; meer is de zachte groep van precies die mensen. De sleutel komt 689 // uit dezelfde functie als bij het schrijven, dus {Anna, Ben} en {Ben, Anna} 690 // vragen hetzelfde gesprek op. 691 const withs = [].concat(req.query.with || []).map(String); 692 if (!withs.length || !withs.every((u) => /^https?:\/\//i.test(u))) { 693 return res.status(400).json({ error: 'with must be one or more actor URIs' }); 694 } 695 const other = ConversationKey.conversationKey(withs); 696 if (!other) return res.status(400).json({ error: 'with must be one or more actor URIs' }); 677 697 const page = AP.conversationHistory(auth.site.slug, other, { 678 698 before: req.query.before ? String(req.query.before) : null, … … 684 704 // dan klopt het ritme van een 'load more' niet meer met wat de gebruiker ziet. 685 705 const size = req.query.limit ? `&limit=${encodeURIComponent(String(req.query.limit))}` : ''; 686 const self = `${baseUrl(req)}/ap/users/${encodeURIComponent(auth.site.slug)}/messages?with=${encodeURIComponent(other)}`; 706 // Elke deelnemer een eigen `with`, zodat de next-link hetzelfde gesprek 707 // opvraagt. De sleutel zelf er kaal in zetten zou bij een groep een spatie in 708 // de URL leggen, en wie die als één adres leest vraagt iets anders op. 709 const withQuery = ConversationKey.participantsOfKey(other).map((u) => `with=${encodeURIComponent(u)}`).join('&'); 710 const self = `${baseUrl(req)}/ap/users/${encodeURIComponent(auth.site.slug)}/messages?${withQuery}`; 687 711 AP.sendAP(res, { 688 712 '@context': AP.AP_CONTEXT, -
src/services/ActivityPubService.js
r746d98a r6e9bc70 549 549 attributedTo: meR, 550 550 inReplyTo: post.in_reply_to || undefined, 551 // FEP-7888 (shaer-mbji): het gesprek waar dit bij hoort, in AS2 dat 552 // andere servers ook lezen. Alleen op wat er een heeft -- oude rijen en 553 // gewone antwoorden blijven precies zoals ze waren. 554 context: post.context || undefined, 551 555 content: post.content, 552 556 // Reply language (rich replies): the AS2 language map next to `content`. -
src/services/ap-inbox.js
r746d98a r6e9bc70 30 30 import { fwStmts } from './ap-following.js'; 31 31 import * as HubInvite from './hub-invite.js'; 32 import * as ConversationKey from './conversation-key.js'; 32 33 33 34 /** … … 852 853 // laten voor iedereen die de objectvorm stuurt. 853 854 const ouder = safeUrl(typeof o.inReplyTo === 'string' ? o.inReplyTo : (o.inReplyTo && o.inReplyTo.id)) || null; 854 const r = db.prepare(`INSERT OR IGNORE INTO ap_mentions (slug, object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, published, in_reply_to, help_request, wave, has_guardians, emoji_json, actor_emoji_json, media_json, created_at) 855 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,${NU_ISO})`) 855 // WIE ER NOG MEER BIJ WAS (shaer-mbji). De afzender plus iedere 856 // persoon in to/cc, zonder ons. Zonder dit wist een bericht van 857 // Anna aan jou en Ben niet meer dat Ben erbij was, en kwam het 858 // alleen in het gesprek met Anna terecht. 859 // 860 // ALLEEN VOOR EEN DIRECT BERICHT. Deze tabel draagt ook publieke 861 // posts die ons noemen, en een openbare post aan jou en Ben is geen 862 // besloten gesprek met z'n drieën -- dat zou een groep verzinnen 863 // die niemand begon. Die blijft bij zijn afzender, zoals altijd. 864 const ander = noteVisibility(o) === 'direct' 865 ? ConversationKey.participantsOf(o, actorUri, actorId(base, slug)) 866 : [actorUri]; 867 const conv = ConversationKey.conversationKey(ander) || actorUri; 868 const r = db.prepare(`INSERT OR IGNORE INTO ap_mentions (slug, object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, published, in_reply_to, help_request, wave, has_guardians, emoji_json, actor_emoji_json, media_json, conv, participants, context, created_at) 869 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,${NU_ISO})`) 856 870 .run(slug, o.id, safeUrl(o.url) || null, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.published || null, ouder, help ? 1 : 0, wave ? 1 : 0, hasG ? 1 : 0, 857 extractEmojiTags(o.tag), emojiJsonOf(ai.emojis), mediaFromNote(o)); 871 extractEmojiTags(o.tag), emojiJsonOf(ai.emojis), mediaFromNote(o), 872 conv, JSON.stringify(ander), ConversationKey.contextOf(o)); 858 873 if (r.changes) { 859 874 // The quote / link-preview card resolves out of band (a remote -
src/services/ap-timeline.js
r746d98a r6e9bc70 248 248 */ 249 249 250 // `other` is hier DE GESPREKSSLEUTEL (shaer-mbji, conversation-key.js), en niet 251 // meer per se één persoon. Voor één ander is dat zijn URI, precies zoals 252 // altijd; voor een groep de gesorteerde set. De naam van de kolom bleef, want 253 // koppen, geschiedenis, leesmarkeringen en ongelezen lezen hem allemaal -- en 254 // die werken nu per gesprek in plaats van per persoon zonder dat er een regel 255 // in hoefde te veranderen. 256 // 257 // Oude rijen hebben geen `conv` en lezen zoals ze altijd lazen: een binnenkomer 258 // bij zijn afzender, een uitgaande bij elke ontvanger afzonderlijk. Geen 259 // migratie die de geschiedenis herschikt; alleen wat nieuw is, groepeert. 250 260 const CONVERSATION_UNION = ` 251 SELECT m.actor_uri AS other, ${STEMPEL('COALESCE(m.published, m.created_at)')} AS stamp, 261 SELECT COALESCE(NULLIF(m.conv, ''), m.actor_uri) AS other, 262 ${STEMPEL('COALESCE(m.published, m.created_at)')} AS stamp, 252 263 'in' AS direction, m.object_uri AS ref 253 264 FROM ap_mentions m 254 265 WHERE m.slug = @slug AND m.actor_uri IS NOT NULL AND m.actor_uri <> '' 266 UNION ALL 267 SELECT o.conv AS other, ${STEMPEL('o.created_at')} AS stamp, 268 'out' AS direction, o.id AS ref 269 FROM ap_outbox o 270 WHERE o.site_slug = @slug AND o.visibility = 'direct' 271 AND o.conv IS NOT NULL AND o.conv <> '' 255 272 UNION ALL 256 273 SELECT j.value AS other, ${STEMPEL('o.created_at')} AS stamp, … … 259 276 JOIN json_each(COALESCE(NULLIF(o.to_actors, ''), json_array(o.to_actor))) j 260 277 WHERE o.site_slug = @slug AND o.visibility = 'direct' 278 AND (o.conv IS NULL OR o.conv = '') 261 279 AND j.value IS NOT NULL AND j.value <> ''`; 262 280 -
src/services/guardianship/delivery.js
r746d98a r6e9bc70 14 14 import db, { NU_ISO } from '../../config/database.js'; 15 15 import { carriesGuardians } from './context.js'; 16 import * as ConversationKey from '../conversation-key.js'; 16 17 17 18 const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public'; … … 33 34 if (!to.length && !cc.length) return 'public'; // no addressing at all: legacy client, keep old behavior 34 35 return 'direct'; 36 } 37 38 /** 39 * De context (FEP-7888) van het bericht waarop geantwoord wordt, als we die 40 * kennen: uit wat binnenkwam of uit wat we zelf stuurden. Een antwoord hoort in 41 * het gesprek van zijn ouder, en niet in een vers gesprek ernaast. 42 */ 43 function contextFor(slug, inReplyTo) { 44 if (!inReplyTo) return null; 45 try { 46 const binnen = db.prepare('SELECT context FROM ap_mentions WHERE slug = ? AND object_uri = ? AND context IS NOT NULL') 47 .get(slug, inReplyTo); 48 if (binnen && binnen.context) return binnen.context; 49 // Een eigen note heeft als uri .../ap/notes/<id>; de id is de outbox-sleutel. 50 const eigen = /\/ap\/notes\/([^/?#]+)$/.exec(String(inReplyTo)); 51 if (eigen) { 52 const rij = db.prepare('SELECT context FROM ap_outbox WHERE site_slug = ? AND id = ? AND context IS NOT NULL') 53 .get(slug, eigen[1]); 54 if (rij && rij.context) return rij.context; 55 } 56 } catch { /* geen context is ook een antwoord */ } 57 return null; 35 58 } 36 59 … … 144 167 .map((a) => ({ url: a.url, mediaType: String(a.mediaType), name: String(a.name || '').slice(0, 120) })); 145 168 const id = crypto.randomUUID(); 146 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, language, attachments, visibility, to_actors, help_request, wave, away_until, created_at) 147 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,${NU_ISO})`) 148 .run(id, site.slug, '', null, inReplyTo || null, resolved[0].uri, resolved[0].handle, content, lang, media.length ? JSON.stringify(media) : null, 'direct', JSON.stringify(resolved.map((r) => r.uri)), helpRequest ? 1 : 0, wave ? 1 : 0, awayUntil || null); 169 // Het gesprek waar dit bij hoort (shaer-mbji): de set ontvangers. Aan Anna en 170 // Ben samen is één gesprek, en niet twee die elk de helft dragen. 171 const conv = ConversationKey.conversationKey(resolved.map((r) => r.uri)); 172 // FEP-7888: het gesprek een naam geven die andere servers ook lezen. Is dit 173 // een antwoord op iets met een context, dan hoort het in diezelfde; anders 174 // begint hier een nieuwe. 175 const context = contextFor(site.slug, inReplyTo) || `${base}/ap/contexts/${crypto.randomUUID()}`; 176 db.prepare(`INSERT INTO ap_outbox (id, site_slug, post_id, post_slug, in_reply_to, to_actor, to_handle, content, language, attachments, visibility, to_actors, help_request, wave, away_until, conv, context, created_at) 177 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,${NU_ISO})`) 178 .run(id, site.slug, '', null, inReplyTo || null, resolved[0].uri, resolved[0].handle, content, lang, media.length ? JSON.stringify(media) : null, 'direct', JSON.stringify(resolved.map((r) => r.uri)), helpRequest ? 1 : 0, wave ? 1 : 0, awayUntil || null, conv, context); 149 179 const row = getOutboxRow(id); 150 180 const note = buildReplyNote(base, site, row);
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)