An ObjectId carries its creation second and a per-process counter, so two
avatars made one after the other by the same login got ids a few counts
apart: a link between personas that every Mastodon client would have
seen. Avatar and Group now generate ids from the UTC day plus eight random
bytes (still valid ObjectIds), and a remote post's id is made from its
published time with a random tail, so posts page in the order they were
written rather than the order they arrived.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
Post gains what federation and the Mastodon API need: Visibility (Public,
Unlisted, FollowersOnly, Direct, Circle, LocalGeo), the author's account
id, to/cc, the Create's id, url, context, quote, InReplyToURI and the
parent's author, a separate SpoilerText next to the title, language,
mentions, hashtags, remote attachments (alt text, blurhash, focus, size),
reply/favourite/reblog counters, revisions, EditedAt and DeletedAt.
Direct messages are Posts with Visibility Direct and a ConversationId;
migration _005 copies DmPost rows across with their ids and fills the new
fields of existing posts. DmPost is left in place so a rollback still sees
the old messages.
Inbound:
- NoteParser reads Note, Article, Page, Question and media types: content,
then contentMap, then _misskey_content; summary as the spoiler and name
as the title; Mention and Hashtag tags; attachments; a PeerTube-style
list attribution prefers the person over the channel; quote URIs.
- Addressing classifies like Mastodon, finding followers-only by the
author's own followers URL (now stored on ForeignAvatar), not by a
"/followers" suffix.
- Create keeps a post when a local persona is addressed or mentioned, when
it replies to a local post (the parent's reply count goes up) or when a
community it follows is addressed; an unsolicited public post is not
stored. Update keeps the previous version as a revision.
The outbox and object endpoints serve only Public and Unlisted posts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
Infrastructure/Data/Indexes runs at every start, after the migrations:
unique on Post/DmPost ObjectURI, ForeignAvatar ActorURI, the Follower
triple, RootToAvatar, RootUser UserName and ReservedName; plain indexes on
the lookups the services actually make (PublicKeyId, author and group
post listings, ParticipantsKey, the delivery queue).
Migration _001 runs first and removes the duplicates the races could
already have left (keeping the newest actor row, the oldest post, the
accepted follower), then fills ReservedName from every avatar and group.
ReservedName is one username space for personas, groups and the instance:
a name is reserved by an insert the unique index arbitrates, before the
avatar or group is saved, so two simultaneous sign-ups cannot both get it.
A short list of names (admin, support, abuse, postmaster, ...) is never
available.
EntityMaps.Warm touches every entity's collection one at a time before
anything else runs. MongoDB.Entities maps the Entity base class on first
touch, and two types mapped at once throw "An item with the same key has
already been added" and stay broken for the life of the process; the
parallel test run hit it, and the delivery worker racing a request could
have too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
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