Changeset d9ad6c5 in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 07/28/2026 06:17:36 PM (6 weeks ago)
- Branches:
- main
- Children:
- a7bcf66
- Parents:
- 2708282
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (11 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
r2708282 rd9ad6c5 832 832 } 833 833 834 /** 835 * Does this note belong in the home timeline (de Krant)? 836 * 837 * Only if it is a POST. A direct note is addressed to named people, so it is a 838 * message: a plain DM, a ward's 🛟 help request (FEP-633c 5.2.1) or a 839 * guardian's wave. Those are stored as mentions instead and surface in 840 * Berichten and the Guardian PWA. A reply belongs to its thread, not the feed. 841 */ 842 export function belongsInTimeline(o) { 843 if (!o || !o.id || o.inReplyTo) return false; 844 return noteVisibility(o) !== 'direct'; 845 } 846 834 847 function iStmts() { 835 848 if (!_insI) { … … 1524 1537 iStmts().ins.run('reply', tgt.post_id, o.id || '', actorUri, ai.name, ai.handle, ai.url, ai.icon, html, o.published || null, tgt.parent_uri, noteVisibility(o), extractEmojiTags(o.tag), emojiJsonOf(ai.emojis)); 1525 1538 console.log('[AP] reply', actorUri, '→', tgt.post_id); 1539 // A reply is a post too: Berichten renders it the way de Krant renders a 1540 // timeline row, so it needs the same media and the same quote/preview card. 1541 { 1542 const where = 'kind = ? AND post_id = ? AND actor_uri = ? AND object_uri = ?'; 1543 const key = ['reply', tgt.post_id, actorUri, o.id || '']; 1544 const mj = mediaFromNote(o); 1545 if (mj && mj !== '[]') { try { db.prepare(`UPDATE ap_interactions SET media_json = ? WHERE ${where}`).run(mj, ...key); } catch { /* ignore */ } } 1546 resolveCard(o).then((c) => { 1547 if (!c) return; 1548 const col = c.column === 'quote_json' ? 'quote_json' : 'embed_json'; // never a value from the wire 1549 try { db.prepare(`UPDATE ap_interactions SET ${col} = ? WHERE ${where}`).run(c.json, ...key); } catch { /* ignore */ } 1550 }).catch(() => { /* best-effort */ }); 1551 } 1526 1552 { 1527 1553 // Private (followers/direct) replies push as a DM ping WITHOUT content … … 1541 1567 } 1542 1568 // Home timeline (client): a top-level post from an account we follow. 1543 if (actorUri && !isLocalActor && !o.inReplyTo && o.id) {1569 if (actorUri && !isLocalActor && belongsInTimeline(o)) { 1544 1570 let subs = []; try { subs = db.prepare('SELECT slug, auto_boost FROM ap_following WHERE actor_uri = ?').all(actorUri); } catch { /* table may not exist yet */ } 1545 1571 if (subs.length) { … … 1610 1636 for (const slug of slugs) { 1611 1637 try { 1612 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, help_request, wave, has_guardians, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)') 1613 .run(slug, o.id, safeUrl(o.url) || null, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.published || null, help ? 1 : 0, wave ? 1 : 0, hasG ? 1 : 0); 1638 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, help_request, wave, has_guardians, emoji_json, actor_emoji_json, media_json, created_at) 1639 VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)`) 1640 .run(slug, o.id, safeUrl(o.url) || null, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.published || null, help ? 1 : 0, wave ? 1 : 0, hasG ? 1 : 0, 1641 extractEmojiTags(o.tag), emojiJsonOf(ai.emojis), mediaFromNote(o)); 1614 1642 if (r.changes) { 1643 // The quote / link-preview card resolves out of band (a remote 1644 // fetch), exactly as it does for a timeline post, so the inbox 1645 // answer is never blocked on it. 1646 resolveCard(o).then((c) => { 1647 if (!c) return; 1648 const col = c.column === 'quote_json' ? 'quote_json' : 'embed_json'; // never a value from the wire 1649 try { db.prepare(`UPDATE ap_mentions SET ${col} = ? WHERE slug = ? AND object_uri = ?`).run(c.json, slug, o.id); } catch { /* ignore */ } 1650 }).catch(() => { /* best-effort */ }); 1615 1651 console.log('[AP] mention', actorUri, '→', slug, help ? '(help request)' : ''); 1616 1652 const vis = noteVisibility(o); … … 2849 2885 // during a flux window, e.g. a fleet-wide update), and drops notes that are gone 2850 2886 // (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync. 2851 const SELFHEAL_VERSION = 2 0; // v20: oEmbed provider registry, so platforms that hide their tags from a server still resolve2887 const SELFHEAL_VERSION = 21; // v21: drop direct notes (🛟 help requests, waves) that were cached as timeline posts 2852 2888 async function fetchNoteAP(url) { 2853 2889 try { … … 2991 3027 return JSON.stringify(snapshot); 2992 3028 } 3029 3030 /** 3031 * The card under a post: a fediverse quote (FEP-044f) when the note has one, 3032 * otherwise an external link preview. Both render as the SAME card, so only one 3033 * of the two is ever stored. Returns {column, json} or null. 3034 * 3035 * Both halves reach out over the network, which is why every caller runs this 3036 * out of band: an inbox answer must never wait on a third party. 3037 */ 3038 async function resolveCard(o) { 3039 if (quoteHrefOf(o)) { 3040 const qj = await resolveQuote(o); 3041 return qj ? { column: 'quote_json', json: qj } : null; 3042 } 3043 const ej = await resolveExternalEmbed(o && o.content); 3044 return ej ? { column: 'embed_json', json: ej } : null; 3045 } 3046 2993 3047 // A generic SSRF-safe AP GET (collections / pages). 2994 3048 async function apGetJson(url) { … … 3158 3212 try { const r = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('selfheal_version'); cur = r ? (parseInt(r.value, 10) || 0) : 0; } catch { return; } 3159 3213 if (cur >= SELFHEAL_VERSION) return; // already healed for this version — skip on normal boots 3214 // v21: direct notes used to land in the timeline as if they were posts, so a 3215 // ward's 🛟 help request showed up in the guardian's Krant. The insert now 3216 // refuses them; drop the ones already cached. Scoped to the two kinds we can 3217 // still recognise afterwards (help request, wave) — a plain public mention 3218 // from someone you follow IS a timeline post and must stay. 3219 try { 3220 const r = db.prepare(`DELETE FROM ap_timeline WHERE EXISTS ( 3221 SELECT 1 FROM ap_mentions m 3222 WHERE m.object_uri = ap_timeline.id AND m.slug = ap_timeline.slug 3223 AND (m.help_request = 1 OR m.wave = 1))`).run(); 3224 if (r.changes) console.log(`[AP] self-heal v21: ${r.changes} direct note(s) removed from the timeline`); 3225 } catch { /* table may predate the columns */ } 3160 3226 let rows = []; 3161 3227 try { rows = db.prepare('SELECT id, slug, content, media_json, nsfw, cw, url, emoji_json, link_json, quote_json, author_uri, author_name, author_emoji_json, reblog_name, reblog_handle, reblog_emoji_json, embed_json FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ } … … 3456 3522 const rows = db.prepare(` 3457 3523 SELECT i.kind, i.actor_name, i.actor_handle, i.actor_url, i.actor_icon, i.content, i.created_at, i.visibility, 3458 i.emoji_json, i.actor_emoji_json, 3524 i.emoji_json, i.actor_emoji_json, i.media_json, i.quote_json, i.embed_json, 3459 3525 p.slug AS post_slug, p.title AS post_title 3460 3526 FROM ap_interactions i LEFT JOIN posts p ON p.id = i.post_id … … 3466 3532 content: stripLeadingMentions(r.content), post_slug: r.post_slug, post_title: r.post_title, created_at: r.created_at, 3467 3533 emoji_json: r.emoji_json, actor_emoji_json: r.actor_emoji_json, // FEP-9098 (messages render) 3534 media_json: r.media_json, quote_json: r.quote_json, embed_json: r.embed_json, // rendered like a Krant post 3468 3535 // followers/direct = a private message to the owner (not on the public thread) → 🔒 in Messages 3469 3536 visibility: r.visibility || 'public', … … 3488 3555 } catch { /* ignore */ } 3489 3556 try { 3490 for (const r of db.prepare('SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, wave, created_at FROM ap_mentions WHERE slug = ? ORDER BY created_at DESC LIMIT ?').all(slug, L)) { 3491 out.push({ type: 'mention', name: r.actor_name, handle: r.actor_handle, url: r.actor_url || r.actor_uri, icon: r.actor_icon, content: stripLeadingMentions(r.content), note_url: r.note_url || r.object_uri, wave: r.wave ? 1 : 0, actorUri: r.actor_uri, created_at: r.created_at }); 3557 for (const r of db.prepare(`SELECT object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, wave, help_request, created_at, 3558 emoji_json, actor_emoji_json, media_json, quote_json, embed_json 3559 FROM ap_mentions WHERE slug = ? ORDER BY created_at DESC LIMIT ?`).all(slug, L)) { 3560 out.push({ type: 'mention', name: r.actor_name, handle: r.actor_handle, url: r.actor_url || r.actor_uri, icon: r.actor_icon, content: stripLeadingMentions(r.content), note_url: r.note_url || r.object_uri, wave: r.wave ? 1 : 0, help_request: r.help_request ? 1 : 0, actorUri: r.actor_uri, created_at: r.created_at, 3561 // Same trimmings a Krant row has, so Berichten renders the post identically. 3562 emoji_json: r.emoji_json, actor_emoji_json: r.actor_emoji_json, media_json: r.media_json, quote_json: r.quote_json, embed_json: r.embed_json }); 3492 3563 } 3493 3564 } catch { /* ignore */ } … … 3716 3787 getReplyUris, markNotificationsSeen, countUnseenNotifications, hasPlayableAudio, 3717 3788 linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections, 3718 noteVisibility, isRejectedObject, rejectInteraction, interactionReportTarget,3789 noteVisibility, belongsInTimeline, isRejectedObject, rejectInteraction, interactionReportTarget, 3719 3790 getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef, prefersEnriched, 3720 3791 };
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)