Commit Graph
15 Commits
Author SHA1 Message Date
thepraandClaude Opus 5.5 4f5df743ae M11: the opt-in crawler, PrivaPub-Stargazer, and /stargazing
Off by default (Statistics:Crawler:Enabled), as the owner decided. When it is on:
- CrawlPlan runs hourly, from StatisticsSchedule. It inserts the configured seeds and
  queues up to HostsPerHour servers whose last visit is older than RevisitDays, are not
  paused by the breaker and are not domain-blocked, spread across the hour.
- CrawlInstance visits one server at a time as
  "PrivaPub-Stargazer/<ref> (+<base>/stargazing)":
  - it reads robots.txt (RFC 9309: its own group first, then PrivaPub, then *; longest
    rule wins; a 4xx allows everything; a 5xx or no answer keeps it out);
  - it describes servers that only crawling ever found, through
    InstanceDescriber.Describe with robots.txt as the path filter;
  - it reads /api/v1/instance/peers through the new GetStringArray, which keeps what it
    read from the first 1 MB instead of refusing a large list;
  - it adds the names a server could ever be reached at as "crawled": DNS only,
    punycode, no addresses, ports or hidden services, and the reserved test names only on
    a test network. Never more than MaxNewHostsPerCrawl per visit or MaxHosts in all, and
    never over a touched server.
- It reads nothing but robots.txt, NodeInfo, the instance API and the peers list.
  IFederationHttp.GetText serves robots.txt, and HttpScope.Crawl carries the
  User-Agent.
- /stargazing explains all this and how to keep the crawler out, says whether it is on,
  and credits DB-IP. GET /clientapi/admin/statistics/crawler shows the frontier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
2026-10-03 12:19:29 +02:00
thepraandClaude Opus 5.5 c5e4934ba6 T5: OAuth and the client API over HTTP
96 integration tests through PrivaPubHost, the real pipeline end to end:
- OAuth: the token's subject is the persona, and neither the token response,
  verify_credentials nor the stored token entries name the root. A wrong password shows
  an error and sets no login cookie; a login without the antiforgery token is a 400; the
  return address never leaves the site; deny answers access_denied with no code; another
  root's persona re-renders the choice with no code; a banned root is sent back to the
  login and a code issued before the ban buys no token; force_login asks again; a code
  works once and its reuse revokes the token it bought; password and refresh_token
  grants are refused; a client_credentials token gets 401 on user routes; a read-only
  token gets 403 with a Mastodon error on POST /api/v1/statuses; follow covers
  read:follows; revoke works; the login and authorize pages send their CSP and no-store;
  the 11th /oauth/login from one address in a minute is a 429.
- Accounts: sign-up, duplicates in any case, invalid models answer 400 with a message,
  login and logout, recovery email, settings, password change, invitation sign-up and
  login (refusing a persona named after the login), recovery without an email, through
  an unreachable mail server, with a wrong and with a valid code, token refresh, the 11th
  sign-up from one address, expired, garbage and foreign-key JWTs.
- Personas: a rootId in the body is ignored, the username regex and reserved names hold,
  personas and groups share ReservedName, an update delivers Update{Person} to followers,
  PublishedOn and the id's day fall within two weeks before creation, the list holds only
  one's own personas, another root's persona cannot be updated.
- Groups: communities and circles are created, joining takes the code and the password,
  members leave and owners cannot, a remote follow request becomes a member only on
  approval, a circle never shows in lookup, account by id, v2 search or /@name, and its
  /flock and /wardens answer 404 unsigned and to non-members, 200 to a member's signed GET.
- Moderation (Exclusive, it suspends localhost): ban, unban and remove; non-admins get
  403; reports are listed without the reporter and resolved; domain blocks are inserted,
  listed and deleted, bad domains refused, and a suspended server's delivery is answered
  202 and kept nowhere; the data endpoints.
- AdminCommands: exit codes 0, 1 and 2 and the resulting policies.

Fixed:
- A banned or removed root kept using /clientapi with its JWT until it expired: only /api
  re-checked the root. JwtEvents.TokenValidated now loads the root and fails the request
  when it is banned or deleted, and takes the policy claims from the database, so a
  demoted admin loses admin at once (and a promoted one gains it).
- The 401 and 403 bodies JwtEvents writes were PascalCase while every other /clientapi
  answer is camelCase; they now use the web defaults.
- /clientapi/user/sniff/again (token refresh) answered an empty 200; it now answers a
  fresh JwtUser, like login.
- Password recovery answered SMTP reply codes as HTTP statuses (421, 454, 554, and 550 for
  an invalid address); a mail server failure is now 503 and an invalid address 400.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
2026-10-03 11:53:23 +02:00
thepraandClaude Opus 5.5 6ef7e2891a M10: the admin statistics API
Under /clientapi/admin/statistics, admin only (the root JWT's IsAdmin policy, like the domain
blocks; /api tokens are persona tokens):
- GET overview?days: totals per channel, inbound and outbound outcomes, the delivery
  success rate, delivery latency p50/p95 from the buckets, active and known servers,
  distinct accounts, and the ledger's written/dropped/failed counts;
- GET hosts?days&sort=volume|failures|latency|host&software&page&limit: per server
  traffic, refusals, failures, latency, accounts, software and delivery health;
- GET hosts/{host}?days: the server's description, its daily series and its last 100
  events;
- GET events?host&channel&outcome&reason&before&limit, and GET server?days for the
  ServerDay rows;
- POST rollups/{day} refolds a past day; POST hosts/{host}/describe describes a server
  again.

Days not yet rolled up, today included, are folded live from the events, so the numbers are
current. Answers that may carry locations carry the DB-IP attribution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
2026-10-03 11:35:40 +02:00
thepraandClaude Opus 5.5 c00865d805 Located posts: local only, rounded, matched against their own radius
A post given a latitude and longitude becomes LocalGeo: its position is
rounded to two decimals (about a kilometre) and stored as a 2dsphere
point, its radius clamped to 1-50 km, and it is local only - no Create,
no delivery, no outbox, never in the Mastodon API or on a profile page.
/clientapi/post/nearby takes the viewer's position for the query only
(it is neither stored nor logged), runs $geoNear and keeps posts within
each post's own radius, minus authors the viewer blocks or mutes. A
located post cannot be direct or in a group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 12:24:30 +02:00
thepraandClaude Opus 5.5 8f75317050 Blocks, mutes, bookmarks, pins and reports
- Blocks are per persona and never federate (a Block activity would tell
  the other server who blocked whom): the blocked account is removed as a
  follower, with a Reject{Follow} if it is remote, unfollowed, cleared from
  home and notifications, and refused with Reject if it follows again.
- Mutes (optionally timed, optionally sparing notifications) and per-
  persona domain blocks keep authors out of home timelines, notifications
  and every status list the API returns; the boosts of a hidden author's
  posts are hidden too.
- Bookmarks and pins (at most five, public or unlisted, own posts) with
  their Mastodon endpoints and flags; pinned posts are the actor's
  `featured` collection at /trophies, and `featuredTags` points at
  /tattoos.
- Reports: /api/v1/reports stores the report and, when forwarding to a
  remote account, sends Flag from the instance actor, so the reporting
  persona is never named to the other server. An inbound Flag about a local
  persona or its posts becomes a report; moderators list and resolve them
  under /clientapi/moderator/reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 12:13:57 +02:00
thepraandClaude Opus 5.5 e99b76dbd7 Home timelines, mention notifications, and threads that fetch their parents
Fanout writes a TimelineEntry for every persona a post should reach: the
author, local followers of a local author, local followers of a remote one,
the members of a direct conversation or a circle. Mastodon's home rules
apply when it is written: a reply shows only to followers of both sides
(or to the one replied to), a reblog only where reblogs are wanted. A
mention of a local persona becomes a Mention notification.

Inbound posts are now also kept when a persona follows their author.

RemotePosts holds what CreateHandler and backfill share: building a Post
from a note, and fetching a public parent the first time a reply to it
arrives, so its author is known (a reply to an unknown or non-public
parent stays out of home timelines). Each fetched ancestor queues a
FetchAncestors job for the next one, up to ten deep.

/clientapi/timeline/home and /clientapi/notifications (with
/notifications/read) page by max_id for the persona's own root only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 11:37:41 +02:00
thepraandClaude Opus 5.5 a76cc34557 Posts reach their audience, edits follow them, deletes leave a tombstone
OutboxPublisher works out who a post goes to, Mastodon's way: followers'
shared inboxes for public, unlisted and followers-only; every mentioned
remote account's own inbox; the recipients only for direct; the parent's
author for a reply; a community's followers for a post in it; nobody for
a circle. Creates, updates and deletes all use it.

- Local posts take a visibility (public, unlisted, followers-only) and a
  spoiler text next to the content-warning flag.
- /clientapi/post/update keeps the previous version as a revision,
  re-renders, and sends Update{Note} with `updated` to the same to/cc.
- Deleting is now soft: the content, title, spoiler, media and revisions
  are cleared, timeline entries removed, the parent's reply count goes
  down, Delete goes to the stored audience, and the object answers 410
  with a Tombstone instead of 404.
- Editing a persona's profile sends Update{Person} to its followers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 11:34:35 +02:00
thepraandClaude Opus 5.5 61f6ca0676 Personas can follow: locally at once, remotely by Follow and Accept
Following records what a local persona follows, local or remote, with
its state and the Follow activity's id. FollowService (behind
/clientapi/follow, /clientapi/unfollow and /clientapi/following):
- resolves @user, user@host or an actor URI;
- a local account is followed in-process: accepted unless it approves
  followers by hand, a Follower row on the other side, a community gains
  the persona as a member, and a Follow or FollowRequest notification;
- a remote account gets a Follow signed by the persona, at
  /grunts/follow-{id}, and stays Requested until an Accept;
- unfollowing deletes the rows, or sends Undo{Follow} to a remote account.

Inbound Accept and Reject are matched to the Follow by its id (or, for an
embedded Follow without one, by its actor), and only from the account that
was followed. A remote Follow now notifies the persona too.

Notification is the per-persona record P1.2 builds on, deduplicated by
type, persona, sender and post. TimelineEntry and Favourite are created
with their indexes for the next commits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 11:32:33 +02:00
thepraandClaude Opus 5.5 e6a362c0b8 Domain blocks: suspend, silence, reject media
DomainBlock (domain, severity, reject-media, public and private comment)
covers the domain and its subdomains. Admins manage them under
/clientapi/admin/domainblocks/{list,insert,delete}; the set is kept in
memory, reloaded on every change and at most five minutes stale.

A suspended domain is refused by FederationHttp.IsAllowed, so nothing is
fetched from it and no job delivers to it, and the inbox drops its
activities with a 202 before fetching any key. Reject-media strips the
attachments of posts from that domain. Silence is recorded for the
timelines and notifications that arrive in P1.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 11:25:05 +02:00
thepraandClaude Opus 5.5 d80cd42a0c Rate limits on accounts and inboxes, a database dump before every deploy
- Sign-up, login, the invitation flows and password recovery allow ten
  requests a minute per client address; the inboxes give each sending
  origin (the keyId's) a bucket of 300 that refills at 300 a minute, and
  answer 429 beyond it, which peers retry.
- deploy.yml dumps the PrivaPub database to /var/backups before it stops
  the service (the last seven are kept), and after the swap checks that
  Swagger answers 404 and that the shared inbox answers junk with 400 and
  an unsigned activity with 401.
- ActivityPubClient and PostBoost, never used, are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 11:02:21 +02:00
thepraandClaude Opus 5.5 0f85030744 No admin by username, no Swagger in production, no exception text to clients
S14 and the privacy items of P0:
- signing up as "admin" no longer grants admin; `PrivaPub admin promote
  <root>` (and `demote`) does, run on the box against the configured
  database;
- Swagger is served in Development only;
- every service and controller answers "Something went wrong." where it
  used to send ex.Message, and the SMTP warnings no longer log the
  recipient's address;
- sign-up and login no longer log the IP, User-Agent and root id together;
- invitation sign-up takes the persona's own AvatarUserName (and optional
  AvatarName) instead of naming the avatar after the private login, and
  refuses a persona username equal to the login's. Invitation login uses
  the named persona, creating it if it is new;
- recovery mail comes from "PrivaPub", not collAnon's support address name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 10:58:39 +02:00
thepraandClaude Opus 5.5 034b792801 Move the federation code under Federation/, namespaces only
Build / Build (push) Successful in 16s
Services/Federation and Controllers/ServerToServer become
Federation/{Actors,Signing,Inbox,Outbox,Rendering,Controllers}, the first step
of the roadmap's layout. No type, route or behaviour changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 10:39:41 +02:00
thepraandClaude Opus 5.5 075c22228a net10, the refactor finished, and federation that works
Build / Build (push) Successful in 28s
Deploy / privapub.thepra.dev (push) Successful in 19s
The tree had not compiled since its first commit: Group, DmGroup and
IGroupUsersService were referenced and never written, an IDE rename had
turned the user-settings DTO into the ViewAvatarServer enum, and the settings
were saved as an entity they no longer were.

Built now:
- Group (an ActivityPub Group actor with its own keys, members, invitation
  code and optional password) and DmGroup (a conversation), with
  /clientapi/group/{list,insert,update,join,leave,approve}.
- Posts and DMs: /clientapi/post/{list,insert,delete}, /clientapi/dm/{list,insert};
  DM recipients are local usernames or user@host handles resolved by WebFinger.
- Invitation sign-up and login against the group's invitation code, checking
  the password before any account is created.
- Federation: WebFinger, NodeInfo 2.0, actors at /peasants/{name} (Person,
  Group, and an Application instance actor) with SPKI keys, draft-cavage
  RSA-SHA256 HTTP signatures both ways, an inbox handling Follow (+Accept),
  Undo, Create, Delete and Update, an outbox, notes at /posts/{id}, and a
  persisted, retried, signed delivery queue. A post to a group is announced
  by the group to its followers (FEP-1b12).
- The unused, broken typed ActivityPub models are replaced by a renderer;
  NSign's HMAC setup, which could not federate, is gone.

Upgrade: net10.0, MongoDB.Entities 25.1 (instance DB API, Standard GUIDs),
Swashbuckle 10 / OpenApi 2, Serilog.AspNetCore 10, MailKit 4.18,
PasswordGenerator 3. The JWT keys are 64 bytes (IdentityModel 8 refuses
shorter for HS512). Production runs its own mongod on 127.0.0.1:27022, as
Sintopia's apps do, and deploys to privapub.thepra.dev from the build runner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
2026-10-01 08:05:56 +02:00
thepra 87c86d2a1c up 2023-03-09 01:32:27 +01:00
thepra 1e66851113 Saving 2023-02-19 00:43:43 +01:00