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
76 lines
4.5 KiB
C#
76 lines
4.5 KiB
C#
using MongoDB.Bson;
|
|
using MongoDB.Driver;
|
|
|
|
namespace PrivaPub.Domain.Moderation
|
|
{
|
|
// Every post's AuthorSuspended and AuthorLimited worked out again from its author's and its server's moderation (R6),
|
|
// and every account's AccountState from when it was silenced or suspended: after a restore merged the moderation it keeps
|
|
// (ProtectiveMerge), and once by migration _020 for the servers silenced before posts carried the flags. Raw documents,
|
|
// since a restore runs before the entities are used. A banned login's personas are never flagged (Restrictions).
|
|
public static class ModerationFlags
|
|
{
|
|
public static async Task<long> Recompute(IMongoDatabase database, CancellationToken token)
|
|
{
|
|
var posts = database.GetCollection<BsonDocument>("Post");
|
|
var flagged = 0L;
|
|
await posts.UpdateManyAsync(new BsonDocument("AuthorSuspended", true), Builders<BsonDocument>.Update.Set("AuthorSuspended", false), cancellationToken: token);
|
|
await posts.UpdateManyAsync(new BsonDocument("AuthorLimited", true), Builders<BsonDocument>.Update.Set("AuthorLimited", false), cancellationToken: token);
|
|
|
|
var moderated = new BsonDocument("$or", new BsonArray
|
|
{
|
|
new BsonDocument("SuspendedAt", new BsonDocument("$ne", BsonNull.Value)),
|
|
new BsonDocument("SilencedAt", new BsonDocument("$ne", BsonNull.Value))
|
|
});
|
|
|
|
// personas here: their own posts and boosts
|
|
var avatars = database.GetCollection<BsonDocument>("Avatar");
|
|
await avatars.UpdateManyAsync(new BsonDocument("AccountState", new BsonDocument("$in", new BsonArray { 1, 2 })),
|
|
Builders<BsonDocument>.Update.Set("AccountState", 0), cancellationToken: token);
|
|
foreach (var avatar in await (await avatars.FindAsync(moderated, cancellationToken: token)).ToListAsync(token))
|
|
{
|
|
var (suspended, silenced) = (IsSet(avatar, "SuspendedAt"), IsSet(avatar, "SilencedAt"));
|
|
await avatars.UpdateOneAsync(new BsonDocument("_id", avatar["_id"]), Builders<BsonDocument>.Update.Set("AccountState", suspended ? 2 : 1),
|
|
cancellationToken: token);
|
|
flagged += (await posts.UpdateManyAsync(new BsonDocument { { "GroupUserId", avatar["_id"].ToString() }, { "IsFederatedCopy", false } },
|
|
Builders<BsonDocument>.Update.Set("AuthorSuspended", suspended).Set("AuthorLimited", silenced), cancellationToken: token)).ModifiedCount;
|
|
}
|
|
|
|
// accounts elsewhere: what they wrote and boosted (an account that deleted itself stays Deleted)
|
|
var foreign = database.GetCollection<BsonDocument>("ForeignAvatar");
|
|
await foreign.UpdateManyAsync(new BsonDocument("AccountState", new BsonDocument("$in", new BsonArray { 1, 2 })),
|
|
Builders<BsonDocument>.Update.Set("AccountState", 0), cancellationToken: token);
|
|
foreach (var account in await (await foreign.FindAsync(moderated, cancellationToken: token)).ToListAsync(token))
|
|
{
|
|
if (account.GetValue("ActorURI", BsonNull.Value) is not BsonString actor)
|
|
continue;
|
|
var (suspended, silenced) = (IsSet(account, "SuspendedAt"), IsSet(account, "SilencedAt"));
|
|
if (account.GetValue("AccountState", 0).ToInt32() != 4)
|
|
await foreign.UpdateOneAsync(new BsonDocument("_id", account["_id"]), Builders<BsonDocument>.Update.Set("AccountState", suspended ? 2 : 1),
|
|
cancellationToken: token);
|
|
var update = Builders<BsonDocument>.Update.Set("AuthorSuspended", suspended);
|
|
if (silenced)
|
|
update = update.Set("AuthorLimited", true);
|
|
flagged += (await posts.UpdateManyAsync(new BsonDocument { { "ActorURI", actor }, { "IsFederatedCopy", true } }, update, cancellationToken: token))
|
|
.ModifiedCount;
|
|
}
|
|
|
|
// servers silenced: every post of theirs, and of their subdomains
|
|
var blocks = database.GetCollection<BsonDocument>("DomainBlock");
|
|
foreach (var block in await (await blocks.FindAsync(new BsonDocument("Severity", 1), cancellationToken: token)).ToListAsync(token))
|
|
if (block.GetValue("Domain", BsonNull.Value) is BsonString domain && !string.IsNullOrWhiteSpace(domain.AsString))
|
|
flagged += (await posts.UpdateManyAsync(new BsonDocument
|
|
{
|
|
{ "ActorURI", OfDomain(domain.AsString) },
|
|
{ "IsFederatedCopy", true }
|
|
}, Builders<BsonDocument>.Update.Set("AuthorLimited", true), cancellationToken: token)).ModifiedCount;
|
|
return flagged;
|
|
}
|
|
|
|
// an actor's address on a server or one of its subdomains
|
|
public static BsonRegularExpression OfDomain(string domain) =>
|
|
new($"^https?://([^/]*\\.)?{System.Text.RegularExpressions.Regex.Escape(Federation.Moderation.DomainBlocks.Normalise(domain))}(:[0-9]+)?(/|$)", "i");
|
|
|
|
static bool IsSet(BsonDocument row, string field) => row.TryGetValue(field, out var value) && !value.IsBsonNull;
|
|
}
|
|
}
|