Changeset f9f3312 in Klonkt


Ignore:
Timestamp:
06/25/2026 07:31:05 AM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
55bba23
Parents:
2af2e69
Message:

feat(fediverse): auto-backfill recent posts to new followers on Follow

Mastodon doesn't fetch an account's history on follow, so new followers saw an empty
timeline. On Follow->Accept we now deliver our most recent 20 published, non-fan posts
as Create (oldest-first) to the new follower's inbox. Fire-and-forget; no more manual
backfills.

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r2af2e69 rf9f3312  
    524524    const accept = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}#accept-${Date.now()}`, type: 'Accept', actor: me, object: act };
    525525    deliver(remote.inbox, accept, `${me}#main-key`, keys.private_pem).catch((e) => console.warn('[AP] Accept delivery failed:', e.message));
     526    // Auto-backfill: send the new follower our recent posts as Create so their
     527    // timeline isn't empty (Mastodon doesn't fetch history on follow). Fire-and-forget.
     528    backfillNewFollower(base, slug, remote.inbox).catch(() => { /* best-effort */ });
    526529    console.log('[AP] Follow', who, '→', slug, verified ? '(sig ok)' : '(sig unverified)');
    527530    return 202;
     
    619622  const create = buildCreate(base, site, post);
    620623  for (const inbox of inboxes) deliverWithRetry(site.slug, inbox, create, keyId, keys.private_pem);
     624}
     625
     626// On a new Follow, send that follower our most recent posts as Create so their
     627// timeline shows our history (Mastodon does not backfill on follow). Oldest-first
     628// so they sort into the follower's timeline at their original dates.
     629async function backfillNewFollower(base, slug, inbox) {
     630  if (!base || !slug || !inbox) return;
     631  const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug);
     632  if (!site) return;
     633  const recent = db.prepare(
     634    `SELECT id, slug, title, content, cover_image_url, published_at, created_at
     635     FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
     636     ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
     637  ).all(site.id).reverse();
     638  if (!recent.length) return;
     639  const keys = getOrCreateKeys(slug);
     640  const keyId = `${actorId(base, slug)}#main-key`;
     641  for (const p of recent) {
     642    try { await deliver(inbox, buildCreate(base, site, p), keyId, keys.private_pem); } catch { /* best-effort */ }
     643    await new Promise((r) => setTimeout(r, 150));
     644  }
     645  console.log('[AP] backfilled', recent.length, 'posts to new follower of', slug);
    621646}
    622647
Note: See TracChangeset for help on using the changeset viewer.