Changeset a677616 in Klonkt


Ignore:
Timestamp:
07/26/2026 08:47:16 AM (6 weeks ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
f3caf19
Parents:
46a30f0
Message:

shaer:author.emojis: custom-emoji in de display-naam op de byline

Een display-naam als 'Laura :bongoCat:' toonde de letterlijke shortcode. De
emoji's van een naam zitten in de ACTOR-tags (niet de note), dus die geeft
Klonkt nu mee als shaer:author.emojis. Lean: actorInfo berekent de emoji-map
alleen als de naam echt een :shortcode: heeft; opslag in author_emoji_json op
inbound, boost en backfill; self-heal v12 -> v13 haalt hem eenmalig op voor
bestaande rijen (alleen als de naam een shortcode heeft, dus 1 actor-fetch per
zo'n rij, niet voor iedereen).

Changed files:
src/config/database.js

  • ap_timeline.author_emoji_json kolom

src/services/ActivityPubService.js

  • actorInfo geeft emojis mee (alleen bij shortcode-naam); actorNameEmojis()
  • storeAuthorEmoji(); aangeroepen op inbound, boost (oai) en backfill
  • self-heal v13: author_emoji_json backfillen via 1 actor-fetch per emoji-naam

src/routes/activitypub.js

  • shaer:author.emojis meegeserveerd

remarks: 180 tests groen.

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

Location:
src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r46a30f0 ra677616  
    550550  ensureColumn('ap_timeline', 'link_json', 'TEXT');          // FEP-e232 object-link (quote/ref) tags from the inbound note, served back as `tag`
    551551  ensureColumn('ap_timeline', 'quote_json', 'TEXT');         // FEP-044f resolved quoted-post snapshot (author + content), for the embedded quote card
     552  ensureColumn('ap_timeline', 'author_emoji_json', 'TEXT');  // FEP-9098 custom emojis in the author's display name (shaer:author.emojis)
    552553  ensureColumn('ap_timeline', 'reblog_name', 'TEXT');        // a followed account boosted this → "X boosted"
    553554  ensureColumn('ap_timeline', 'reblog_handle', 'TEXT');      //   the booster's @handle
  • src/routes/activitypub.js

    r46a30f0 ra677616  
    180180        name: t.author_name || undefined, handle: t.author_handle || undefined,
    181181        icon: t.author_icon || undefined, url: t.author_url || undefined,
     182        // FEP-9098: emojis in the display name (":shortcode:"), if any.
     183        emojis: (() => { try { return t.author_emoji_json ? JSON.parse(t.author_emoji_json) : undefined; } catch { return undefined; } })(),
    182184      } : undefined,
    183185      // When a followed account boosted this, who did ("X boosted"). Omitted for
  • src/services/ActivityPubService.js

    r46a30f0 ra677616  
    869869  const handle = doc && doc.preferredUsername ? `@${doc.preferredUsername}@${host}` : deriveHandle(actorUri);
    870870  const icon = doc && doc.icon ? (doc.icon.url || (Array.isArray(doc.icon) && doc.icon[0] && doc.icon[0].url)) : null;
     871  const name = (doc && (doc.name || doc.preferredUsername)) || handle;
    871872  return {
    872     name: (doc && (doc.name || doc.preferredUsername)) || handle,
     873    name,
    873874    handle,
    874875    url: safeUrl((doc && (doc.url || doc.id)) || actorUri) || null,
    875876    icon: safeUrl(icon) || null,
     877    // FEP-9098 custom emojis in the display name (":shortcode:"), so the byline
     878    // renders them. Only computed when the name actually has a shortcode.
     879    emojis: /:[A-Za-z0-9_+-]+:/.test(name) ? actorNameEmojis(doc) : undefined,
    876880  };
     881}
     882
     883// Map ":shortcode:" → image url from an actor doc's Emoji tags (for a custom-
     884// emoji display name). Undefined when there are none.
     885function actorNameEmojis(doc) {
     886  const arr = doc && Array.isArray(doc.tag) ? doc.tag : (doc && doc.tag ? [doc.tag] : []);
     887  const out = {};
     888  for (const t of arr) {
     889    if (!t || (Array.isArray(t.type) ? t.type[0] : t.type) !== 'Emoji' || typeof t.name !== 'string' || !t.icon) continue;
     890    const u = t.icon.url || (Array.isArray(t.icon) && t.icon[0] && t.icon[0].url);
     891    if (u) out[t.name] = u;
     892  }
     893  return Object.keys(out).length ? out : undefined;
    877894}
    878895
     
    15391556          // FEP-9098: keep the note's custom-emoji tags so the C2S inbox read can serve them.
    15401557          { 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 */ } } }
     1558          storeAuthorEmoji(o.id, s.slug, ai);   // custom-emoji display name for the byline
     1559
    15411560          // FEP-e232 + FEP-044f: keep the note's object-link/quote tags for the same read.
    15421561          { 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 */ } } }
     
    16721691            try { const r = tlStmts().ins.run(bn.id, s.slug, origUri || '', oai.name, oai.handle, oai.icon, oai.url, html, bn.url || null, new Date().toISOString(), media, bn.sensitive ? 1 : 0, bn.summary || null); inserted = r.changes > 0; } catch { /* ignore */ }
    16731692            if (inserted) { try { db.prepare('UPDATE ap_timeline SET reblog_name = ?, reblog_handle = ?, reblog_icon = ? WHERE slug = ? AND id = ?').run(booster.name, booster.handle, booster.icon, s.slug, bn.id); } catch { /* ignore */ } }
     1693            storeAuthorEmoji(bn.id, s.slug, oai);   // custom-emoji display name for the byline
    16741694          }
    16751695          console.log('[AP] timeline boost +', actorUri, 'x' + subs.length);
     
    26652685}
    26662686
     2687// Store the author's display-name emoji map (from actorInfo().emojis) on a
     2688// timeline row, so the byline can render a ":shortcode:" name. No-op when the
     2689// name has no custom emoji (the common case).
     2690function storeAuthorEmoji(id, slug, ai) {
     2691  if (!ai || !ai.emojis || !Object.keys(ai.emojis).length) return;
     2692  try { db.prepare('UPDATE ap_timeline SET author_emoji_json = ? WHERE id = ? AND slug = ?').run(JSON.stringify(ai.emojis), id, slug); } catch { /* ignore */ }
     2693}
     2694
    26672695// ── Cirkel = posts from the accounts you auto-boost ("feature an artist") ──
    26682696let _abCount, _cirkelPosts, _cirkelMembers;
     
    27612789// during a flux window, e.g. a fleet-wide update), and drops notes that are gone
    27622790// (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync.
    2763 const SELFHEAL_VERSION = 12; // v12: quote card snapshot media as array + quoted-post emojis
     2791const SELFHEAL_VERSION = 13; // v13: capture custom-emoji display names (author_emoji_json) onto already-cached posts
    27642792async function fetchNoteAP(url) {
    27652793  try {
     
    28562884        // FEP-9098: keep custom-emoji tags from backfilled posts too.
    28572885        { 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 */ } } }
     2886        storeAuthorEmoji(o.id, slug, ai);   // custom-emoji display name for the byline
    28582887        // FEP-e232 + FEP-044f: keep object-link/quote tags from backfilled posts too.
    28592888        { 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 */ } } }
     
    29803009    if (cur >= SELFHEAL_VERSION) return; // already healed for this version — skip on normal boots
    29813010    let rows = [];
    2982     try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url, emoji_json, link_json, quote_json FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ }
     3011    try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url, emoji_json, link_json, quote_json, author_uri, author_name, author_emoji_json FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ }
    29833012    let healed = 0, failed = 0;
    29843013    for (const r of rows) {
     
    30003029          db.prepare('UPDATE ap_timeline SET content = ?, media_json = ?, nsfw = ?, cw = ?, url = COALESCE(?, url), emoji_json = ?, link_json = ?, quote_json = ? WHERE id = ?').run(html || r.content, media, nsfw, cw, url, emoji, link, quote, r.id);
    30013030          healed++;
     3031        }
     3032        // v13: a custom-emoji display name needs the author's emoji map. Fetch
     3033        // the actor once, only for rows whose name has a shortcode and no map yet.
     3034        if (/:[A-Za-z0-9_+-]+:/.test(r.author_name || '') && !r.author_emoji_json && r.author_uri) {
     3035          const ai = actorInfo(await fetchActor(r.author_uri), r.author_uri);
     3036          if (ai.emojis) { try { db.prepare('UPDATE ap_timeline SET author_emoji_json = ? WHERE id = ?').run(JSON.stringify(ai.emojis), r.id); } catch { /* ignore */ } }
    30023037        }
    30033038      } catch { failed++; /* per-note best-effort */ }
Note: See TracChangeset for help on using the changeset viewer.