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:
1 parent
4c88980374
commit
c572471b88
30 files changed
+2604
-67
No files matched your search
@@ -2,7 +2,9 @@
|
||||
|
||||
@* The reports moderators handle (/clientapi/moderator/reports): from someone here (never said who) or from another
|
||||
server, about an account and some of its posts, a violation with the server's rules it names (their texts as PrivaPub
|
||||
keeps them, a removed rule's too); resolved here. *@
|
||||
keeps them, a removed rule's too). "Resolve and…" resolves one with what the staff does about it (nothing more, the
|
||||
account silenced or suspended, the reported posts removed), a note for the staff and a warning for a persona here; the
|
||||
act comes first, so one that fails leaves the report open. A resolved report shows what was done and its note. *@
|
||||
<div class="flex flex-col gap-3 w-full" data-reports>
|
||||
<DToggle LabelText="@Localizer["Show the resolved ones"]" Value="resolved" ValueChanged="ShowResolved" ValueExpression="() => resolved" />
|
||||
|
||||
@@ -33,16 +35,54 @@
|
||||
<span class="flex-1"></span>
|
||||
@if (!report.IsResolved)
|
||||
{
|
||||
<DButton Size="DSize.Sm" Color="DColor.Success" Disabled="@(IsDefaultDisabled || busy.Contains(report.ID))" OnClick="() => Resolve(report)"
|
||||
data-report-resolve>
|
||||
<i class="ion-md-checkmark"></i><span>@Localizer["Resolve"]</span>
|
||||
</DButton>
|
||||
<DDropdown IsOpen="@(menuOpen == report.ID)" IsOpenChanged="open => menuOpen = open ? report.ID : null" Align="DDropdownAlign.End"
|
||||
CloseOnContentClick="true" SheetOnNarrow="true"
|
||||
TriggerClass="@(menuOpen == report.ID ? "btn btn-sm btn-success btn-active" : "btn btn-sm btn-success")"
|
||||
ContentClass="mt-2 flex flex-col gap-2 p-2 rounded-box w-64 max-md:w-full bg-base-100 neo-raised neo-xs" data-report-resolve>
|
||||
<Trigger>
|
||||
<i class="ion-md-checkmark" aria-hidden="true"></i><span>@Localizer["Resolve and…"]</span>
|
||||
</Trigger>
|
||||
<ChildContent>
|
||||
<DButton Size="DSize.Sm" class="justify-start" Disabled="IsDefaultDisabled" OnClick="() => AskResolve(report, ModerationActions.None)"
|
||||
data-report-resolve-action="none">
|
||||
<i class="ion-md-checkmark" aria-hidden="true"></i><span>@Localizer["Resolve only"]</span>
|
||||
</DButton>
|
||||
@if (report.TargetAccountId is { Length: > 0 })
|
||||
{
|
||||
<DButton Size="DSize.Sm" class="justify-start" Disabled="IsDefaultDisabled" OnClick="() => AskResolve(report, ModerationActions.Silence)"
|
||||
data-report-resolve-action="silence">
|
||||
<i class="ion-md-volume-off text-warning" aria-hidden="true"></i><span>@Localizer["Silence the account"]</span>
|
||||
</DButton>
|
||||
<DButton Size="DSize.Sm" class="justify-start" Disabled="IsDefaultDisabled" OnClick="() => AskResolve(report, ModerationActions.Suspend)"
|
||||
data-report-resolve-action="suspend">
|
||||
<i class="ion-md-remove-circle text-error" aria-hidden="true"></i><span>@Localizer["Suspend the account"]</span>
|
||||
</DButton>
|
||||
}
|
||||
@if (report.PostIds.Count > 0)
|
||||
{
|
||||
<DButton Size="DSize.Sm" class="justify-start" Disabled="IsDefaultDisabled" OnClick="() => AskResolve(report, ModerationActions.RemovePosts)"
|
||||
data-report-resolve-action="remove_posts">
|
||||
<i class="ion-md-trash text-error" aria-hidden="true"></i><span>@Localizer["Remove the reported posts"]</span>
|
||||
</DButton>
|
||||
}
|
||||
</ChildContent>
|
||||
</DDropdown>
|
||||
}
|
||||
else if (report.ResolvedAt is { } resolvedAt)
|
||||
{
|
||||
<span class="text-xs opacity-70">@Localizer["Resolved {0}", resolvedAt.GetPassedTime(Localizer)]</span>
|
||||
}
|
||||
</div>
|
||||
@if (report.IsResolved && report.Resolution is { Length: > 0 } resolution)
|
||||
{
|
||||
<div class="flex flex-col gap-1 text-sm rounded-lg neo-inset neo-2xs p-2" data-report-resolution="@resolution">
|
||||
<b>@ResolutionName(resolution)</b>
|
||||
@if (report.ResolutionNote is { Length: > 0 } resolutionNote)
|
||||
{
|
||||
<span class="text-xs opacity-80 whitespace-pre-line break-words" data-report-resolution-note>@resolutionNote</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<p class="text-sm">
|
||||
<b>@Localizer["About"]</b>
|
||||
@if (report.TargetAccountId is { Length: > 0 } targetId)
|
||||
@@ -104,12 +144,24 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (resolving is not null)
|
||||
{
|
||||
<ActDialog Question="@ResolveQuestion" Text="@ResolveText" ConfirmText="@ResolveConfirm" Danger="@(resolveAction != ModerationActions.None)"
|
||||
Act="@("resolve-" + resolveAction)" Staff="true" AskNote="true" AskWarning="@(resolveAction != ModerationActions.None)"
|
||||
WarningHint="@Localizer["Only a persona here is told; an account elsewhere is told nothing."]" OnConfirm="Resolve" OnClosed="CloseResolve" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Inject] AdminService AdminService { get; set; }
|
||||
[Inject] ModerationService ModerationService { get; set; }
|
||||
|
||||
List<ViewReport> reports = [];
|
||||
bool resolved;
|
||||
HashSet<string> busy = [];
|
||||
// the report whose "Resolve and…" menu is open
|
||||
string menuOpen;
|
||||
// the report being resolved and how, while its dialog asks
|
||||
ViewReport resolving;
|
||||
string resolveAction;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -147,18 +199,66 @@
|
||||
_ => Localizer["Other"]
|
||||
};
|
||||
|
||||
async Task Resolve(ViewReport report)
|
||||
string ResolutionName(string action) => action switch
|
||||
{
|
||||
busy.Add(report.ID);
|
||||
var result = await AdminService.Resolve(report.ID);
|
||||
busy.Remove(report.ID);
|
||||
if (result.IsInvalid)
|
||||
{
|
||||
Toast.AddResponseError(result);
|
||||
return;
|
||||
}
|
||||
ModerationActions.Silence => Localizer["The account was silenced."],
|
||||
ModerationActions.Suspend => Localizer["The account was suspended."],
|
||||
ModerationActions.RemovePosts => Localizer["The reported posts were removed."],
|
||||
_ => Localizer["Resolved without an act."]
|
||||
};
|
||||
|
||||
reports.Remove(report);
|
||||
string ResolveQuestion => resolveAction switch
|
||||
{
|
||||
ModerationActions.Silence => Localizer["Resolve it and silence the account?"],
|
||||
ModerationActions.Suspend => Localizer["Resolve it and suspend the account?"],
|
||||
ModerationActions.RemovePosts => Localizer["Resolve it and remove the reported posts?"],
|
||||
_ => Localizer["Resolve the report?"]
|
||||
};
|
||||
|
||||
string ResolveText => resolveAction switch
|
||||
{
|
||||
ModerationActions.Silence => Localizer["Its posts stay out of public timelines, hashtags, trends and search; its followers still read them."],
|
||||
ModerationActions.Suspend => Localizer["Nothing of it is shown here any more and it can't act; nothing new is told to other servers. It can be lifted later."],
|
||||
ModerationActions.RemovePosts => Localizer["A post written here is deleted as its author would delete it; a copy of one from elsewhere is hidden here."],
|
||||
_ => Localizer["Nothing is done to the account or its posts."]
|
||||
};
|
||||
|
||||
string ResolveConfirm => resolveAction switch
|
||||
{
|
||||
ModerationActions.Silence => Localizer["Silence"],
|
||||
ModerationActions.Suspend => Localizer["Suspend"],
|
||||
ModerationActions.RemovePosts => Localizer["Remove the posts"],
|
||||
_ => Localizer["Resolve"]
|
||||
};
|
||||
|
||||
void AskResolve(ViewReport report, string action)
|
||||
{
|
||||
menuOpen = null;
|
||||
resolving = report;
|
||||
resolveAction = action;
|
||||
}
|
||||
|
||||
void CloseResolve() => (resolving, resolveAction) = (null, null);
|
||||
|
||||
async Task<WebResult> Resolve(ActDialog.Answer answer)
|
||||
{
|
||||
var report = resolving;
|
||||
var action = resolveAction;
|
||||
var result = await ModerationService.ResolveReport(report.ID, new ResolveReportForm
|
||||
{
|
||||
Action = action,
|
||||
Note = answer.Note,
|
||||
Warning = action == ModerationActions.None ? null : answer.Warning
|
||||
});
|
||||
if (result.IsInvalid)
|
||||
return result;
|
||||
|
||||
// shown at once as PrivaPub now keeps it: resolved, with what was done and its note
|
||||
report.IsResolved = true;
|
||||
report.ResolvedAt = DateTime.UtcNow;
|
||||
report.Resolution = action;
|
||||
report.ResolutionNote = answer.Note;
|
||||
Toast.AddResponseSuccess(Localizer["Report resolved."]);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user