R8 the Mastodon API made real: featured tags, endorsements, notes, notify and languages, suggestions, the direct stream
Featured tags per persona (/api/v1/featured_tags, suggestions, tags/:name/feature, accounts' featured tags, max 10),
listed at /tattoos as Hashtags and sent as Add/Remove{Hashtag} to followers on the featured collection, as Mastodon
sends them; remote accounts' tags received and read from featuredTags. Endorsements (pin/endorse) and private notes
(PersonaNote) kept per persona and never federated; the relationship's notifying, languages, endorsed and note, with
follow's notify (status notifications) and languages (home filter). Reading preferences set on /clientapi. Suggestions
v1/v2 with dismissals and staff picks, never the persona or a sibling. conversation events on the direct stream.
translate, annual reports and the admin API answer. New collections backed up and removed with their persona.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
1 parent
d08fbabaa5
commit
87529bca49
42 files changed
+1505
-77
No files matched your search
@@ -31,8 +31,10 @@ Its defining idea: **one private login owns several public personas.**
|
||||
|
||||
Privacy features in the model:
|
||||
- location-ranged posts (`Post.Location`, `RangeKm`), local-only and never federated;
|
||||
- contacts a persona can share (`Avatar.SharedPersonalContacts`) and private notes (`RootUserNote`, `PersonalNote`)
|
||||
exist only as model classes, with no endpoint.
|
||||
- contacts a persona can share (`Avatar.SharedPersonalContacts`) and the root's notes (`RootUserNote`, `PersonalNote`)
|
||||
exist only as model classes, with no endpoint. A persona's private note on an account is `PersonaNote` (R8), its own:
|
||||
`RootUserNote` is keyed by the root and would show a note made as one persona to its siblings, so it is never used for
|
||||
it.
|
||||
|
||||
The intended client is the Mastodon client API (Tusky, Elk, Phanpy, Ivory), where each avatar logs in as its own
|
||||
account. The private `/clientapi` covers what Mastodon can't express. `thepra/decePubClient`
|
||||
@@ -57,7 +59,7 @@ actor-scoped route gets a name in the same spirit, agreed with the owner, and a
|
||||
| DM context | `/peasants/{name}/whispers/{id}` |
|
||||
| Quote permission (FEP-044f `QuoteAuthorization`) | `/peasants/{name}/parrot-licences/{id}` |
|
||||
| Featured (pinned posts) | `/peasants/{name}/trophies` |
|
||||
| Featured tags | `/peasants/{name}/tattoos` |
|
||||
| Featured tags (an `OrderedCollection` of `Hashtag`s, R8) | `/peasants/{name}/tattoos` |
|
||||
| Wall (FEP-400e) | `/peasants/{name}/graffiti` |
|
||||
| Followers synchronisation (FEP-8fcf) | `/peasants/{name}/groupies/roll-call` |
|
||||
| A group's members and moderators | `/peasants/{name}/flock`, `/peasants/{name}/wardens` |
|
||||
@@ -127,7 +129,8 @@ PrivaPub/ ASP.NET Core Web API, net10.0
|
||||
Instance/ InstanceSettings (the server's own settings in memory), MediaLimitsFromSettings
|
||||
Moderation/ ModerationLog (the staff's log, AdminAction), AccountModeration (silence, suspend, posts
|
||||
removed), Restrictions (who is held back, in memory), ModerationFlags (posts' flags again)
|
||||
Social/ FollowService (local in-process, remote Follow/Accept), Notifications, Announcements
|
||||
Social/ FollowService (local in-process, remote Follow/Accept), Notifications, Announcements,
|
||||
FeaturedTags (a persona's featured hashtags, sent and received), Suggestions (who to follow)
|
||||
Timelines/ Fanout (TimelineEntry rows, Mastodon's home rules), TimelineService
|
||||
Privacy/ VisibilityPolicy (IsPublic expression, CanSee)
|
||||
Relationships/ RelationshipService (blocks, mutes, account domain blocks; Hidden), ReportService
|
||||
@@ -143,7 +146,8 @@ PrivaPub/ ASP.NET Core Web API, net10.0
|
||||
Entities/ Mappers/ Mastodon entities; MastodonMapper (Account, Status), AccountSearch
|
||||
Controllers/ apps, instance, accounts, statuses, timelines, notifications, search, lists, filters,
|
||||
media, polls, reactions, reports, scheduled statuses, streaming, tags, trends, cdns,
|
||||
provenance, custom emojis; the stubs are `StubsController` in `SearchController.cs`
|
||||
provenance, custom emojis, featured tags, suggestions; the stubs are `StubsController`
|
||||
in `SearchController.cs`
|
||||
Web/Pages/ Razor: /@{user}, /@{user}/{id} (public posts only, strict CSP, noindex);
|
||||
OAuth/: /oauth/login (root password), /oauth/authorize (choose persona, consent)
|
||||
Services/ RootUsersService, GroupUsersService, PostsService, AppConfigurationService, RootRemoval,
|
||||
@@ -350,6 +354,81 @@ client's role route never does.
|
||||
here and elsewhere, and the posts taken down, before it does; indexes on `SilencedAt` and `SuspendedAt` of both account
|
||||
collections and a partial one on `Post.ModeratedAt`. No new collection, so backups hold it all as they did.
|
||||
|
||||
## The Mastodon API made real (completion program R8)
|
||||
|
||||
- **Every row is one persona's** (`Models/Social/Featuring.cs`, new collections): `FeaturedTag`, `Endorsement`,
|
||||
`PersonaNote` and `SuggestionDismissal` are keyed by the persona (`AvatarId`), never by its root, with unique indexes on
|
||||
(persona, name or account); `RemoteFeaturedTag` is an account elsewhere's (by `ActorURI`). `Following` gained `Notify`
|
||||
and `Languages` (null: every language), `ForeignAvatar` `FeaturedTagsURL`, `AvatarSettings` `ExpandMedia`
|
||||
(`default`, `show_all`, `hide_all`) and `ExpandSpoilers`, `NotificationType` `Status` (last, as enum members are stored
|
||||
as ints).
|
||||
- **Featured tags** (`Domain/Social/FeaturedTags.cs`, `FeaturedTagsController`, `TagsController`): `GET`/`POST
|
||||
/api/v1/featured_tags` (`name`; 422 for a name that is no hashtag or past `FeaturedTags.MaxFeatured`, 10, which both
|
||||
instance APIs advertise as `max_featured_tags`; one featured already is answered as it is), `DELETE
|
||||
/api/v1/featured_tags/{id}` (`{}`, 404), `GET /api/v1/featured_tags/suggestions` (Mastodon's `Tag`s: the tags of the
|
||||
persona's last 200 posts it does not feature, the most used first, ten), `POST /api/v1/tags/{name}/feature|unfeature`
|
||||
(the `Tag`, with Mastodon 4.4's `featuring`, which every `Tag` now carries for a signed-in persona), `GET
|
||||
/api/v1/accounts/{id}/featured_tags` (anonymous too: a persona's own, an account elsewhere's from `RemoteFeaturedTag`, a
|
||||
community none, a suspended account 404). Mastodon's `FeaturedTag`: `statuses_count` a string and `last_status_at` a
|
||||
day, counted from the account's public and unlisted posts held here with the tag. They are public, so they federate:
|
||||
`/tattoos` lists them as `Hashtag`s, and featuring one sends `Add{Hashtag}`, unfeaturing `Remove{Hashtag}`, to the
|
||||
persona's followers, **with its `featured` collection (`/trophies`) as the target**, as Mastodon sends them
|
||||
(`AddHashtagSerializer`) and the only target Mastodon takes a Hashtag from. Received: `FeaturedHandler` takes a `Hashtag`
|
||||
added to or removed from an account's `featured` or `featuredTags` (an object with no id, read before anything takes its
|
||||
`href` for a post), and `AccountCountsJob` reads the `featuredTags` collection (its own server only, three pages, 20 tags
|
||||
at most) with the account's counts.
|
||||
- **Endorsements** (`AccountsController`): `POST /api/v1/accounts/{id}/pin|unpin` (Mastodon's old names) and
|
||||
`/endorse|/unendorse` (4.4) answer the relationship; only an account the persona follows (422 otherwise, as Mastodon
|
||||
says it), 500 at most. `GET /api/v1/endorsements` lists the persona's own, newest first, those it still follows (paged
|
||||
by `Link`); `GET /api/v1/accounts/{id}/endorsements` answers them to the persona itself and `[]` for every other account:
|
||||
**endorsements stay private** (the owner's default), never federated as featured accounts. Unfollowing drops the
|
||||
endorsement, as on Mastodon; a `Move` carries the endorsement and the note over to the new account.
|
||||
- **Private notes:** `POST /api/v1/accounts/{id}/note` (`comment`, 2000 characters at most, 422 beyond; empty takes it
|
||||
away) answers the relationship, whose `note` is the persona's own `PersonaNote`. Never federated.
|
||||
- **The relationship** fills `notifying` and `languages` from the follow, `endorsed` (only while it follows) and `note`.
|
||||
`POST /api/v1/accounts/{id}/follow` takes `notify` and `languages[]` (each changed only when given, as on Mastodon; an
|
||||
empty list is every language). `Fanout` keeps a post in another language out of that follower's home (a post whose
|
||||
language is unknown comes in; languages compare by their primary subtag) and gives a follower with `notify` a `status`
|
||||
notification for the account's own posts and its replies to itself, never its boosts, once the post reached that home
|
||||
(Mastodon's `FeedInsertWorker`). Archives carry both in `following_accounts.csv`'s own columns.
|
||||
- **Preferences:** `GET /api/v1/preferences` answers `reading:expand:media` and `reading:expand:spoilers` from the
|
||||
persona's settings, which Mastodon's API sets nowhere, so `GET`/`POST /clientapi/avatar/private/preferences`
|
||||
(`ViewPersonaPreferences`, `PersonaPreferencesForm`, the login's own persona; 404 for any other, 422 for another mode)
|
||||
sets them.
|
||||
- **Suggestions** (`Domain/Social/Suggestions.cs`, `SuggestionsController`): `GET /api/v1/suggestions` (accounts) and
|
||||
`/api/v2/suggestions` (`source`, `sources`, `account`), `limit` 40 (80 at most); `DELETE /api/v1/suggestions/{account_id}`
|
||||
(`{}`) dismisses one for that persona for good. In order: the staff's picks (`ServerSettings.StaffPicks`, `staff` /
|
||||
`featured`), past interactions (accounts the persona favourited, boosted or answered lately, `past_interactions`, which
|
||||
4.3's `sources` has no word for), then global: accounts followed by at least three personas here (`most_followed`) and
|
||||
personas here whose public posts of the last 30 days were favourited and boosted most (`most_interactions`),
|
||||
discoverable ones only, worked out at most every ten minutes. **Never the persona itself and never a sibling persona of
|
||||
its login** (`RootToAvatar` read server-side only: suggesting a sibling would pair them the moment the persona followed
|
||||
it), nor what it follows, asked to follow, blocked, muted, was blocked by (here or elsewhere), dismissed or hides by
|
||||
server, nor an account held back (`Counted.HeldBackPersonas`), suspended, silenced or shown `limited`; a community only
|
||||
as a staff pick.
|
||||
- **Staff picks** (`InstanceAdminController`, administrators, checked against the database): `GET
|
||||
/clientapi/admin/staff-picks`, `POST /staff-picks/insert` (`StaffPickForm`: a persona here or a person elsewhere by its
|
||||
Mastodon id; 404 for no such account, 409 for one picked already, 422 past 40), `POST /staff-picks/delete?accountId=`;
|
||||
each answers `List<ViewStaffPick>`, which `ViewServerSettings.StaffPicks` shows too (`IsShown` false for one deleted,
|
||||
suspended or silenced since). They are kept in the settings but never changed by the settings form, so an older page
|
||||
saving the form keeps them. Logged as `staff_pick.insert` and `staff_pick.delete` (target `persona` or `account`, so an
|
||||
entry about a persona of the acting login keeps no actor).
|
||||
- **familiar_followers stays empty by design:** which accounts the persona follows also follow another would tell who
|
||||
follows whom among the personas here; every account answers `accounts: []`.
|
||||
- **The direct stream:** `StreamingController` takes `stream=direct` (and the WebSocket's `subscribe` to `direct`); a
|
||||
direct message saved here or arriving (`StatusService`, `CreateHandler`, after `ConversationStates.Posted`) tells the
|
||||
`direct` streams of the conversation's personas a `conversation` event (`Fanout.Conversation`), mapped per persona as
|
||||
`/api/v1/conversations` maps it.
|
||||
- **Stubs that answer:** `POST /api/v1/statuses/{id}/translate` 422 (translation is cut), `GET /api/v1/annual_reports`
|
||||
`{"annual_reports":[],"accounts":[],"statuses":[]}`, every verb under `/api/v1/admin/**` (and `GET`/`POST` under
|
||||
`/api/v2/admin/**`) 403 `{"error":"This action is not allowed"}`, signed in or not (moderation stays on `/clientapi`).
|
||||
- **Removal, backups, archives:** `PersonaRemoval` deletes the persona's featured tags, endorsements, notes and
|
||||
dismissals, and what other personas kept about it (their endorsements of it, notes on it, dismissals of it); the Delete
|
||||
of the actor tells other servers. Every new collection is in every backup (none is in `ServerBackup.Excluded`: they are
|
||||
personas' own data, the remote featured tags a cache like `RemoteFeatured`); a restore keeps them as the backup has them,
|
||||
and `ProtectiveMerge` deletes those of a persona deleted since. Mastodon's account archive has no featured tags, notes or
|
||||
endorsements, so the persona archive leaves them out.
|
||||
|
||||
## Custom emoji (completion program R7)
|
||||
|
||||
- **The server's own emoji** (`Models/Post/LocalEmoji.cs`, a collection of its own): a shortcode (`[a-zA-Z0-9_]{2,64}`,
|
||||
@@ -541,6 +620,10 @@ client's role route never does.
|
||||
(`ObjectShapes.Flairs`, both dialects). Every media URL in it goes
|
||||
through the proxy; only page links (`link.url`, an event's online link) point at the remote site, because following
|
||||
one is the reader's choice.
|
||||
8. **What a persona keeps through the API is that persona's** (R8): featured tags (public, federated), endorsements and
|
||||
private notes (shown to it alone, never federated), dismissed suggestions. Suggestions never offer the persona itself
|
||||
or a sibling persona of its login, and `familiar_followers` answers no one for every account, since it would tell who
|
||||
follows whom here.
|
||||
|
||||
## Media invariants
|
||||
|
||||
@@ -673,7 +756,9 @@ client's role route never does.
|
||||
- **An actor's `published` is `PublishedOn`**, a random whole day up to two weeks before creation, so personas made the
|
||||
same day do not share a date. New persona and group ids carry that same day (`GenerateNewID`), because local clients
|
||||
see ids. Never emit `CreatedAt`.
|
||||
- **Per-avatar state stays per avatar:** blocks, mutes, notifications, follows. Nothing may relate sibling avatars.
|
||||
- **Per-avatar state stays per avatar:** blocks, mutes, notifications, follows, and since R8 featured tags,
|
||||
endorsements, private notes (`PersonaNote`, never `RootUserNote`) and dismissed suggestions. Nothing may relate sibling
|
||||
avatars: suggestions never offer a sibling, and familiar followers stay empty.
|
||||
- **Blocks federate** (owner decision, 2026-10-01): a block is sent as `Block` from the blocking avatar, an unblock as
|
||||
`Undo{Block}`. Reports still leave as `Flag` from the server's own actors (the instance actor, or the reporter for
|
||||
communities on Lemmy, PieFed and Mbin), never from the reporting avatar.
|
||||
@@ -1061,6 +1146,14 @@ tools/pasture/run.sh down # removes e
|
||||
in the status's `emojis` on Mastodon, GoToSocial (by URI), Misskey (the note's `emojis`) and Akkoma; her display name
|
||||
shows it on Mastodon; her `EmojiReact` with it reaches Misskey and Akkoma as that emoji; deleted, it leaves the list,
|
||||
its id answers 410 and posts here carry it no more; the staff's log keeps the insert and the delete.
|
||||
- **Featured tags and the API made real (`scenarios/featured.sh`, needs mastodon; after emoji in batch A):** mastouser
|
||||
follows feat_alice, who features a tag: Mastodon's `featured_tags` of her account shows it (her `Add{Hashtag}`, the
|
||||
`/tattoos` collection serving it), unfeaturing takes it away there; mastouser features a tag, which feat_alice reads
|
||||
(following him) through `/api/v1/accounts/:id/featured_tags`; following mastouser with `notify=true` gives feat_alice a
|
||||
`status` notification for his next post; her SSE and WebSocket `direct` streams tell a `conversation` event for his
|
||||
DM; her endorsement of him and her note on him are hers alone (feat_bob, a sibling persona of the same login, sees
|
||||
neither, and `GET /api/v1/accounts/:id/endorsements` answers nobody else), and feat_bob is never suggested to her; the
|
||||
stubs (translate 422, annual reports, the admin API 403) answer.
|
||||
- **Mailpit (1.31.4, `peers/mailpit.sh`, as `mailpit.test`):** the pasture's mail server. `appsettings.Pasture.json`
|
||||
sends PrivaPub's mail to `pasture-mailpit:1025` (plain SMTP, any login accepted) and its recovery links to
|
||||
`https://decepub.test`. `scenarios/accounts.sh` reads what arrived through Mailpit's API: an address set with the
|
||||
|
||||
Reference in new issue
Block a user