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 Recompute(IMongoDatabase database, CancellationToken token) { var posts = database.GetCollection("Post"); var flagged = 0L; await posts.UpdateManyAsync(new BsonDocument("AuthorSuspended", true), Builders.Update.Set("AuthorSuspended", false), cancellationToken: token); await posts.UpdateManyAsync(new BsonDocument("AuthorLimited", true), Builders.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("Avatar"); await avatars.UpdateManyAsync(new BsonDocument("AccountState", new BsonDocument("$in", new BsonArray { 1, 2 })), Builders.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.Update.Set("AccountState", suspended ? 2 : 1), cancellationToken: token); flagged += (await posts.UpdateManyAsync(new BsonDocument { { "GroupUserId", avatar["_id"].ToString() }, { "IsFederatedCopy", false } }, Builders.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("ForeignAvatar"); await foreign.UpdateManyAsync(new BsonDocument("AccountState", new BsonDocument("$in", new BsonArray { 1, 2 })), Builders.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.Update.Set("AccountState", suspended ? 2 : 1), cancellationToken: token); var update = Builders.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("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.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; } }