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

+72 -3
View File
@@ -10,8 +10,10 @@
hide filter leaves it out, a warn filter folds it behind the filter's title, a blur filter blurs its media; "Filter this
post" puts it in one of them. Located posts have no Mastodon actions (PrivaPub keeps them off the Mastodon API); their
author deletes them through /clientapi. A threadiverse post takes a downvote; the persona's own public post is given who
may quote it, and a post quoting it can have its quote revoked; a notification is dismissed from its line. Nerd stats
are on the time, the author and its server's history, the place or the CDN, the media and the counts. *@
may quote it, and a post quoting it can have its quote revoked; a notification is dismissed from its line, and a
warning from the server's staff (moderation_warning) shows what they did and what they wrote. A moderator removes
another's post from its menu, with a note and a warning, asked first. Nerd stats are on the time, the author and its
server's history, the place or the CDN, the media and the counts. *@
@if (!IsFilteredOut)
{
<article class="flex gap-3 w-full rounded-xl neo-raised @(Highlighted ? "neo-sm" : "neo-2xs") @CssContainer"
@@ -60,6 +62,20 @@
</DButton>
}
</p>
@if (notification.ModerationWarning is { } warning)
{
<div class="flex flex-col gap-1 p-2 rounded-lg neo-inset neo-2xs text-sm" data-notification-warning="@warning.Action">
<b>@WarningActionText(warning.Action)</b>
@if (warning.Text is { Length: > 0 } warningText)
{
<p class="whitespace-pre-line break-words" data-notification-warning-text>@warningText</p>
}
@if (warning.StatusIds.Count > 0)
{
<p class="text-xs opacity-70">@Localizer["It is about {0} of your posts.", warning.StatusIds.Count]</p>
}
</div>
}
}
else if (Post.BoostedBy is { } booster)
{
@@ -434,6 +450,14 @@
<i aria-hidden="true" class="ion-md-remove-circle text-base text-error"></i>
<span>@Localizer["Block"]</span>
</DButton>
@if (CascadingState.IsModerator && !Post.Located)
{
<DButton Size="DSize.Sm" class="justify-start whitespace-nowrap" Disabled="IsDefaultDisabled || isActing"
OnClick="@(() => isModeratingPost = true)" data-moderate-remove-post>
<i aria-hidden="true" class="ion-md-trash text-base text-error"></i>
<span>@Localizer["Remove post (moderation)"]</span>
</DButton>
}
}
else
{
@@ -508,6 +532,14 @@
{
<ReportDialog Account="reported" Post="status" OnClosed="@(() => isReporting = false)" />
}
@if (isModeratingPost)
{
<ActDialog Question="@Localizer["Remove this post as a moderator?"]"
Text="@Localizer["A post written here is deleted as its author would delete it; a copy of one from elsewhere is hidden here."]"
ConfirmText="@Localizer["Remove the post"]" Danger="true" Act="remove-post" Staff="true" AskNote="true" AskWarning="true"
WarningHint="@Localizer["Only a persona here is told; an account elsewhere is told nothing."]" OnConfirm="RemoveAsModerator"
OnClosed="ModerationClosed" />
}
@if (showInteractedBy is { } boosts)
{
<InteractedByDialog StatusId="@status.Id" Boosts="boosts" OnClosed="@(() => showInteractedBy = null)" />
@@ -544,7 +576,7 @@
}
}
}
else if (Post.Notification?.Account is { } follower)
else if (Post.Notification is { Type: not "moderation_warning", Account: { } follower })
{
<p class="text-sm">
<a class="font-bold" href="@($"account/{Uri.EscapeDataString(follower.Id)}")">@follower.Name</a>
@@ -577,6 +609,7 @@
[Inject] TimelineService TimelineService { get; set; }
[Inject] InstanceService InstanceService { get; set; }
[Inject] ProvenanceService ProvenanceService { get; set; }
[Inject] ModerationService ModerationService { get; set; }
[Parameter, EditorRequired] public FeedPost Post { get; set; }
[Parameter] public EventCallback<FeedPost> OnDeleted { get; set; }
@@ -610,6 +643,9 @@
bool isReporting;
bool isFilteringPost;
bool isChoosingQuotePolicy;
bool isModeratingPost;
// the staff removed the post: the page takes it out once the dialog closes
bool removedByModerator;
// who liked it (false) or boosted it (true), when that list is open
bool? showInteractedBy;
List<StatusEdit> history = [];
@@ -1123,6 +1159,26 @@
await OnDeleted.InvokeAsync(Post);
}
// a moderator takes another's post down (/clientapi/moderator/posts/remove): deleted when it was written here, hidden
// here when it is a copy from elsewhere
async Task<WebResult> RemoveAsModerator(ActDialog.Answer answer)
{
var result = await ModerationService.RemovePost(new RemovePostForm { PostId = Status.Id, Note = answer.Note, Warning = answer.Warning });
if (result.IsInvalid)
return result;
removedByModerator = true;
Toast.AddResponseSuccess(Localizer["Post removed."]);
return result;
}
async Task ModerationClosed()
{
isModeratingPost = false;
if (removedByModerator)
await OnDeleted.InvokeAsync(Post);
}
// someone's post on the persona's wall: its owner deletes it there (PrivaPub tells the wall's readers, and Smithereen
// deletes the post)
bool IsOnOwnWall(Status status) => !IsOwn && status.Privapub?.Wall?.Id is { } owner && owner == CascadingState.PersonaId;
@@ -1293,9 +1349,21 @@
"poll" => "ion-md-podium",
"update" or "quoted_update" => "ion-md-create",
"quote" => "ion-md-quote",
"moderation_warning" => "ion-md-warning text-warning",
_ => "ion-md-notifications"
};
// what a warning from the server's staff says they did
string WarningActionText(string action) => action switch
{
"silence" => Localizer["This persona is silenced: its posts stay out of public timelines, hashtags, trends and search."],
"suspend" => Localizer["This persona was suspended."],
"delete_statuses" => Localizer["A post of yours was removed."],
"mark_statuses_as_sensitive" or "sensitive" => Localizer["Your media were marked as sensitive."],
"disable" => Localizer["This login was disabled."],
_ => Localizer["A warning from this server's staff."]
};
string NotificationText(Notification notification)
{
var name = notification.Account?.Name;
@@ -1320,6 +1388,7 @@
"update" => Localizer["{0} edited a post", name],
"quote" => Localizer["{0} quoted your post", name],
"pleroma:emoji_reaction" => Localizer["{0} reacted {1}", name, notification.Emoji],
"moderation_warning" => Localizer["This server's staff warned you"],
_ => Localizer["{0}: {1}", name, notification.Type]
};
}