Changeset 61d52ed in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 10/14/2026 04:25:21 PM (6 hours ago)
- Branches:
- main
- Parents:
- 6e9bc70
- git-author:
- Bart <bart@…> (10/14/2026 04:23:29 PM)
- git-committer:
- Bart <bart@…> (10/14/2026 04:25:21 PM)
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r6e9bc70 r61d52ed 2496 2496 2497 2497 /** 2498 * De directe antwoorden op één note, genormaliseerd voor de C2S-lezer. 2499 * 2500 * ALLEEN ophalen en normaliseren; de poorten zitten in de route. De 2501 * kringfilter (de dichte stand van shaer:externalThreads) woont in 2502 * filterThreadToCircle en de beeld/muziek/emoji-poorten in 2503 * gateAttachments/stripEmojiTags -- per verzoek, buiten deze cache om, want de 2504 * stand van een poort mag hier niet twee minuten bevriezen. Geblokkeerde 2505 * actors zijn een andere categorie en verdwijnen WEL hier, zonder telling: 2506 * een blokkade is onzichtbaar, ook als getal. 2498 * Eén vreemde note in de vorm die de C2S-lezer krijgt: geschoonde HTML, de 2499 * byline ingesloten in attributedTo, alleen emoji-tags. Gedeeld door de thread 2500 * (antwoorden) en getObject (één note), zodat die twee niet uit elkaar lopen. 2501 * 2502 * `parentFallback` is voor de thread: een antwoord in de replies-collectie van 2503 * X dat zelf geen inReplyTo noemt, hangt aantoonbaar onder X. Voor één losse 2504 * note is er zo'n bewijs niet, en dan blijft inReplyTo leeg. 2507 2505 */ 2508 export async function getThread(slug, objectUri) { 2509 const key = `${slug}|${objectUri}`; 2510 const hit = threadViewCache.get(key); 2511 if (hit && Date.now() - hit.at < THREAD_VIEW_TTL_MS) return hit.out; 2512 2513 // De status waarmee de BRON antwoordde op de note zelf. 401/403/404/410 is 2514 // een besluit van die server (niet gedeeld, of weg); alles daarbuiten -- ook 2515 // een stuk netwerk dat wegviel -- is een storing. De route moet dat verschil 2516 // kunnen zeggen, anders wijst de melding naar de verkeerde partij. 2517 let sourceStatus = 0; 2518 const get = (u) => localNoteObject(u, slug) || signedGetJson(slug, u, (st) => { sourceStatus = st; }); 2519 const note = await get(objectUri); 2520 const repliesRef = note && note.replies; 2521 let coll = null; 2522 if (typeof repliesRef === 'string') coll = await signedGetJson(slug, repliesRef); 2523 else if (repliesRef && typeof repliesRef === 'object') { 2524 coll = collectionItems(repliesRef).length || repliesRef.first ? repliesRef 2525 : (repliesRef.id ? await signedGetJson(slug, repliesRef.id) : repliesRef); 2526 } 2527 // De pagina-wandeling van collectReplyItems, maar met behoud van INLINE 2528 // objecten (die niet opnieuw opgehaald hoeven). Niet "de eerste pagina": 2529 // Mastodon serveert `first` als inline-pagina met LEGE items en een `next` 2530 // waar de antwoorden echt staan -- wie alleen de eerste pagina leest, ziet 2531 // op elke Mastodon-post een leeg gesprek. Dat was precies Barts melding 2532 // (8-8, reacties op een vreemde post). Eigen posts maskeerden het: die 2533 // gaan door de lokale kortsluiting en hebben orderedItems meteen vol. 2534 let items = []; 2535 let node = coll; 2536 if (node && node.first && !collectionItems(node).length) { 2537 node = typeof node.first === 'string' ? await signedGetJson(slug, node.first) : node.first; 2538 } 2539 let pages = 0; 2540 while (node && pages++ < 3 && items.length < THREAD_VIEW_LIMIT) { 2541 items.push(...collectionItems(node)); 2542 if (!node.next) break; 2543 node = typeof node.next === 'string' ? await signedGetJson(slug, node.next) : node.next; 2544 } 2545 items = items.slice(0, THREAD_VIEW_LIMIT); 2546 2547 // Alles tegelijk in plaats van om de beurt: dertig vreemde servers na elkaar 2548 // afwachten is een halve minuut kijken naar een spinner. 2549 const objs = await Promise.all(items.map(async (it) => { 2550 const o = typeof it === 'string' ? await get(it) : (it && it.object && typeof it.object === 'object' ? it.object : it); 2551 return (o && o.id && o.attributedTo) ? o : null; 2552 })); 2553 2554 const kept = []; 2555 for (const o of objs) { 2556 if (!o) continue; 2557 const actorUri = actorUriOf(o.attributedTo); 2558 if (!actorUri || isBlockedAny(actorUri)) continue; // een blokkade telt niet mee 2559 kept.push({ o, actorUri }); 2560 } 2561 2562 // Bylines: één fetch per unieke auteur, niet één per antwoord. 2563 const authors = new Map(); 2564 await Promise.all([...new Set(kept.map((k) => k.actorUri))].map(async (uri) => { 2565 authors.set(uri, localActorObject(uri) || await signedGetJson(slug, uri).catch(() => null)); 2566 })); 2567 2568 const notes = kept.map(({ o, actorUri }) => ({ 2506 function readerNote(o, actorUri, authorDoc, parentFallback = null) { 2507 return { 2569 2508 id: o.id, 2570 2509 type: 'Note', 2571 2510 // De ingesloten actor (shaer-nmw): de byline hoort in attributedTo, waar 2572 2511 // elke AP-lezer hem zoekt, en niet in een eigen property ernaast. 2573 attributedTo: actorObject(actorUri, actorInfo(author s.get(actorUri), actorUri)),2574 inReplyTo: (typeof o.inReplyTo === 'string' ? o.inReplyTo : (o.inReplyTo && o.inReplyTo.id)) || objectUri,2512 attributedTo: actorObject(actorUri, actorInfo(authorDoc, actorUri)), 2513 inReplyTo: (typeof o.inReplyTo === 'string' ? o.inReplyTo : (o.inReplyTo && o.inReplyTo.id)) || parentFallback || undefined, 2575 2514 content: HtmlSanitizerService.sanitize(String(o.content || '').slice(0, 50_000)), 2576 2515 url: safeUrl(typeof o.url === 'string' ? o.url : (o.url && o.url.href)) || undefined, … … 2597 2536 return out.length ? out : undefined; 2598 2537 })(), 2599 })).sort((a, b) => String(a.published || '').localeCompare(String(b.published || ''))); 2538 }; 2539 } 2540 2541 /** 2542 * De directe antwoorden op één note, genormaliseerd voor de C2S-lezer. 2543 * 2544 * ALLEEN ophalen en normaliseren; de poorten zitten in de route. De 2545 * kringfilter (de dichte stand van shaer:externalThreads) woont in 2546 * filterThreadToCircle en de beeld/muziek/emoji-poorten in 2547 * gateAttachments/stripEmojiTags -- per verzoek, buiten deze cache om, want de 2548 * stand van een poort mag hier niet twee minuten bevriezen. Geblokkeerde 2549 * actors zijn een andere categorie en verdwijnen WEL hier, zonder telling: 2550 * een blokkade is onzichtbaar, ook als getal. 2551 */ 2552 export async function getThread(slug, objectUri) { 2553 const key = `${slug}|${objectUri}`; 2554 const hit = threadViewCache.get(key); 2555 if (hit && Date.now() - hit.at < THREAD_VIEW_TTL_MS) return hit.out; 2556 2557 // De status waarmee de BRON antwoordde op de note zelf. 401/403/404/410 is 2558 // een besluit van die server (niet gedeeld, of weg); alles daarbuiten -- ook 2559 // een stuk netwerk dat wegviel -- is een storing. De route moet dat verschil 2560 // kunnen zeggen, anders wijst de melding naar de verkeerde partij. 2561 let sourceStatus = 0; 2562 const get = (u) => localNoteObject(u, slug) || signedGetJson(slug, u, (st) => { sourceStatus = st; }); 2563 const note = await get(objectUri); 2564 const repliesRef = note && note.replies; 2565 let coll = null; 2566 if (typeof repliesRef === 'string') coll = await signedGetJson(slug, repliesRef); 2567 else if (repliesRef && typeof repliesRef === 'object') { 2568 coll = collectionItems(repliesRef).length || repliesRef.first ? repliesRef 2569 : (repliesRef.id ? await signedGetJson(slug, repliesRef.id) : repliesRef); 2570 } 2571 // De pagina-wandeling van collectReplyItems, maar met behoud van INLINE 2572 // objecten (die niet opnieuw opgehaald hoeven). Niet "de eerste pagina": 2573 // Mastodon serveert `first` als inline-pagina met LEGE items en een `next` 2574 // waar de antwoorden echt staan -- wie alleen de eerste pagina leest, ziet 2575 // op elke Mastodon-post een leeg gesprek. Dat was precies Barts melding 2576 // (8-8, reacties op een vreemde post). Eigen posts maskeerden het: die 2577 // gaan door de lokale kortsluiting en hebben orderedItems meteen vol. 2578 let items = []; 2579 let node = coll; 2580 if (node && node.first && !collectionItems(node).length) { 2581 node = typeof node.first === 'string' ? await signedGetJson(slug, node.first) : node.first; 2582 } 2583 let pages = 0; 2584 while (node && pages++ < 3 && items.length < THREAD_VIEW_LIMIT) { 2585 items.push(...collectionItems(node)); 2586 if (!node.next) break; 2587 node = typeof node.next === 'string' ? await signedGetJson(slug, node.next) : node.next; 2588 } 2589 items = items.slice(0, THREAD_VIEW_LIMIT); 2590 2591 // Alles tegelijk in plaats van om de beurt: dertig vreemde servers na elkaar 2592 // afwachten is een halve minuut kijken naar een spinner. 2593 const objs = await Promise.all(items.map(async (it) => { 2594 const o = typeof it === 'string' ? await get(it) : (it && it.object && typeof it.object === 'object' ? it.object : it); 2595 return (o && o.id && o.attributedTo) ? o : null; 2596 })); 2597 2598 const kept = []; 2599 for (const o of objs) { 2600 if (!o) continue; 2601 const actorUri = actorUriOf(o.attributedTo); 2602 if (!actorUri || isBlockedAny(actorUri)) continue; // een blokkade telt niet mee 2603 kept.push({ o, actorUri }); 2604 } 2605 2606 // Bylines: één fetch per unieke auteur, niet één per antwoord. 2607 const authors = new Map(); 2608 await Promise.all([...new Set(kept.map((k) => k.actorUri))].map(async (uri) => { 2609 authors.set(uri, localActorObject(uri) || await signedGetJson(slug, uri).catch(() => null)); 2610 })); 2611 2612 const notes = kept.map(({ o, actorUri }) => readerNote(o, actorUri, authors.get(actorUri), objectUri)) 2613 .sort((a, b) => String(a.published || '').localeCompare(String(b.published || ''))); 2600 2614 2601 2615 const out = { notes, found: !!note, sourceStatus }; … … 2606 2620 } 2607 2621 return out; 2622 } 2623 2624 /** 2625 * Eén note, ondertekend opgehaald, in dezelfde vorm als een thread-antwoord. 2626 * 2627 * De thread hierboven geeft alleen wat ONDER een post hangt. Wie een gesprek 2628 * naar boven wil volgen -- een antwoord kwam binnen, de ouder nooit -- kan die 2629 * ouder zonder de sleutel die hier woont niet halen bij een server in secure 2630 * mode. Niets wordt bewaard, net als bij de thread. 2631 * 2632 * inReplyTo staat er alleen als de bron hem gaf: een verzonnen ouder is erger 2633 * dan geen. Een geblokkeerde auteur maakt de note onvindbaar, met dezelfde 2634 * status als een bron die nee zegt -- een blokkade hoort onzichtbaar te zijn. 2635 */ 2636 export async function getObject(slug, objectUri) { 2637 let sourceStatus = 0; 2638 const o = localNoteObject(objectUri, slug) 2639 || await signedGetJson(slug, objectUri, (st) => { sourceStatus = st; }); 2640 if (!o || !o.id || !o.attributedTo) return { note: null, found: false, sourceStatus }; 2641 const actorUri = actorUriOf(o.attributedTo); 2642 if (!actorUri || isBlockedAny(actorUri)) return { note: null, found: false, sourceStatus: 404 }; 2643 const author = localActorObject(actorUri) || await signedGetJson(slug, actorUri).catch(() => null); 2644 return { note: readerNote(o, actorUri, author), found: true, sourceStatus }; 2608 2645 } 2609 2646 … … 4346 4383 getNotifications, listBlocks, isBlockedAny, blockTarget, unblock, 4347 4384 deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker, 4348 sendMaybe304, etagFor, onGuardian, wakeGuardian, onGuardianshipEvent, proposeGate, getReplyUris, getThread, filterThreadToCircle, gateAttachments, stripEmojiTags, actorObject, previewObject, quoteObject, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio,4385 sendMaybe304, etagFor, onGuardian, wakeGuardian, onGuardianshipEvent, proposeGate, getReplyUris, getThread, getObject, filterThreadToCircle, gateAttachments, stripEmojiTags, actorObject, previewObject, quoteObject, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio, 4349 4386 linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections, 4350 4387 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)