Changeset 81b2e1e in Klonkt


Ignore:
Timestamp:
07/20/2026 09:38:41 PM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
024f4f8
Parents:
0cea12b
git-author:
Robin <roboburr@…> (07/20/2026 09:38:40 PM)
git-committer:
Robin <roboburr@…> (07/20/2026 09:38:41 PM)
Message:

Feature: C2S posts honor to/cc visibility (shaer-60b wire)

A note POSTed over C2S now federates the way it is addressed instead
of always going loud-public. Public in to = public; Public in cc =
quiet public (unlisted: followers in to, Public in cc on the built
note); followers-only = friends, riding the existing fan_only pipeline
(followers-only delivery + web gating); mention-only addressing =
direct, stored followers-gated and kept local until mention addressing
lands in the client. No addressing at all keeps the legacy public
behavior. Visibility is stored in the new additive posts.ap_visibility
column.

Changed files:
src/services/ActivityPubService.js

  • c2sVisibility(object): addressing -> bucket
  • c2sCreatePost stores ap_visibility + fan_only mapping, skips federation for direct
  • buildNote: quiet -> to followers, Public in cc

src/config/database.js

  • additive column posts.ap_visibility

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

  • bucket mapping, quiet addressing, friends never Public

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

Files:
1 added
2 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r0cea12b r81b2e1e  
    440440  // Rich replies: JSON array [{url, mediaType, name}] → `attachment` on the Note.
    441441  ensureColumn('ap_outbox', 'attachments', 'TEXT');
     442  ensureColumn('posts', 'ap_visibility', 'TEXT');   // public|quiet|friends|direct (C2S addressing, shaer-60b)
    442443}
    443444
  • src/services/ActivityPubService.js

    r0cea12b r81b2e1e  
    429429    // fan_only = "fans only" → followers-only visibility (delivered to your followers
    430430    // but not addressed to Public, so Mastodon shows it only to them and can't boost it).
    431     to: post.fan_only ? [`${aId}/followers`] : [PUBLIC],
     431    to: (post.fan_only || post.ap_visibility === 'quiet') ? [`${aId}/followers`] : [PUBLIC],
    432432    // Mentioned actors (from inline @user@host links the caller resolved) are addressed in cc
    433433    // so Mastodon notifies them; empty unless the content was mention-linked (delivery time).
    434     cc: [...new Set([...(post.fan_only ? [] : [`${aId}/followers`]), ..._mentionCc])],
     434    cc: [...new Set([
     435      ...(post.ap_visibility === 'quiet' ? [PUBLIC] : []),          // quiet public: Public in cc, not to
     436      ...((post.fan_only || post.ap_visibility === 'quiet') ? [] : [`${aId}/followers`]),
     437      ..._mentionCc])],
    435438    tag: [...buildHashtagList(base, post.tags, body), ..._mentionTags],
    436439    replies: `${id}/replies`,
     
    18871890  const slug = 'n-' + postId.slice(0, 8);
    18881891  const now = new Date().toISOString();
    1889   db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, type, language, created_at, updated_at, published_at)
    1890               VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`)
    1891     .run(postId, site.id, slug, user.id, '', html, '', 'published', 'post', object.language || 'nl', now, now, now);
     1892  // Visibility from the note's addressing (shaer-60b): Public in `to` = loud
     1893  // public, Public in `cc` = quiet public (unlisted), followers-only = friends
     1894  // (rides the existing fan_only pipeline: followers-only AP delivery + web
     1895  // gating), neither = participants-only (kept local until mention addressing
     1896  // lands; still followers-gated on the web).
     1897  const vis = c2sVisibility(object);
     1898  const fanOnly = (vis === 'friends' || vis === 'direct') ? 1 : 0;
     1899  db.prepare(`INSERT INTO posts (id, site_id, slug, author_id, title, content, excerpt, status, type, language, fan_only, ap_visibility, created_at, updated_at, published_at)
     1900              VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
     1901    .run(postId, site.id, slug, user.id, '', html, '', 'published', 'post', object.language || 'nl', fanOnly, vis, now, now, now);
    18921902  try { db.prepare('UPDATE posts SET content_rendered = ? WHERE id = ?').run(bakePostContent(html), postId); } catch { /* render fallback covers it */ }
    18931903  bakePostContentWithMentions(html).then((h) => { try { db.prepare('UPDATE posts SET content_rendered = ? WHERE id = ?').run(h, postId); } catch { /* keep sync bake */ } }).catch(() => {});
    18941904  try { db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?,?,?,?)').run(HtmlSanitizerService.toPlainText(html), '', user.username || '', postId); } catch { /* FTS non-fatal */ }
    1895   deliverCreate(site, { id: postId, slug, title: '', content: html, published_at: now, created_at: now }).catch(() => { /* best-effort */ });
     1905  if (vis !== 'direct') {
     1906    deliverCreate(site, { id: postId, slug, title: '', content: html, published_at: now, created_at: now, fan_only: fanOnly, ap_visibility: vis }).catch(() => { /* best-effort */ });
     1907  }
    18961908  return { status: 201, id: postId, url: `${base}/ap/notes/${postId}` };
     1909}
     1910
     1911// Addressing → visibility. Arrays or bare strings; unknown shapes read as the
     1912// safest bucket they match.
     1913export function c2sVisibility(object) {
     1914  const arr = (v) => (Array.isArray(v) ? v : (v ? [v] : [])).filter((x) => typeof x === 'string');
     1915  const to = arr(object.to), cc = arr(object.cc);
     1916  const isPublic = (x) => x === PUBLIC || x === 'as:Public' || x === 'Public';
     1917  const isFollowers = (x) => /\/followers\/?$/.test(x);
     1918  if (to.some(isPublic)) return 'public';
     1919  if (cc.some(isPublic)) return 'quiet';
     1920  if (to.some(isFollowers) || cc.some(isFollowers)) return 'friends';
     1921  if (!to.length && !cc.length) return 'public';   // no addressing at all: legacy client, keep old behavior
     1922  return 'direct';
    18971923}
    18981924
     
    29042930  linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
    29052931  noteVisibility, isRejectedObject, rejectInteraction, interactionReportTarget,
    2906   getMessages, notificationsSeenAt, ingestOutboxActivity,
     2932  getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility,
    29072933};
Note: See TracChangeset for help on using the changeset viewer.