Files
SocialPub/PrivaPub.ClientModels/Admin/Moderation.cs
T
thepraandClaude Opus 5.5 9f68ca3ed0 R6 moderation: roles, silence and suspension, bans that write nothing on the personas, deletions, report actions
Roles (user, moderator, administrator) from the CLI and /clientapi/admin/roots/{id}/role, never one's own nor the last
administrator. Silence keeps an account's posts out of public timelines, hashtags, trends, search and relays while its
followers still get them; suspension refuses and revokes a local persona's tokens, answers 404 for its posts and
collections, holds back every delivery and tells other servers nothing new, its actor unchanged; a suspended remote
account's activities are dropped and nothing goes to its own inbox (a shared one still serves its server). A banned
login's personas are held back in memory alone (Restrictions), so nothing pairs them in stored data. Personas and groups
are deleted one at a time (PersonaRemoval, GroupRemoval), by their login or an administrator with the password. Reports
resolve with an action and a note; posts are taken down, a remote copy hidden. Warnings reach the persona as Mastodon's
moderation_warning. Every staff act goes to the staff's log. Migration _020 limits posts from servers already silenced;
a restore keeps the moderation made since the backup.

Verified: interop.sh moderation gts mastodon with SecureMode, 194 passed; restore.sh, 25 passed, now with a suspension,
a silence and a takedown made after the backup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
2026-10-17 05:23:45 +02:00

160 lines
7.0 KiB
C#

using PrivaPub.ClientModels.Resources;
using System.ComponentModel.DataAnnotations;
namespace PrivaPub.ClientModels.Admin
{
// The staff's acts on accounts and posts (completion program R6). Moderators silence and suspend personas here and
// accounts elsewhere, remove posts and resolve reports with one of those acts; administrators also change logins
// (roles, bans, deletions, two-factor sign-in). Every act is kept in the staff's log.
public static class ModerationActions
{
public const string None = "none";
public const string Silence = "silence";
public const string Unsilence = "unsilence";
public const string Suspend = "suspend";
public const string Unsuspend = "unsuspend";
public const string RemovePosts = "remove_posts";//a report's resolution only: the posts it names
public static bool IsAccountAction(string action) => action is Silence or Unsilence or Suspend or Unsuspend;
public static bool IsResolution(string action) => action is None or Silence or Suspend or RemovePosts;
}
// POST /clientapi/moderator/accounts/moderate: a persona here (its id) or an account elsewhere (its id here)
public class ModerateAccountForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(64, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string AccountId { get; set; }
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(16, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Action { get; set; }//ModerationActions: silence, unsilence, suspend, unsuspend
// the staff's own words: kept in the log and on the account, never shown to it
[StringLength(1000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Note { get; set; }
// what a persona here is told with its warning (Mastodon's moderation_warning); nothing reaches an account elsewhere
[StringLength(1000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Warning { get; set; }
}
// POST /clientapi/moderator/posts/remove: a post here is deleted as its author would (the deletion federates), a copy of
// a post elsewhere is taken down here only
public class RemovePostForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(64, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string PostId { get; set; }
[StringLength(1000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Note { get; set; }
[StringLength(1000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Warning { get; set; }
}
// POST /clientapi/moderator/reports/{id}/resolve: the body is optional; without one, or with action none, the report is
// only marked resolved
public class ResolveReportForm
{
[StringLength(16, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Action { get; set; } = ModerationActions.None;//none, silence, suspend, remove_posts
[StringLength(1000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Note { get; set; }
[StringLength(1000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Warning { get; set; }
}
// an account as moderators see it: a persona here (never its login) or an account elsewhere, and what the staff did to it
public class ViewModeratedAccount
{
public string Id { get; set; }
public bool IsLocal { get; set; } = false;
public string UserName { get; set; }
public string DisplayName { get; set; }
public string Handle { get; set; }//user@host
public string Url { get; set; }//its actor's id
public string Domain { get; set; }
public DateTime? SilencedAt { get; set; }
public DateTime? SuspendedAt { get; set; }
public bool DomainSilenced { get; set; } = false;//an account elsewhere whose server is silenced: limited without its own silence
public bool DomainSuspended { get; set; } = false;
public string Note { get; set; }//the staff's last note on it
}
// GET /clientapi/moderator/accounts?q= and /clientapi/moderator/accounts/moderated?offset=&limit=: Next is the offset of
// the next page, null on the last
public class ViewModeratedAccountsPage
{
public List<ViewModeratedAccount> Items { get; set; } = [];
public int? Next { get; set; }
}
// a login as administrators see it (GET /clientapi/admin/roots), with its personas: only administrators ever read a login
// next to its personas
public class ViewAdminRoot
{
public string Id { get; set; }
public string UserName { get; set; }
public string Role { get; set; }//user, moderator, admin
public bool IsBanned { get; set; } = false;
public bool HasEmail { get; set; } = false;
public bool IsEmailValidated { get; set; } = false;
public bool HasTotp { get; set; } = false;
public bool IsYou { get; set; } = false;
public DateTime CreatedAt { get; set; }
public List<ViewAdminPersona> Personas { get; set; } = [];
}
public class ViewAdminPersona
{
public string Id { get; set; }
public string UserName { get; set; }
public string DisplayName { get; set; }
public string Handle { get; set; }
public DateTime? SilencedAt { get; set; }
public DateTime? SuspendedAt { get; set; }
public List<ViewAdminGroup> Groups { get; set; } = [];//the groups it owns
}
public class ViewAdminGroup
{
public string Id { get; set; }
public string UserName { get; set; }
public string Name { get; set; }
public bool IsCommunity { get; set; } = false;
public int MembersCount { get; set; } = 0;
}
// newest first; Next is the `before` of the next page, null on the last
public class ViewAdminRootsPage
{
public List<ViewAdminRoot> Items { get; set; } = [];
public string Next { get; set; }
}
// POST /clientapi/admin/roots/{id}/role
public class RootRoleForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
StringLength(16, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Role { get; set; }//user, moderator, admin
}
// what can't be undone asks for the administrator's own password: deleting a persona or a group
public class AdminPasswordForm
{
[Required(ErrorMessageResourceName = "Required", ErrorMessageResourceType = typeof(ErrorsResource)),
Display(Name = "Password", ResourceType = typeof(FieldsNameResource))]
public string Password { get; set; }
[StringLength(1000, ErrorMessageResourceName = "MaxLengthString", ErrorMessageResourceType = typeof(ErrorsResource))]
public string Note { get; set; }
}
}