Changeset eb36688 in Klonkt
- Timestamp:
- 07/26/2026 08:01:25 AM (6 weeks ago)
- Branches:
- main
- Children:
- cc3cf7d
- Parents:
- 8f8b40f
- Files:
-
- 1 added
- 3 edited
-
src/config/database.js (modified) (1 diff)
-
src/routes/activitypub.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (7 diffs)
-
test/object-links.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r8f8b40f reb36688 548 548 ensureColumn('ap_timeline', 'cw', 'TEXT'); // remote content-warning text 549 549 ensureColumn('ap_timeline', 'emoji_json', 'TEXT'); // FEP-9098 custom emoji Emoji tags from the inbound note, served back as `tag` 550 ensureColumn('ap_timeline', 'link_json', 'TEXT'); // FEP-e232 object-link (quote/ref) tags from the inbound note, served back as `tag` 550 551 ensureColumn('ap_timeline', 'reblog_name', 'TEXT'); // a followed account boosted this → "X boosted" 551 552 ensureColumn('ap_timeline', 'reblog_handle', 'TEXT'); // the booster's @handle -
src/routes/activitypub.js
r8f8b40f reb36688 162 162 // client renders their images/audio like own outbox posts. 163 163 attachment: AP.timelineAttachments(t.media_json), 164 // FEP-9098 custom emojis: the note's Emoji tags, so the client can 165 // render :shortcode: as an image instead of literal text. 166 tag: AP.timelineEmojis(t.emoji_json), 164 // The note's preserved tags, so the client can render them: FEP-9098 165 // Emoji tags (:shortcode: → image) and FEP-e232 Link tags (quotes / 166 // inline object references). Combined into one `tag` array; omitted 167 // when the note has neither. 168 tag: (() => { 169 const tags = [...(AP.timelineEmojis(t.emoji_json) || []), ...(AP.timelineObjectLinks(t.link_json) || [])]; 170 return tags.length ? tags : undefined; 171 })(), 167 172 }, 168 173 })); -
src/services/ActivityPubService.js
r8f8b40f reb36688 1539 1539 // FEP-9098: keep the note's custom-emoji tags so the C2S inbox read can serve them. 1540 1540 { const ej = extractEmojiTags(o.tag); if (ej) { try { db.prepare('UPDATE ap_timeline SET emoji_json = ? WHERE id = ? AND slug = ?').run(ej, o.id, s.slug); } catch { /* ignore */ } } } 1541 // FEP-e232: keep the note's object-link (quote/ref) tags for the same read. 1542 { const lj = extractObjectLinkTags(o.tag); if (lj) { try { db.prepare('UPDATE ap_timeline SET link_json = ? WHERE id = ? AND slug = ?').run(lj, o.id, s.slug); } catch { /* ignore */ } } } 1541 1543 if (poll) { try { db.prepare('UPDATE ap_timeline SET poll_json = ? WHERE id = ? AND slug = ?').run(JSON.stringify(poll), o.id, s.slug); } catch { /* ignore */ } } 1542 1544 } … … 2582 2584 } 2583 2585 2586 // FEP-e232 object links (quotes / inline references). Inbound: keep the note's 2587 // Link tags whose mediaType marks an AP object (the AS2-profiled ld+json, or 2588 // activity+json as its equivalent) as JSON, so the C2S inbox read can serve 2589 // them back and a client (Shaer) can render the quote/reference. Mirrors 2590 // extractEmojiTags. Plain hyperlinks (text/html) and Mentions are dropped. 2591 export function extractObjectLinkTags(tag) { 2592 const arr = Array.isArray(tag) ? tag : (tag ? [tag] : []); 2593 const links = arr.filter((t) => { 2594 if (!t || (Array.isArray(t.type) ? t.type[0] : t.type) !== 'Link') return false; 2595 if (typeof t.href !== 'string' || !t.href) return false; 2596 const mt = String(t.mediaType || '').toLowerCase(); 2597 return (mt.startsWith('application/ld+json') && mt.includes('activitystreams')) 2598 || mt.startsWith('application/activity+json'); 2599 }); 2600 return links.length ? JSON.stringify(links) : null; 2601 } 2602 export function timelineObjectLinks(linkJson) { 2603 try { const arr = linkJson ? JSON.parse(linkJson) : null; return (Array.isArray(arr) && arr.length) ? arr : undefined; } 2604 catch { return undefined; } 2605 } 2606 2584 2607 // ── Cirkel = posts from the accounts you auto-boost ("feature an artist") ── 2585 2608 let _abCount, _cirkelPosts, _cirkelMembers; … … 2678 2701 // during a flux window, e.g. a fleet-wide update), and drops notes that are gone 2679 2702 // (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync. 2680 const SELFHEAL_VERSION = 8; // v8: also re-capture FEP-9098 custom-emoji tags (emoji_json) onto already-cached posts2703 const SELFHEAL_VERSION = 9; // v9: also re-capture FEP-e232 object-link tags (link_json) onto already-cached posts 2681 2704 async function fetchNoteAP(url) { 2682 2705 try { … … 2739 2762 // FEP-9098: keep custom-emoji tags from backfilled posts too. 2740 2763 { const ej = extractEmojiTags(o.tag); if (ej) { try { db.prepare('UPDATE ap_timeline SET emoji_json = ? WHERE id = ? AND slug = ?').run(ej, o.id, slug); } catch { /* ignore */ } } } 2764 // FEP-e232: keep object-link (quote/ref) tags from backfilled posts too. 2765 { const lj = extractObjectLinkTags(o.tag); if (lj) { try { db.prepare('UPDATE ap_timeline SET link_json = ? WHERE id = ? AND slug = ?').run(lj, o.id, slug); } catch { /* ignore */ } } } 2741 2766 // Set poll_json if this is a poll and we don't already have it (COALESCE preserves a vote). 2742 2767 if (poll) { try { db.prepare('UPDATE ap_timeline SET poll_json = COALESCE(poll_json, ?) WHERE id = ? AND slug = ?').run(JSON.stringify(poll), o.id, slug); } catch { /* ignore */ } } … … 2859 2884 if (cur >= SELFHEAL_VERSION) return; // already healed for this version — skip on normal boots 2860 2885 let rows = []; 2861 try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url, emoji_json FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ }2886 try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url, emoji_json, link_json FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ } 2862 2887 let healed = 0, failed = 0; 2863 2888 for (const r of rows) { … … 2872 2897 const url = note.url || null; // re-sync the human url (catches a remote slug rename) 2873 2898 const emoji = extractEmojiTags(note.tag); // FEP-9098: re-capture custom-emoji tags (v8) 2874 if ((html && html !== r.content) || media !== (r.media_json || '[]') || nsfw !== (r.nsfw || 0) || (cw || '') !== (r.cw || '') || (url && url !== r.url) || (emoji || '') !== (r.emoji_json || '')) { 2875 db.prepare('UPDATE ap_timeline SET content = ?, media_json = ?, nsfw = ?, cw = ?, url = COALESCE(?, url), emoji_json = ? WHERE id = ?').run(html || r.content, media, nsfw, cw, url, emoji, r.id); 2899 const link = extractObjectLinkTags(note.tag); // FEP-e232: re-capture object-link tags (v9) 2900 if ((html && html !== r.content) || media !== (r.media_json || '[]') || nsfw !== (r.nsfw || 0) || (cw || '') !== (r.cw || '') || (url && url !== r.url) || (emoji || '') !== (r.emoji_json || '') || (link || '') !== (r.link_json || '')) { 2901 db.prepare('UPDATE ap_timeline SET content = ?, media_json = ?, nsfw = ?, cw = ?, url = COALESCE(?, url), emoji_json = ?, link_json = ? WHERE id = ?').run(html || r.content, media, nsfw, cw, url, emoji, link, r.id); 2876 2902 healed++; 2877 2903 } … … 3377 3403 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote, 3378 3404 listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote, 3379 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, sendInteraction, voteOnPoll, voteOnRemotePoll,3405 webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, timelineEmojis, timelineObjectLinks, sendInteraction, voteOnPoll, voteOnRemotePoll, 3380 3406 acceptGatedFollow, rejectGatedFollow, isWardGuardian, sendFollowDecision, 3381 3407 parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
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)