Changeset 7922694 in Klonkt


Ignore:
Timestamp:
07/20/2026 11:03:52 PM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
5143ccf
Parents:
155c24e
git-author:
Robin <roboburr@…> (07/20/2026 11:03:51 PM)
git-committer:
Robin <roboburr@…> (07/20/2026 11:03:52 PM)
Message:

Feature: owner followers/following carry name + avatar (shaer-aa3)

The C2S owner view of followers and following returned bare actor
URIs, so a client could only show ids. Each entry is now an AS2 actor
reference { id, type: Person, name, preferredUsername, icon } built
from the best cached display Klonkt already holds: the follower's own
row (now cached at Follow time), then ap_following, interactions,
timeline, mentions, falling back to a handle derived from the URI.
Priority is the set display name, then the chosen username, then the
id. ap_followers gains name/handle/icon, populated when an inbound
Follow is accepted (fetchActor already runs there).

Changed files:
src/config/database.js

  • additive columns ap_followers.name/handle/icon

src/services/ActivityPubService.js

  • cache follower display on inbound Follow
  • actorDisplay(slug, uri): best cached display across the caches
  • buildActorRef(slug, uri): AS2 actor reference with display

src/routes/activitypub.js

  • owner followers/following map URIs through buildActorRef

New file:
test/c2s-contacts.test.js

  • name/username/id priority + interaction-cache fallback

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

Files:
1 added
3 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r155c24e r7922694  
    444444  ensureColumn('ap_outbox', 'to_actors', 'TEXT');   // JSON array of recipient actor URIs for direct notes
    445445  ensureColumn('ap_outbox', 'help_request', 'INTEGER'); // FEP-633c shaer:helpRequest (ward's call for help)
     446  ensureColumn('ap_followers', 'name', 'TEXT');    // cached display name (shaer-aa3)
     447  ensureColumn('ap_followers', 'handle', 'TEXT');  // @user@host
     448  ensureColumn('ap_followers', 'icon', 'TEXT');    // avatar URL
    446449}
    447450
  • src/routes/activitypub.js

    r155c24e r7922694  
    175175  if (!site) return res.status(404).end();
    176176  if (owner) {
    177     const items = db.prepare('SELECT actor_uri FROM ap_followers WHERE slug = ? ORDER BY created_at').all(site.slug).map((r) => r.actor_uri);
     177    const uris = db.prepare('SELECT actor_uri FROM ap_followers WHERE slug = ? ORDER BY created_at').all(site.slug).map((r) => r.actor_uri);
     178    const items = uris.map((u) => AP.buildActorRef(site.slug, u));   // name + avatar (shaer-aa3)
    178179    return AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, items.length, items));
    179180  }
     
    190191  if (owner) {
    191192    let items = [];
    192     try { items = db.prepare("SELECT actor_uri FROM ap_following WHERE slug = ? AND status = 'accepted' ORDER BY created_at").all(site.slug).map((r) => r.actor_uri); } catch { /* table may not exist */ }
     193    try { items = db.prepare("SELECT actor_uri FROM ap_following WHERE slug = ? AND status = 'accepted' ORDER BY created_at").all(site.slug).map((r) => AP.buildActorRef(site.slug, r.actor_uri)); } catch { /* table may not exist */ }
    193194    return AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, items.length, items));
    194195  }
  • src/services/ActivityPubService.js

    r155c24e r7922694  
    621621
    622622// ── followers store (lazy stmts) ──────────────────────────────────
    623 let _insF, _delF, _listF, _cntF;
     623let _insF, _updFDisp, _delF, _listF, _cntF;
    624624function fStmts() {
    625625  if (!_insF) {
    626     _insF = db.prepare('INSERT OR IGNORE INTO ap_followers (slug, actor_uri, inbox, shared_inbox, created_at) VALUES (?,?,?,?,CURRENT_TIMESTAMP)');
     626    _insF = db.prepare('INSERT OR IGNORE INTO ap_followers (slug, actor_uri, inbox, shared_inbox, name, handle, icon, created_at) VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
     627    _updFDisp = db.prepare('UPDATE ap_followers SET name = COALESCE(?, name), handle = COALESCE(?, handle), icon = COALESCE(?, icon) WHERE slug = ? AND actor_uri = ?');
    627628    _delF = db.prepare('DELETE FROM ap_followers WHERE slug = ? AND actor_uri = ?');
    628629    _listF = db.prepare('SELECT inbox, shared_inbox FROM ap_followers WHERE slug = ?');
     
    646647  const info = db.prepare('DELETE FROM ap_followers WHERE slug = ? AND id = ?').run(slug, id);
    647648  return info.changes > 0;
     649}
     650
     651// Best cached display for an actor URI, across the caches Klonkt already fills:
     652// followers (now with name/icon), following, interactions, timeline, mentions.
     653// Falls back to a handle derived from the URI. Display info is not sensitive.
     654export function actorDisplay(slug, uri) {
     655  const ok = (r) => r && (r.name || r.icon);
     656  try {
     657    let r = db.prepare('SELECT name, handle, icon FROM ap_followers WHERE slug = ? AND actor_uri = ?').get(slug, uri);
     658    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
     659    r = db.prepare('SELECT name, handle, icon FROM ap_following WHERE slug = ? AND actor_uri = ?').get(slug, uri);
     660    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
     661    r = db.prepare('SELECT actor_name AS name, actor_handle AS handle, actor_icon AS icon FROM ap_interactions WHERE actor_uri = ? AND (actor_name IS NOT NULL OR actor_icon IS NOT NULL) ORDER BY created_at DESC LIMIT 1').get(uri);
     662    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
     663    r = db.prepare('SELECT author_name AS name, author_handle AS handle, author_icon AS icon FROM ap_timeline WHERE author_uri = ? AND (author_name IS NOT NULL OR author_icon IS NOT NULL) LIMIT 1').get(uri);
     664    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
     665    r = db.prepare('SELECT actor_name AS name, actor_handle AS handle, actor_icon AS icon FROM ap_mentions WHERE actor_uri = ? AND (actor_name IS NOT NULL OR actor_icon IS NOT NULL) ORDER BY created_at DESC LIMIT 1').get(uri);
     666    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
     667  } catch { /* ignore */ }
     668  return { name: null, handle: deriveHandle(uri), icon: null };
     669}
     670
     671// AS2 actor reference with display, for the owner C2S followers/following view.
     672// preferredUsername = the local part of the handle; name = the set display name.
     673export function buildActorRef(slug, uri) {
     674  const d = actorDisplay(slug, uri);
     675  const user = d.handle && d.handle[0] === '@' ? d.handle.slice(1).split('@')[0] : null;
     676  const out = { id: uri, type: 'Person' };
     677  if (d.name) out.name = d.name;
     678  if (user) out.preferredUsername = user;
     679  if (d.icon) out.icon = { type: 'Image', url: d.icon };
     680  return out;
    648681}
    649682
     
    12551288    if (!remote || !remote.inbox) return 202; // can't reach them → drop quietly
    12561289    const sharedInbox = (remote.endpoints && remote.endpoints.sharedInbox) || null;
    1257     fStmts().ins.run(slug, who, remote.inbox, sharedInbox);
     1290    const fi = actorInfo(remote, who);   // cache display for the friends list (shaer-aa3)
     1291    fStmts().ins.run(slug, who, remote.inbox, sharedInbox, fi.name, fi.handle, fi.icon);
     1292    try { _updFDisp.run(fi.name, fi.handle, fi.icon, slug, who); } catch { /* best effort */ }
    12581293    const me = actorId(base, slug);
    12591294    const keys = getOrCreateKeys(slug);
     
    30353070  linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
    30363071  noteVisibility, isRejectedObject, rejectInteraction, interactionReportTarget,
    3037   getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility,
     3072  getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef,
    30383073};
Note: See TracChangeset for help on using the changeset viewer.