8 Commits
Author SHA1 Message Date
thepraandClaude Opus 5.5 6fccb6fe35 P7: downvotes go out, and feeds are read through the server's reader
Build / Build (push) Successful in 8m45s
Deploy / privapub.thepra.dev (push) Successful in 9m0s
Votes out. A persona's downvote is a Dislike (POST /api/v1/statuses/:id/downvote and /undownvote, the viewer's own as
privapub.votes.downvoted). One vote a post: a changed vote is sent as the new vote alone, as Lemmy sends one, since an
Undo of the old one beside it could arrive after it and take the new one away; Undo goes only when a vote is taken back.
A vote on a post in a remote community goes to the community, which counts it, and to the author only when on another
server: one copy a server, since PieFed drops a second copy of an activity it has just seen. Mbin keeps a favourite
apart from a vote, so there a favourite stays after a switch to a downvote.

Feeds followed (owner decision 2026-10-07: through the server). Following a feed (Lemmy's multi-community, PieFed's feed)
keeps a FeedSubscription for the persona and nothing else; the new Service privapub_feeds (LocalActorKind.Reader,
reserved by migration _017) follows every community of the feeds read here, reconciled when a persona follows or leaves
one, when a feed's list is read again (kept as it was when it cannot be read) and every six hours. Its Following rows
carry FollowerKind, so nobody's home gets what it brings in and its unanswered follows are sent again as its own. The
persona reads GET /api/v1/timelines/feed/:id (the feed's threads, ours included) and lists its feeds at GET /api/v1/feeds.

Checked in the pasture, every scenario: 873 pass. The 14 failures are Hubzilla's (identical on the previous commit:
Hubzilla no longer answers a follow in this pasture since its restore) and two activities Smithereen never sent;
followsync passes once the pasture's restore is older than the 14-day pause. Live: alice's downvotes count as downvotes
on Lemmy 1.0 and PieFed, replacing her upvote; Lemmy 1.0 and PieFed take privapub_feeds' follows and their threads reach
the feed timelines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-08 02:15:28 +02:00
thepraandClaude Opus 5.5 0695c08ac3 A remote thread brings the replies that never reached us
The context of a remote post showed only what PrivaPub happened to hold:
replies from servers nobody here follows were never seen, and only the
ancestors were ever fetched. Now a persona opening a public remote thread
queues FetchReplies for the post and its root, at most once an hour each.

The job reads the thread's own collection first (FEP-7888 `context`, which
Mastodon 4.5+ serves with every reply at any depth; posts or, as FEP-f228
allows, the activities that made them), and otherwise the post's `replies`
(PeerTube's `comments`) and the replies' own, two levels down. At most 5
pages and 100 posts a job, signed by the instance actor, never a persona;
each post is fetched from its own origin and stored through StoreContext,
so only public and unlisted ones are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-05 04:54:38 +02:00
thepraandClaude Opus 5.5 864dae4469 A boost reaches the author's server once
A boost and its undo went to the author's personal inbox and to the shared
inbox of its server's followers: two copies at once, which Misskey counted
twice as it processed them and undid once (seen in decePub's e2e boost on
Misskey). The author's server now gets them through its shared inbox, as
Mastodon sends them; a like still goes to the author's own inbox.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-05 02:57:33 +02:00
thepraandClaude Opus 5.5 b710493701 A boost or a favourite given again is sent again
Every boost of a post was announce-{post}, and every favourite like-{post}:
after an Undo, giving it again made the same activity id, which the
delivery queue had already delivered and dropped (and a server that
remembers ids would ignore). Found by decePub's e2e actions against the
pasture, which unboost and boost again. Now each boost and favourite is an
activity of its own, as Mastodon's are, and an Undo names the one it ends;
a concurrent second boost is removed after saving. An edit's Update is
named to the millisecond, a quote approval's to the tick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-05 02:48:35 +02:00
thepraandClaude Opus 5.5 d6131af289 A public reply names the author it answers in cc
A reply whose text had lost the @name of the author it answers (decePub
prefills it, a reader may delete it) was addressed to nobody on the
author's server. PrivaPub delivered it to the author's inbox anyway, but
GoToSocial keeps only what is addressed to someone there, so the reply
never appeared under the post. A public or unlisted reply to a remote
post now names its parent's author in cc, as Pleroma does; the reply
already says whom it answers, so nothing new is revealed. The parent
author's actor id is kept on the reply (InReplyToActorURI).

Found by decePub's end-to-end tests on the town.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-05 00:22:48 +02:00
thepraandClaude Opus 5.5 6b55b5c35c A status asked for by id is shown, and remote warnings are their own
GET /api/v1/statuses/:id applied the viewer's own blocks and mutes and
answered 200 with null for a post of an author the viewer had blocked;
decePub's thread page then showed nothing. As in Mastodon, a status asked
for by id (or acted on) is shown whatever the viewer blocked or muted,
and only lists leave such posts out; when nothing can be shown the
answer is 404, never null, and search leaves it out.

A remote post marked sensitive with an empty summary (Akkoma's sensitive
media, Lemmy's NSFW) got an invented "Content warning" that hid its
words. A remote post now keeps its own summary: sensitive without one
hides the media only. A local post warned without words still gets the
default warning.

Both found by decePub's end-to-end tests on the town.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-05 00:09:51 +02:00
thepraandClaude Opus 5.5 ab526ed291 T12, T13: Sharkey and Akkoma in the pasture, and two bugs Akkoma found
Build / Build (push) Successful in 4m51s
Deploy / privapub.thepra.dev (push) Successful in 5m2s
Sharkey 2025.4.7 is the Misskey peer under another name, image, database, Redis db and
home. Its scenario is Misskey's plus edits both ways and the FEP-e232 quote tag: 40 checks
pass.

Akkoma 3.20.1 publishes no image, so tools/pasture/images/akkoma installs its OTP
release, pinned by checksum, and appends Caddy's CA to the CA bundles the release ships.
Its scenario has 44 checks, which pass three runs in a row:
- follows, posts, CW, followers-only posts and the published time;
- replies, likes, boosts and their undos;
- EmojiReact both ways and withdrawn;
- DMs, polls, quotes, media, edits with history and deletes, both ways;
- unfollow, block, unblock and statistics.

The two bugs, both fixed:
- Followers-only posts arrived as DMs on Pleroma and Akkoma. They call a post private
  only if an address in `to` contains "/followers" or its cc is not empty. Ours is
  /groupies, and a post mentioning nobody had an empty cc. The followers collection is now
  named in cc as well, which tells nobody anything new.
- Akkoma's open polls showed as ended and refused votes. Akkoma carries an open poll's
  end in `closed` and sends no `endTime`. A `closed` in the future is now read as the end.

Neither of these is a PrivaPub bug:
- Akkoma's Linkify never takes @user@host.test for a mention, so its DM addresses alice
  with to[].
- Its API never reports a remote blocker as blocked_by, so the block is read from its
  database.

Also in this commit:
- The rate limits are configuration (RateLimits: AccountsPerMinute, InboxBurst,
  InboxPerTenSeconds), with the old values as defaults. The pasture raises the accounts
  limit, which back-to-back runs from one address had hit.
- A new Lemmy never sends what it queued for a server before it started that server's
  send worker, so the scenario waits for the worker before its first follow.

All six peers in one clean pass: GoToSocial 54 (+1 expected), Mastodon 49 (+2), Misskey
35, Sharkey 40, Akkoma 44, and Lemmy 20 (+3) once the worker wait was added. 642 tests
pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
2026-10-03 15:24:08 +02:00
thepraandClaude Opus 5.5 fc5bb9511f T6: the Mastodon API over HTTP
88 integration tests drive the Mastodon client API through the whole server
(PrivaPubHost), with remote actors on an in-process Peer and deliveries read
from the job queue. Helpers live in Support/Host/MastodonHelpers.cs.

Coverage:
- Accounts: verify_credentials (no root id or login name); update_credentials
  with indexed and array fields_attributes (form and JSON), source[*],
  quote_policy, locked/bot, avatar and header uploads resized and stripped of
  EXIF and XMP; lookup (local, @domain, remote; a circle, the instance actor
  and a circle's id answer 404); search with and without resolve (only a
  signed-in persona resolves, the Peer is untouched otherwise), by post and
  actor address, hashtags, undiscoverable personas; account statuses with
  pinned, exclude_replies, exclude_reblogs, only_media, tagged and Link paging
  both ways; followers-only posts for followers (local and remote authors);
  community accounts; followers/following only to their owner; follow (open,
  locked, remote Follow delivery), unfollow and Undo; follow requests from
  local and remote followers answered with the original Follow;
  remove_from_followers; blocks with Reject and Block/Undo deliveries; mutes
  with duration and the notifications choice, never federated; domain blocks;
  relationships with junk ids; a banned login's tokens; reports forwarded as a
  Flag from the instance actor only; account stub routes.
- Statuses: each visibility's to/cc as delivered; CW as summary; replies to
  local and remote posts (mention, inReplyTo, the author's inbox); polls and
  votes (local, and remote votes only to the author without published); media
  attached only by its owner; quotes, the quotes list and revocation; edit
  history, source and the Update delivery; delete for redraft, the 410
  Tombstone and the Delete delivery; favourite/reblog counts with Like,
  Announce and their Undos; favourited_by, reblogged_by; bookmarks; pins;
  interaction_policy matching canQuote in the note and in the Update;
  strangers get 404 for followers-only and direct posts; a located post is
  unreachable by id for anyone else on every route; statuses?id[];
  Idempotency-Key; scopes; deleting a reblog.
- Timelines: home paging with max_id, since_id and min_id; public local and
  remote; tag (anonymous); list stub; favourites; conversations and read;
  markers; notifications with types[], exclude_types[], account_id, paging,
  get, dismiss, clear and unread_count.
- Instance: v1 and v2 (4.2.0 (compatible; PrivaPub)), peers, activity, rules,
  extended_description, apps and every stub route.
- Media: v1 and v2 uploads, owner-only GET and PUT, 422 for unsupported or
  unreadable files, video and audio made with ffmpeg lavfi sources and checked
  with ffprobe; every remote media address goes through the proxy; the proxy
  refuses unsigned URLs, streams ranges as 206 without caching, caches whole
  downloads and serves them with ranges, and streams anything over
  Media:MaxProxiedBytes (a SmallProxyHost) without caching.
- Provenance of local, delivered (signature) and fetched (instance actor,
  no signature, the trigger as activity) posts, visibility of provenance,
  instance descriptions; reading any of them makes no outbound request.
  Pleroma reactions with EmojiReact and Undo deliveries, and local reaction
  notifications.

Bugs fixed:
- remove_from_followers deleted the Follower row but never told a remote
  follower. It now sends Reject{Follow} with the stored Follow id, through
  RelationshipService.RemoveFollower, which Block now shares.
- VisibilityPolicy.CanSee refused followers-only posts to accepted followers,
  so a post in their home timeline answered 404 to GET, context, favourite and
  reply. Followers of the author (local or remote) may now see them.
- Account statuses of a remote account hid followers-only posts from
  personas that follow it.
- exclude_replies dropped the author's own threads; like Mastodon it now
  drops only replies to other accounts.
- A community account's statuses were always empty: they are now the posts
  addressed to the community.
- Pinning someone else's visible post answered 404; it answers 422 like
  Mastodon.
- GET /api/v1/notifications/:id answered 200 with null when the notification's
  post was gone; it answers 404.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
2026-10-03 11:58:31 +02:00