Changeset cc3cf7d in Klonkt
- Timestamp:
- 07/26/2026 08:08:15 AM (6 weeks ago)
- Branches:
- main
- Children:
- 6fd0e20
- Parents:
- eb36688
- Files:
-
- 2 edited
-
src/services/ActivityPubService.js (modified) (5 diffs)
-
test/object-links.test.js (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
reb36688 rcc3cf7d 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 = extract ObjectLinkTags(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 // FEP-e232 + FEP-044f: keep the note's object-link/quote tags for the same read. 1542 { const lj = extractLinkJson(o); if (lj) { try { db.prepare('UPDATE ap_timeline SET link_json = ? WHERE id = ? AND slug = ?').run(lj, o.id, s.slug); } catch { /* ignore */ } } } 1543 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 */ } } 1544 1544 } … … 2605 2605 } 2606 2606 2607 // FEP-044f quote posts: a quote is usually NOT an FEP-e232 tag but an 2608 // object-level property. FEP-044f §"how to recognise" lists them all: 2609 // `quote` (the FEP property, a string or an embedded Link/object), and the 2610 // de-facto `quoteUrl` (as:), `quoteUri` (fedibird), `_misskey_quote` (misskey). 2611 // This returns the quoted object's URL from whichever is present. 2612 export function extractQuoteUrl(note) { 2613 if (!note || typeof note !== 'object') return null; 2614 const q = note.quote ?? note.quoteUrl ?? note.quoteUri ?? note['_misskey_quote']; 2615 if (!q) return null; 2616 if (typeof q === 'string') return q || null; 2617 if (typeof q === 'object') return (typeof q.id === 'string' && q.id) || (typeof q.href === 'string' && q.href) || null; 2618 return null; 2619 } 2620 2621 // The note's object-link tags for storage: real FEP-e232 Link tags PLUS any 2622 // FEP-044f object-level quote, normalised to one FEP-e232-shaped Link (rel 2623 // _misskey_quote) so the client's single object-link path renders them all. 2624 // Deduped by href. Returns the JSON to store (or null if the note has neither). 2625 export function extractLinkJson(note) { 2626 const links = []; 2627 const fromTag = extractObjectLinkTags(note && note.tag); 2628 if (fromTag) { try { links.push(...JSON.parse(fromTag)); } catch { /* ignore */ } } 2629 const qUrl = extractQuoteUrl(note); 2630 if (qUrl && !links.some((l) => l && l.href === qUrl)) { 2631 links.push({ type: 'Link', mediaType: 'application/activity+json', href: qUrl, 2632 rel: ['https://misskey-hub.net/ns#_misskey_quote'], name: qUrl }); 2633 } 2634 return links.length ? JSON.stringify(links) : null; 2635 } 2636 2607 2637 // ── Cirkel = posts from the accounts you auto-boost ("feature an artist") ── 2608 2638 let _abCount, _cirkelPosts, _cirkelMembers; … … 2701 2731 // during a flux window, e.g. a fleet-wide update), and drops notes that are gone 2702 2732 // (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync. 2703 const SELFHEAL_VERSION = 9; // v9: also re-capture FEP-e232 object-link tags (link_json) onto already-cached posts2733 const SELFHEAL_VERSION = 10; // v10: also capture FEP-044f object-level quotes (quote/quoteUrl/quoteUri/_misskey_quote) into link_json 2704 2734 async function fetchNoteAP(url) { 2705 2735 try { … … 2762 2792 // FEP-9098: keep custom-emoji tags from backfilled posts too. 2763 2793 { 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 = extract ObjectLinkTags(o.tag); if (lj) { try { db.prepare('UPDATE ap_timeline SET link_json = ? WHERE id = ? AND slug = ?').run(lj, o.id, slug); } catch { /* ignore */ } } }2794 // FEP-e232 + FEP-044f: keep object-link/quote tags from backfilled posts too. 2795 { const lj = extractLinkJson(o); if (lj) { try { db.prepare('UPDATE ap_timeline SET link_json = ? WHERE id = ? AND slug = ?').run(lj, o.id, slug); } catch { /* ignore */ } } } 2766 2796 // Set poll_json if this is a poll and we don't already have it (COALESCE preserves a vote). 2767 2797 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 */ } } … … 2897 2927 const url = note.url || null; // re-sync the human url (catches a remote slug rename) 2898 2928 const emoji = extractEmojiTags(note.tag); // FEP-9098: re-capture custom-emoji tags (v8) 2899 const link = extract ObjectLinkTags(note.tag); // FEP-e232: re-capture object-linktags (v9)2929 const link = extractLinkJson(note); // FEP-e232 + FEP-044f: re-capture object-link/quote tags (v9) 2900 2930 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 2931 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); -
test/object-links.test.js
reb36688 rcc3cf7d 6 6 const dbMod = await import('../src/config/database.js'); 7 7 dbMod.initializeDatabase(); 8 const { extractObjectLinkTags, timelineObjectLinks } = await import('../src/services/ActivityPubService.js');9 const AP = { extractObjectLinkTags, timelineObjectLinks };8 const { extractObjectLinkTags, timelineObjectLinks, extractQuoteUrl, extractLinkJson } = await import('../src/services/ActivityPubService.js'); 9 const AP = { extractObjectLinkTags, timelineObjectLinks, extractQuoteUrl, extractLinkJson }; 10 10 11 11 test('extractObjectLinkTags keeps AS2-profiled ld+json and activity+json Links; drops plain links and mentions', () => { … … 31 31 assert.equal(AP.timelineObjectLinks('not json'), undefined); 32 32 }); 33 34 // FEP-044f: object-level quote properties are the common representation. 35 test('extractQuoteUrl reads quote / quoteUrl / quoteUri / _misskey_quote (string or embedded object)', () => { 36 assert.equal(AP.extractQuoteUrl({ quote: 'https://s/objects/9' }), 'https://s/objects/9'); 37 assert.equal(AP.extractQuoteUrl({ quoteUrl: 'https://s/q1' }), 'https://s/q1'); 38 assert.equal(AP.extractQuoteUrl({ quoteUri: 'https://s/q2' }), 'https://s/q2'); 39 assert.equal(AP.extractQuoteUrl({ _misskey_quote: 'https://s/q3' }), 'https://s/q3'); 40 assert.equal(AP.extractQuoteUrl({ quote: { type: 'Link', href: 'https://s/q4' } }), 'https://s/q4'); 41 assert.equal(AP.extractQuoteUrl({ content: 'no quote' }), null); 42 }); 43 44 test('extractLinkJson normalises an object-level quote into one FEP-e232 Link (rel _misskey_quote)', () => { 45 const json = AP.extractLinkJson({ content: 'nice', quoteUrl: 'https://s/objects/9' }); 46 const arr = AP.timelineObjectLinks(json); 47 assert.equal(arr.length, 1); 48 assert.equal(arr[0].href, 'https://s/objects/9'); 49 assert.ok(arr[0].rel.some((r) => r.includes('quote'))); 50 }); 51 52 test('extractLinkJson merges a real FEP-e232 Link with an object-level quote, deduped by href', () => { 53 const note = { 54 tag: [{ type: 'Link', mediaType: 'application/activity+json', href: 'https://s/objects/9' }], 55 quoteUrl: 'https://s/objects/9', // same target → not duplicated 56 }; 57 const arr = AP.timelineObjectLinks(AP.extractLinkJson(note)); 58 assert.equal(arr.length, 1); 59 assert.equal(arr[0].href, 'https://s/objects/9'); 60 });
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)