Changeset 4407c67 in Klonkt


Ignore:
Timestamp:
07/19/2026 03:53:49 PM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
2d66d66
Parents:
bf72108
git-author:
Robin <roboburr@…> (07/19/2026 03:53:33 PM)
git-committer:
Robin <roboburr@…> (07/19/2026 03:53:49 PM)
Message:

Feature: C2S owner can read own followers/following (klonkt-demo-6kc)

The followers and following collections stay count-only for the public
(privacy), but a request carrying a C2S bearer scoped to that site (the account
owner) now returns the real actor URIs, so a client (Shaer) can build a friends
list. This was the one gap keeping the Shaer app's orbit empty against a real
Klonkt (it worked against the shaer-daemon, which serves the full lists).

  • buildFollowers/buildFollowing take an optional items array: when present, orderedItems carries the URIs and totalItems reflects them; otherwise count-only as before.
  • The two GET routes verify a bearer (OAuth.verifyBearer) and, when it is scoped to the requested slug, return the full list from ap_followers.actor_uri / ap_following.actor_uri (status=accepted); everyone else gets count-only. A private site's owner can read it even when it is not publicly listed.

3 new builder tests (count-only vs owner items vs empty owner list); 83 green.
Live-verified: owner bearer -> real URIs (alice/bob) in orderedItems; no bearer
-> orderedItems empty with the count intact; a token for another slug does not
unlock it.

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

Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • CHANGELOG.de.md

    rbf72108 r4407c67  
    77
    88### Hinzugefügt
     9- **Der Kontoinhaber kann seine eigenen Follower und Gefolgten über C2S lesen.**
     10  Die `followers`- und `following`-Sammlungen bleiben für die Öffentlichkeit
     11  count-only (Datenschutz), aber eine Anfrage mit einem auf diese Seite
     12  begrenzten C2S-Bearer liefert jetzt die echten Actor-URIs, damit eine App
     13  (Shaer) eine Freundesliste bauen kann. Für anonyme Aufrufer ändert sich
     14  nichts.
    915- **App-Zugriff über OAuth 2.0 (ActivityPub Client-to-Server, Phase 1).** Klonkt
    1016  spricht jetzt den standardmäßigen AP-C2S-Auth-Handshake, damit native und
  • CHANGELOG.md

    rbf72108 r4407c67  
    77
    88### Added
     9- **The account owner can read their own followers and following over C2S.** The
     10  `followers` and `following` collections stay count-only for the public
     11  (privacy), but a request carrying a C2S bearer scoped to that site now returns
     12  the real actor URIs, so an app (Shaer) can build a friends list. Anonymous
     13  callers are unchanged.
    914- **App access via OAuth 2.0 (ActivityPub Client-to-Server, phase 1).** Klonkt
    1015  now speaks the standard AP C2S auth handshake so native and web clients (the
  • CHANGELOG.nl.md

    rbf72108 r4407c67  
    77
    88### Toegevoegd
     9- **De account-eigenaar kan zijn eigen followers en following lezen via C2S.** De
     10  `followers`- en `following`-collecties blijven count-only voor het publiek
     11  (privacy), maar een verzoek met een C2S-bearer die op die site scoped is geeft
     12  nu de echte actor-URI's terug, zodat een app (Shaer) een vriendenlijst kan
     13  bouwen. Voor anonieme bezoekers verandert er niets.
    914- **App-toegang via OAuth 2.0 (ActivityPub Client-to-Server, fase 1).** Klonkt
    1015  spreekt nu de standaard AP C2S-authenticatie, zodat native en web-apps (de
  • src/routes/activitypub.js

    rbf72108 r4407c67  
    8484});
    8585
    86 // ── Followers (count only) ────────────────────────────────────────
     86// ── Followers (count-only public, full for the owner) ─────────────
     87// A C2S bearer scoped to this site (the account owner) gets the real actor
     88// URIs so their own client can build a friends list; everyone else gets the
     89// count only (privacy).
    8790router.get('/ap/users/:slug/followers', (req, res) => {
    88   const site = publicSite(req.params.slug);
    89   if (!site) return res.status(404).end();
     91  const auth = OAuth.verifyBearer(req.headers.authorization);
     92  const owner = auth && auth.site.slug === req.params.slug;
     93  const site = owner ? auth.site : publicSite(req.params.slug);
     94  if (!site) return res.status(404).end();
     95  if (owner) {
     96    const items = db.prepare('SELECT actor_uri FROM ap_followers WHERE slug = ? ORDER BY created_at').all(site.slug).map((r) => r.actor_uri);
     97    return AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, items.length, items));
     98  }
    9099  const n = db.prepare('SELECT COUNT(*) n FROM ap_followers WHERE slug = ?').get(site.slug).n;
    91100  AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, n));
    92101});
    93102
    94 // ── Following (count only) ────────────────────────────────────────
     103// ── Following (count-only public, full for the owner) ─────────────
    95104router.get('/ap/users/:slug/following', (req, res) => {
    96   const site = publicSite(req.params.slug);
    97   if (!site) return res.status(404).end();
     105  const auth = OAuth.verifyBearer(req.headers.authorization);
     106  const owner = auth && auth.site.slug === req.params.slug;
     107  const site = owner ? auth.site : publicSite(req.params.slug);
     108  if (!site) return res.status(404).end();
     109  if (owner) {
     110    let items = [];
     111    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 */ }
     112    return AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, items.length, items));
     113  }
    98114  let n = 0;
    99115  try { n = db.prepare("SELECT COUNT(*) n FROM ap_following WHERE slug = ? AND status = 'accepted'").get(site.slug).n; } catch { /* table may not exist */ }
  • src/services/ActivityPubService.js

    rbf72108 r4407c67  
    542542}
    543543
    544 export function buildFollowers(base, site, count) {
     544// Public callers get a count-only collection (privacy). The authenticated
     545// account owner (a C2S bearer scoped to this site) gets the real actor URIs via
     546// `items`, so their own client can build a friends list.
     547export function buildFollowers(base, site, count, items = null) {
    545548  const id = `${actorId(base, site.slug)}/followers`;
    546549  return {
     
    548551    id,
    549552    type: 'OrderedCollection',
    550     totalItems: count || 0,
    551     orderedItems: [], // hidden for privacy; count only
     553    totalItems: items ? items.length : (count || 0),
     554    orderedItems: items || [], // count-only for the public; full for the owner
    552555  };
    553556}
     
    555558// The accounts this site follows — count only, mirroring buildFollowers. The spec lists
    556559// `following` as a standard actor property; Hubzilla/Friendica + crawlers expect it.
    557 export function buildFollowing(base, site, count) {
     560export function buildFollowing(base, site, count, items = null) {
    558561  const id = `${actorId(base, site.slug)}/following`;
    559562  return {
     
    561564    id,
    562565    type: 'OrderedCollection',
    563     totalItems: count || 0,
    564     orderedItems: [], // count only
     566    totalItems: items ? items.length : (count || 0),
     567    orderedItems: items || [], // count-only for the public; full for the owner
    565568  };
    566569}
Note: See TracChangeset for help on using the changeset viewer.