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
160 lines
7.0 KiB
C#
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; }
|
|
}
|
|
}
|