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
This commit is contained in:
1 parent
70d1ffe9a2
commit
9f68ca3ed0
71 files changed
+2229
-219
No files matched your search
@@ -110,6 +110,8 @@ namespace PrivaPub.Federation.Controllers
|
||||
var items = new List<JsonNode>();
|
||||
foreach (var post in latest)
|
||||
{
|
||||
if (VisibilityPolicy.Barred(post))
|
||||
continue;
|
||||
if (local.Kind == LocalActorKind.Group)
|
||||
{
|
||||
items.Add(ActivityPubRenderer.Announce(local, post.ObjectURI, $"announce-{post.ID}", post.CreationDate));
|
||||
@@ -152,7 +154,8 @@ namespace PrivaPub.Federation.Controllers
|
||||
if (local is not { HasWall: true })
|
||||
return NotFound();
|
||||
var wall = local.Wall;
|
||||
System.Linq.Expressions.Expression<Func<PostEntity, bool>> onWall = p => !p.DeletedAt.HasValue && !p.AuthorGone && !p.IsLocalOnly
|
||||
System.Linq.Expressions.Expression<Func<PostEntity, bool>> onWall = p => !p.DeletedAt.HasValue && !p.AuthorGone && !p.AuthorSuspended
|
||||
&& !p.ModeratedAt.HasValue && !p.IsLocalOnly
|
||||
&& (p.Visibility == PostVisibility.Public || p.Visibility == PostVisibility.Unlisted)
|
||||
&& (p.WallURI == wall
|
||||
|| p.GroupUserId == local.Id && !p.IsFederatedCopy && p.ReblogOfPostId == null && p.AnsweringToPostId == null && p.InReplyToURI == null && p.GroupId == null);
|
||||
@@ -282,6 +285,7 @@ namespace PrivaPub.Federation.Controllers
|
||||
return NotFound();
|
||||
var replies = await _dbEntities.Posts.Match(p => p.AnsweringToPostId == post.ID && !p.IsLocalOnly && p.ReblogOfPostId == null)
|
||||
.Match(VisibilityPolicy.IsPublic).Sort(p => p.CreationDate, Order.Ascending).Limit(MaxThread).ExecuteAsync(token);
|
||||
replies.RemoveAll(VisibilityPolicy.Barred);
|
||||
return Activity(ActivityPubRenderer.OrderedCollection(local.PostUri(post.ID) + "/replies", replies.Count,
|
||||
replies.Select(r => (JsonNode)r.ObjectURI)));
|
||||
}
|
||||
@@ -299,7 +303,7 @@ namespace PrivaPub.Federation.Controllers
|
||||
{
|
||||
var next = await _dbEntities.Posts.Match(p => level.Contains(p.AnsweringToPostId) && !p.IsLocalOnly && p.ReblogOfPostId == null)
|
||||
.Match(VisibilityPolicy.IsPublic).Sort(p => p.CreationDate, Order.Ascending).Limit(MaxThread - thread.Count).ExecuteAsync(token);
|
||||
thread.AddRange(next);
|
||||
thread.AddRange(next.Where(p => !VisibilityPolicy.Barred(p)));
|
||||
level = next.Select(p => p.ID).ToList();
|
||||
}
|
||||
var collection = ActivityPubRenderer.OrderedCollection(root.ContextURI, thread.Count, thread.Select(p => (JsonNode)p.ObjectURI));
|
||||
@@ -541,6 +545,15 @@ namespace PrivaPub.Federation.Controllers
|
||||
context.Result = StatusCode(StatusCodes.Status401Unauthorized);
|
||||
return;
|
||||
}
|
||||
// a suspended persona (by itself or with its banned login) answers 404 for everything but its actor and its inbox:
|
||||
// nothing new is told of it, and lifting the suspension serves it all again (owner decision 2026-10-17)
|
||||
if (HttpMethods.IsGet(Request.Method) && context.RouteData.Values["actor"] is string name
|
||||
&& context.ActionDescriptor is Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor { ActionName: not nameof(GetActor) }
|
||||
&& await _localActors.FindByUserName(name, HttpContext.RequestAborted) is { Kind: LocalActorKind.Person, Suspended: true })
|
||||
{
|
||||
context.Result = NotFound();
|
||||
return;
|
||||
}
|
||||
await next();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user