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

+31 -4
View File
@@ -4,10 +4,11 @@
<Title>@Localizer["Administration"]</Title>
@* Moderators handle reports and read the staff's log. Administrators also say what the server is (Instance), set its
rules, write its announcements, block servers for everyone, choose its relays and media limits, read the federation
statistics, and back up and restore the server. PrivaPub checks the role on every call; the sections only follow it.
Each section is a collapse marked data-admin-section; its panel loads when the page opens. *@
@* Moderators handle reports, moderate accounts here and elsewhere (Accounts) and read the staff's log. Administrators
also change the logins and their personas (Users & personas), say what the server is (Instance), set its rules, write
its announcements, block servers for everyone, choose its relays and media limits, read the federation statistics, and
back up and restore the server. PrivaPub checks the role on every call; the sections only follow it. Each section is
a collapse marked data-admin-section; its panel loads when the page opens. *@
<section class="block relative w-full h-full neo-inset neo-xs rounded-xl">
<div class="flex flex-col gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto">
@@ -25,6 +26,19 @@
</ChildContent>
</DCollapse>
<DCollapse Icon="DCollapseIcon.Arrow" data-admin-section="accounts">
<Header>
<p class="inline-flex items-center gap-2">
<i class="ion-md-contacts text-xl"></i> <span>@Localizer["Accounts"]</span>
</p>
</Header>
<ChildContent>
<div class="block w-full p-3 md:p-4">
<AccountsPanel />
</div>
</ChildContent>
</DCollapse>
<DCollapse Icon="DCollapseIcon.Arrow" data-admin-section="log">
<Header>
<p class="inline-flex items-center gap-2">
@@ -39,6 +53,19 @@
</DCollapse>
<AuthorizeView Policy="@Policies.IsAdmin">
<DCollapse Icon="DCollapseIcon.Arrow" data-admin-section="users">
<Header>
<p class="inline-flex items-center gap-2">
<i class="ion-md-people text-xl"></i> <span>@Localizer["Users & personas"]</span>
</p>
</Header>
<ChildContent>
<div class="block w-full p-3 md:p-4">
<UsersPanel />
</div>
</ChildContent>
</DCollapse>
<DCollapse Icon="DCollapseIcon.Arrow" data-admin-section="instance">
<Header>
<p class="inline-flex items-center gap-2">
+36 -1
View File
@@ -4,7 +4,8 @@
<Title>@Localizer["Groups"]</Title>
@* The current persona's communities and circles (/clientapi/group): made, joined by invitation, and left; for the ones
it runs, edited, their invitation shown, the requests to join answered and members taken out. A circle's members are
it runs, edited, their invitation shown, the requests to join answered and members taken out; for the ones it owns,
deleted with the login's password, asked first (its members are told, its name stays taken). A circle's members are
never shown to anyone but its owner and moderators, here as on every server. *@
<section class="block relative w-full h-full neo-inset neo-xs rounded-xl">
<div class="flex flex-col gap-4 p-4 md:p-5 w-full h-full absolute overflow-y-auto">
@@ -179,6 +180,13 @@
</div>
}
</div>
<div class="flex flex-wrap items-center justify-end gap-2 pt-2 border-t border-base-300">
<span class="text-xs opacity-70 flex-1 min-w-48">@Localizer["Deleting the group tells its members, and its name stays taken."]</span>
<DButton Size="DSize.Sm" Disabled="@(IsDefaultDisabled || isSaving)" OnClick="() => deleting = group" data-group-delete-own>
<i class="ion-md-trash text-error" aria-hidden="true"></i><span>@Localizer["Delete the group"]</span>
</DButton>
</div>
}
</article>
}
@@ -186,6 +194,14 @@
</div>
</section>
@if (deleting is not null)
{
<ActDialog Question="@Localizer["Delete the group {0}?", deleting.Name ?? deleting.UserName]"
Text="@Localizer["Its members and followers are told, and its name stays taken. It cannot be undone."]"
ConfirmText="@Localizer["Delete for good"]" Danger="true" Act="group-delete" AskPassword="true" OnConfirm="Delete"
OnClosed="() => deleting = null" />
}
@code {
static readonly string[] PostingPolicies = ["followers", "anyone", "moderators"];
@@ -200,6 +216,8 @@
bool editDiscoverable;
bool editApproves;
ViewGroup open;
// the group whose deletion the dialog asks about
ViewGroup deleting;
string loadedPersonaId;
bool isSaving;
@@ -300,6 +318,23 @@
Toast.AddResponseSuccess(Localizer["You left {0}.", group.Name ?? group.UserName]);
}
async Task<WebResult> Delete(ActDialog.Answer answer)
{
var group = deleting;
var result = await GroupService.Delete(CascadingState.PersonaId, group.Id, answer.Password);
if (result.IsInvalid)
return result;
groups = [.. groups.Where(g => g.Id != group.Id)];
if (open?.Id == group.Id)
{
open = null;
members = [];
}
Toast.AddResponseSuccess(Localizer["{0} is deleted.", group.Name ?? group.UserName]);
return result;
}
async Task Toggle(ViewGroup group)
{
if (open?.Id == group.Id)
+15 -2
View File
@@ -114,14 +114,27 @@
<div class="flex flex-col gap-4 w-full max-w-md mt-6 md:mt-16">
<h1 class="text-xl font-bold text-center">@Localizer["Who are you today?"]</h1>
<p class="text-sm opacity-70 text-center">@Localizer["Each persona has its own token; PrivaPub never tells anyone they share a login."]</p>
@if (CascadingState.Personas.All(p => !CascadingState.CanUse(p)))
{
<p class="text-sm text-center" data-personas-suspended>
@Localizer["Every persona of this login is suspended by this server's staff. Settings can still make a new one."]
</p>
<DButton Href="settings">@Localizer["Settings"]</DButton>
}
@foreach (var persona in CascadingState.Personas)
{
<button type="button" class="flex items-center gap-3 p-3 rounded-xl text-left neo-raised neo-xs hover:neo-sm transition-shadow @(persona.Id == CascadingState.PersonaId ? "ring-2 ring-primary" : null)"
disabled="@(IsDefaultDisabled || isWorking)" @onclick="() => Choose(persona.Id)">
@* a persona the staff suspended can't be chosen until that is lifted *@
<button type="button" class="flex items-center gap-3 p-3 rounded-xl text-left neo-raised neo-xs hover:neo-sm transition-shadow disabled:opacity-60 @(persona.Id == CascadingState.PersonaId ? "ring-2 ring-primary" : null)"
disabled="@(IsDefaultDisabled || isWorking || !CascadingState.CanUse(persona))" @onclick="() => Choose(persona.Id)"
data-persona-choice="@persona.UserName" data-persona-state="@(CascadingState.CanUse(persona) ? "normal" : "suspended")">
<img alt="" class="h-12 w-12 flex-none object-cover rounded-full neo-raised neo-2xs" src="@(persona.PictureURL ?? "imgs/icon-192.png")" />
<span class="flex flex-col min-w-0">
<b class="truncate">@(persona.Name ?? persona.UserName)</b>
<span class="text-xs opacity-70 truncate">@(persona.Handle ?? "@" + persona.UserName)</span>
@if (!CascadingState.CanUse(persona))
{
<span class="text-xs text-error">@Localizer["Suspended by this server's staff: it can't act until that is lifted."]</span>
}
</span>
@if (persona.Id == CascadingState.PersonaId)
{
+7
View File
@@ -126,6 +126,13 @@
{
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft"><i class="ion-md-people"></i> @Localizer["Groups"]</DBadge>
}
@if (account.Limited == true)
{
<DBadge Size="DSize.Sm" Variant="DBadgeVariant.Soft" Color="DColor.Warning" data-account-limited
title="@Localizer["This server's staff limited it: its posts stay out of public timelines, trends and search."]">
<i class="ion-md-volume-off"></i> @Localizer["Limited"]
</DBadge>
}
</div>
@if (!string.IsNullOrWhiteSpace(account.Note))