Changeset 80c36a1 in Klonkt


Ignore:
Timestamp:
06/27/2026 01:04:55 AM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
dcff893
Parents:
e390844
Message:

feat(fedi): fans-only = followers-only federation (option A)

A fan_only post now federates to your followers but addressed followers-only (to:
followers, no Public) so only they see it in their feed and it can't be boosted public.
buildNote sets the audience from post.fan_only; create/save/scheduler/delete hooks no
longer skip fan_only (they pass fan_only through). The public outbox still excludes
fan_only, and the on-site fan-gate is unchanged (so it stays hidden on the site).

Location:
src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/posts.js

    re390844 r80c36a1  
    232232    } catch (e) { /* FTS index issues are non-fatal */ }
    233233
    234     // ActivityPub: federate a freshly published public post to followers.
    235     if (!fanOnly) {
     234    // ActivityPub: federate a freshly published post to followers. fan_only → delivered
     235    // to followers but addressed followers-only (option A: "fans" = your fedi followers).
     236    if (status === 'published') {
    236237      ActivityPubService.deliverCreate(site, {
    237238        id: postId, slug: finalSlug, title: title || finalSlug,
    238239        content: cleanContent, cover_image_url: cover_image_url || null,
    239         published_at: publishedAt, created_at: now,
     240        published_at: publishedAt, created_at: now, fan_only: fanOnly,
    240241      }).catch(() => { /* best-effort */ });
    241242    }
     
    356357  // ActivityPub: federate edits to followers. A post that BECOMES published →
    357358  // Create (new post); an already-published post that's edited → Update (so
    358   // Mastodon refreshes its cached copy). fan_only posts don't federate.
    359   if (finalStatus === 'published' && !fanOnly) {
     359  // Mastodon refreshes its cached copy). fan_only → followers-only (option A).
     360  if (finalStatus === 'published') {
    360361    const apPost = {
    361362      id: post.id, slug: finalSlug, title: title || finalSlug,
    362363      content: cleanContent, cover_image_url: cover_image_url || null,
    363       published_at: publishedAt, created_at: post.created_at,
     364      published_at: publishedAt, created_at: post.created_at, fan_only: fanOnly,
    364365    };
    365366    if (post.status !== 'published') ActivityPubService.deliverCreate(site, apPost).catch(() => { /* best-effort */ });
     
    391392  }
    392393
    393   // ActivityPub: tell followers the post is gone (Delete + Tombstone), but only
    394   // if it was actually federated (published + not fan-only). Fire before the row
    395   // is removed — we still have post.id (= the Note id).
    396   if (post.status === 'published' && !post.fan_only) {
     394  // ActivityPub: tell followers the post is gone (Delete + Tombstone) if it was
     395  // federated (any published post now federates — fan_only goes followers-only).
     396  // Fire before the row is removed — we still have post.id (= the Note id).
     397  if (post.status === 'published') {
    397398    ActivityPubService.deliverDelete(site, post).catch(() => { /* best-effort */ });
    398399  }
  • src/services/ActivityPubService.js

    re390844 r80c36a1  
    240240    url: human,
    241241    published: new Date(post.published_at || post.created_at || Date.now()).toISOString(),
    242     to: [PUBLIC],
    243     cc: [`${aId}/followers`],
     242    // fan_only = "fans only" → followers-only visibility (delivered to your followers
     243    // but not addressed to Public, so Mastodon shows it only to them and can't boost it).
     244    to: post.fan_only ? [`${aId}/followers`] : [PUBLIC],
     245    cc: post.fan_only ? [] : [`${aId}/followers`],
    244246    tag: Array.isArray(post.tags) ? post.tags.map((t) => ({ type: 'Hashtag', name: '#' + String(t).replace(/\s+/g, '') })) : [],
    245247    replies: `${id}/replies`,
  • src/services/Scheduler.js

    re390844 r80c36a1  
    3232      // get a duplicate FTS row → duplicate search hits.
    3333      try { ftsDel.run(p.id); fts.run(HtmlSanitizerService.toPlainText(p.content || ''), p.title || '', p.username || '', p.id); } catch { /* FTS failure is non-fatal */ }
    34       // ActivityPub: federate the now-published post to followers.
    35       if (!p.fan_only) {
    36         try {
    37           const site = siteStmt.get(p.site_id);
    38           if (site) {
    39             ActivityPubService.deliverCreate(site, {
    40               id: p.id, slug: p.slug, title: p.title || p.slug,
    41               content: p.content, cover_image_url: p.cover_image_url || null,
    42               published_at: p.published_at || p.publish_at, created_at: p.created_at,
    43             }).catch(() => { /* best-effort */ });
    44           }
    45         } catch { /* non-fatal */ }
    46       }
     34      // ActivityPub: federate the now-published post to followers (fan_only → followers-only).
     35      try {
     36        const site = siteStmt.get(p.site_id);
     37        if (site) {
     38          ActivityPubService.deliverCreate(site, {
     39            id: p.id, slug: p.slug, title: p.title || p.slug,
     40            content: p.content, cover_image_url: p.cover_image_url || null,
     41            published_at: p.published_at || p.publish_at, created_at: p.created_at, fan_only: p.fan_only,
     42          }).catch(() => { /* best-effort */ });
     43        }
     44      } catch { /* non-fatal */ }
    4745    }
    4846    return due.length;
Note: See TracChangeset for help on using the changeset viewer.