Changeset eb36688 in Klonkt


Ignore:
Timestamp:
07/26/2026 08:01:25 AM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
cc3cf7d
Parents:
8f8b40f
Message:

FEP-e232 emit: Klonkt bewaart + serveert object-link (quote) tags op de C2S inbox

De client toonde nog geen quotes omdat Klonkt de Link-tags helemaal niet
doorgaf: de inbox-read serveerde alleen de FEP-9098 Emoji-tags (uit emoji_json),
al het andere werd weggegooid. Nu bewaren we ook de FEP-e232 Link-tags (quotes /
inline object-referenties) en serveren we ze mee, precies gespiegeld op de
emoji-aanpak.

Een Link telt als object-link als de mediaType een AP-object markeert: het
AS2-geprofileerde application/ld+json, of application/activity+json als
gelijkwaardige. Kale hyperlinks (text/html) en Mentions blijven eruit.

De inbox-read combineert nu Emoji- en Link-tags in een enkele tag-array (weg
als de note geen van beide heeft). De self-heal gaat naar v9 zodat bestaande
timeline-rijen de Link-tags eenmalig alsnog oppikken.

Changed files:
src/config/database.js

  • ap_timeline.link_json kolom (FEP-e232 object-link tags)

src/services/ActivityPubService.js

  • extractObjectLinkTags() + timelineObjectLinks() (spiegel van de emoji-functies)
  • link_json opslaan bij inbound Create en bij outbox-backfill
  • self-heal v8 -> v9: link_json meenemen (select, change-detect, update)
  • timelineObjectLinks in de default-export

src/routes/activitypub.js

  • inbox-read serveert Emoji- + Link-tags samen als tag

New file:
test/object-links.test.js

  • extractObjectLinkTags houdt AS2-ld+json en activity+json Links, laat text/html en Mention vallen; round-trip via timelineObjectLinks

remarks: 175 tests groen (was 171).

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r8f8b40f reb36688  
    548548  ensureColumn('ap_timeline', 'cw', 'TEXT');                 // remote content-warning text
    549549  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`
    550551  ensureColumn('ap_timeline', 'reblog_name', 'TEXT');        // a followed account boosted this → "X boosted"
    551552  ensureColumn('ap_timeline', 'reblog_handle', 'TEXT');      //   the booster's @handle
  • src/routes/activitypub.js

    r8f8b40f reb36688  
    162162      // client renders their images/audio like own outbox posts.
    163163      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      })(),
    167172    },
    168173  }));
  • src/services/ActivityPubService.js

    r8f8b40f reb36688  
    15391539          // FEP-9098: keep the note's custom-emoji tags so the C2S inbox read can serve them.
    15401540          { 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 */ } } }
    15411543          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 */ } }
    15421544        }
     
    25822584}
    25832585
     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.
     2591export 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}
     2602export 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
    25842607// ── Cirkel = posts from the accounts you auto-boost ("feature an artist") ──
    25852608let _abCount, _cirkelPosts, _cirkelMembers;
     
    26782701// during a flux window, e.g. a fleet-wide update), and drops notes that are gone
    26792702// (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 posts
     2703const SELFHEAL_VERSION = 9; // v9: also re-capture FEP-e232 object-link tags (link_json) onto already-cached posts
    26812704async function fetchNoteAP(url) {
    26822705  try {
     
    27392762        // FEP-9098: keep custom-emoji tags from backfilled posts too.
    27402763        { 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 */ } } }
    27412766        // Set poll_json if this is a poll and we don't already have it (COALESCE preserves a vote).
    27422767        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 */ } }
     
    28592884    if (cur >= SELFHEAL_VERSION) return; // already healed for this version — skip on normal boots
    28602885    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 */ }
    28622887    let healed = 0, failed = 0;
    28632888    for (const r of rows) {
     
    28722897        const url = note.url || null;          // re-sync the human url (catches a remote slug rename)
    28732898        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);
    28762902          healed++;
    28772903        }
     
    33773403  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
    33783404  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,
    33803406  acceptGatedFollow, rejectGatedFollow, isWardGuardian, sendFollowDecision,
    33813407  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
Note: See TracChangeset for help on using the changeset viewer.