R6 moderation in decePub: users and personas, accounts, report actions, deletions, suspended personas

Administration's Users & personas (administrators): logins searched and opened into their personas and groups; roles,
bans, two-factor reset and deletion of a login; silence, suspension and deletion of a persona; a group deleted. Accounts
(moderators): local personas and remote accounts searched or listed as moderated, silenced and suspended with a staff
note and a warning. Reports resolve with an action. ActDialog asks for the note, the warning or the password and is the
act's confirmation. A login deletes its own persona and the groups it owns with its password. Suspended personas can't
be chosen; one refused a token mid-session moves the session to another, or to none, once. Warnings show in
notifications, limited accounts on their profile, and moderators remove a post from its card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-17 05:41:10 +02:00
1 parent 4c88980374
commit c572471b88
30 files changed
+2604 -67

No files matched your search

+67 -15
View File
@@ -160,8 +160,15 @@ Below `md` the app is laid out for a phone, and a touch screen gets thumb-sized
- No history entries are pushed.
- **What can't be undone asks first** through `Services/ConfirmService.cs` (`Ask`, shown by `ConfirmHost` in
`MainLayout`): deleting a post, taking it off a wall, muting, blocking, removing a follower, revoking a quote, leaving a
group, dropping a scheduled post, signing out, and for an administrator deleting an announcement and removing the
server's thumbnail.
group, dropping a scheduled post, signing out, and for an administrator deleting an announcement, removing the
server's thumbnail, and changing a login's role, banning or unbanning it, resetting its two-factor sign-in and deleting
it. An act that takes words or a password asks in `Components/ActDialog.razor` instead, which is its confirmation:
what it does, the fields, Cancel (`data-act-cancel`) and the act (`data-act-submit`); a failure (a wrong password) stays
in the dialog. The staff's (`Staff`: `data-moderate-confirm="{act}"`, a note `data-moderate-note`, a warning
`data-moderate-warning`, the administrator's own password `data-admin-password`) are silencing and suspending an account
and undoing it, removing a post, resolving a report, deleting a persona or a group; the login's own
(`data-own-confirm="{act}"`, its password `data-own-password`) are deleting one of its personas and a group a persona
owns.
- **On a touch screen:**
- a tap on a post opens its thread (`main.js`, `data-thread` on the card, its `data-thread-link`), and cards are
pinned to the globe with a mouse only;
@@ -207,9 +214,16 @@ all of them in every theme. When you add or change one:
- `PublicCacheData` (page settings: theme colours, dark/light/system theming, language), persisted in localStorage;
- the theme, applied through `AppStatusService.ApplyTheme` (`wwwroot/js/theme.js`);
- `IsOnline` / `IsOffline`, polled every 10 seconds;
- the session: `IsSignedIn`, `Personas` (the root's), `PersonaId` and `Persona` (its Mastodon `Account`), with
`LoadSession`, `SwitchPersona` and `SignOut`. A persona change re-renders every subscriber; pages compare
`PersonaId` and reload what belongs to the persona. The JWT is renewed hourly when needed;
- the session: `IsSignedIn`, `Personas` (the root's), `PersonaId` and `Persona` (its Mastodon `Account`), `IsModerator`
and `IsAdmin` (the JWT's policies), with `LoadSession`, `SwitchPersona`, `PersonaDeleted` and `SignOut`. A persona
change re-renders every subscriber; pages compare `PersonaId` and reload what belongs to the persona. The JWT is renewed
hourly when needed;
- a persona the staff suspended (`ViewAvatar.AccountState` `Suspended` in `/clientapi/avatar/private/list`, `CanUse`) is
never acted as: `LoadSession` skips it, `SwitchPersona` refuses it, and the switcher and Login's chooser show it
disabled (`data-persona-state`). When PrivaPub refuses the current persona a token while the JWT is good (suspended
meanwhile, or gone), `HttpService` marks it refused (`TokenAuthStateProvider.Refuse`; no request acts as it again, each
answered `FailureCodes.PersonaRefused` without asking PrivaPub, until it is picked anew or the session is read again)
and `CascadingState` reads the personas again and moves to another usable one, or to none, saying why once;
- `ProcessError` / `ProcessWarning`, which log through `ILoggingService` and show a toast;
- `LogFromJs`, which JS calls back into through a `DotNetObjectReference`.
@@ -300,7 +314,10 @@ Never redeclare the base's members in a component.
following them, trends, the directory), `PortabilityService` (Mastodon's CSV exports and imports), `AdminService`
(`/clientapi/moderator`, the staff's log among it, and `/clientapi/admin`'s blocks, statistics and backups),
`InstanceAdminService` (the server's own settings, thumbnail, announcements and relays: PrivaPub's
`InstanceAdminController`), `AboutService` (what the server says of itself to anyone: the instance API, its documents
`InstanceAdminController`), `ModerationService` (the staff's acts: accounts found, silenced, suspended and given back,
posts removed and reports resolved on `/clientapi/moderator`; logins read with their personas and groups, their role,
ban, two-factor reset and deletion, and a persona's or a group's deletion with the administrator's password on
`/clientapi/admin`), `AboutService` (what the server says of itself to anyone: the instance API, its documents
and published domain blocks, never with credentials), `AnnouncementService` (`/api/v1/announcements`: dismissing and
reacting), `MediaService` (uploads, and deleting those no post holds), `GroupService`, `InstanceService` and
`ProvenanceService`.
@@ -334,7 +351,8 @@ Never redeclare the base's members in a component.
A profile shows the posts the account pins above its posts (`pinned=true`: what an account elsewhere features); the
persona pins and unpins its own public and unlisted posts from the card's menu.
An account that moved says where to (`Account.Moved`, which PrivaPub fills once the new account names the old one),
linking the new account's profile.
linking the new account's profile. One the staff silenced, or whose server is silenced, says Limited
(`Account.Limited`, `data-account-limited`).
Another's profile and posts can be reported (`Components/ReportDialog.razor`, `/api/v1/reports`): a category, words,
and for a remote account a copy to its server, which PrivaPub sends from its own actor, never naming the persona. A
violation (`ReportCategory.Violation`, offered while the server has rules) ticks the server's rules it breaks
@@ -352,8 +370,9 @@ Never redeclare the base's members in a component.
A card in a fixed-height flex column with `overflow-hidden` needs `flex-none`, or it collapses to nothing.
- **Groups** (`Pages/Groups.razor` at `/groups`): the persona's communities and circles (`GroupService`, `/clientapi/group`):
made (a circle unless it is a community, with an invitation code and an optional password), joined by invitation and
left; the ones it runs edited, their invitation shown, the requests to join let in or declined, members taken out.
Only a group's owner and moderators see who is in it.
left; the ones it runs edited, their invitation shown, the requests to join let in or declined, members taken out; the
ones it owns deleted with the login's password (`data-group-delete-own`, `ActDialog`; `/clientapi/group/delete`,
`DeleteGroupForm`: its members are told, its name stays taken). Only a group's owner and moderators see who is in it.
- **Lists** (`Pages/Lists.razor` at `/lists`): made, renamed, given a replies policy and made exclusive (their members
leave the home feed) there, filled from the accounts the persona follows; a followed account's profile has a Lists
menu too. A list is read as the feed's Lists tab (`FeedFilter.ListId`). Lists never leave PrivaPub.
@@ -415,7 +434,11 @@ Never redeclare the base's members in a component.
password and a code from the app or a recovery code (`/totp/disable`)); every session of it ended ("Sign out
everywhere", see **One sign-in**); or the whole of it
deleted, every persona with it, through `/clientapi/user/update/password` and `/clientapi/user/delete`; the client then
signs out), the persona's profile (`ProfileEditor`:
signs out), its personas (`PersonasEditor`: each used, shown silenced or suspended (`data-persona-state`; a suspended
one can't be used), and deleted with the login's password (`data-persona-delete-own`, `ActDialog`;
`/clientapi/avatar/private/delete`, `DeletePersonaForm`), the groups it owns with it; `CascadingState.PersonaDeleted`
forgets its token and, when it was the current one, acts as another, or as none, and a new one is made below), the
persona's profile (`ProfileEditor`:
`update_credentials` as multipart, its words as written from `Account.Source`, pictures (streamed from the file,
and removed with `DELETE /api/v1/profile/avatar|header`), fields, locked, bot,
discoverable, indexable and posting defaults; the composer starts a new post at its default visibility), its mutes,
@@ -430,9 +453,29 @@ Never redeclare the base's members in a component.
`data-admin-section`, and its panel loads when the page opens. Moderators see Reports (`reports`) and the Moderation
log (`log`); administrators also see Instance (`instance`), Rules (`rules`), Announcements (`announcements`),
Blocked servers (`blocks`), Relays (`relays`), Media limits (`media`), Statistics (`statistics`) and Backups
(`backups`). PrivaPub checks the role on every call (moderators: `/clientapi/moderator`, administrators:
`/clientapi/admin`, checked against its database too). Users & personas come with R6, custom emoji with R7: until
then they have no section.
(`backups`). Moderators also see Accounts (`accounts`), administrators Users & personas (`users`). PrivaPub checks the
role on every call (moderators: `/clientapi/moderator`, administrators: `/clientapi/admin`, checked against its database
too). Custom emoji come with R7: until then they have no section.
- **Users & personas** (`UsersPanel`, `data-users`, `/clientapi/admin/roots`; the one place a login is shown with its
personas): newest first, 50 at a time (`data-users-more`), found by a login's or a persona's name (`data-users-search`;
`data-users-reload`). Each login (`data-user="{login name}"`, `data-user-role` user|moderator|admin,
`data-user-banned` true|false) shows its badges (`data-user-you`, `data-user-banned-badge`, `data-user-totp`,
`data-user-email` none|verified|unverified) and, unless it is the administrator's own, its role
(`data-user-role-select`), `data-user-ban` or `data-user-unban`, `data-user-totp-reset` (with two-factor on) and
`data-user-delete`, each asked first (ConfirmService; a role PrivaPub refuses, one's own or the last administrator's,
is a 409 and the select shows the role again). `data-user-open` shows its personas (`data-user-personas`): each
(`data-persona="{userName}"`, `data-persona-state` normal|silenced|suspended) with `data-persona-silence` or
`-unsilence`, `-suspend` or `-unsuspend` and `data-persona-delete` (the administrator's password), and the groups it
owns (`data-group="{userName}"`, `data-group-delete`, the password). An act on the administrator's own persona moves
the session with it (`CascadingState.PersonaDeleted`, `ReloadPersonas`).
- **Accounts** (`AccountsPanel`, `data-accounts`, `/clientapi/moderator/accounts`): accounts here and elsewhere found by a
name, `user@host` or an address (`data-accounts-search`, results in `data-accounts-results`), and the ones silenced or
suspended, most recently first (`data-accounts-moderated`, 50 at a time, `data-accounts-more`). Each
(`data-moderated-account="{handle}"`, `data-account-state` normal|silenced|suspended, `data-account-id`) shows what the
staff did (`data-account-silenced`, `-suspended`, its server's block `data-account-domain-silenced`, `-suspended`, the
staff's note `data-account-note`) and `data-account-silence` or `-unsilence`, `-suspend` or `-unsuspend`, asked in
`ActDialog` with a note and, for a persona here and a silence or suspension, a warning it is told (Mastodon's
`moderation_warning`; an account elsewhere is told nothing).
- **The settings save whole.** `POST /clientapi/admin/settings` replaces everything `ServerSettingsForm` carries (a rule
left out is removed), so Instance, Rules and Media limits each save through `InstanceAdminService.SaveSettings`: it
reads the settings PrivaPub has now, builds the form from them (`FormOf`: the rules not deleted, in order, the media
@@ -475,7 +518,14 @@ Never redeclare the base's members in a component.
(`data-log-by`: you, a login name to administrators, `cli`, or none), what, to what (`data-log-target`) and its note
(`data-log-note`). `data-log-reload`.
- **Reports** (`ReportsPanel`): a violation's rules (`data-report-rules`, `data-report-rule-id`) as PrivaPub keeps their
texts, a removed rule's too.
texts, a removed rule's too. "Resolve and…" (`data-report-resolve`, a `DDropdown SheetOnNarrow`) resolves it with
`data-report-resolve-action` none, silence, suspend (with an account) or remove_posts (with posts), asked in
`ActDialog` with a note and a warning (`ResolveReportForm`; the act comes first, so one that fails leaves the report
open). A resolved report shows what was done and its note (`data-report-resolution="{action}"`,
`data-report-resolution-note`).
- **A post from the feed:** a moderator removes another's post from its card's menu (`data-moderate-remove-post`,
`ActDialog`, `/clientapi/moderator/posts/remove`): one written here is deleted as its author would, and its author
warned; a copy of one from elsewhere is hidden here. The page takes the card out.
- **Statistics** (`Components/Admin/StatisticsPanel.razor`, `/clientapi/admin/statistics`, typed in ClientModels'
`Admin/ViewStatistics.cs`): the period's overview, the servers (by traffic, failures, latency or name), the crawler, the
ledger's events and the server's own days. Counted per server, never per account.
@@ -499,7 +549,9 @@ Never redeclare the base's members in a component.
what notifications from people the persona does not follow, people who do not follow it, new accounts, private
mentions it did not ask for and limited accounts become (accept, filter, drop; `/api/v2/notifications/policy`), and the
filtered ones, one request per account, let in (with that account from then on) or dismissed. The Notifications tab
links there while requests wait. A notification is dismissed from its line (`data-notification-dismiss`,
links there while requests wait. A warning from the staff (`moderation_warning`, `Notification.ModerationWarning`) shows
what they did and what they wrote (`data-notification-warning="{action}"`, `data-notification-warning-text`). A
notification is dismissed from its line (`data-notification-dismiss`,
`TimelineService.DismissNotification`: `/api/v2/notifications/:group_key/dismiss`, its whole group, `ungrouped-{id}` for
one alone), and the page takes that item out (`Content.OnDismissed`).
- **Scheduled posts** (`Pages/Scheduled.razor` at `/scheduled`): the composer's clock button gives a Mastodon API post a