Files
SocialPub/PrivaPub/Domain/Moderation/Restrictions.cs
T

111 lines
4.7 KiB
C#
Raw Normal View History

using MongoDB.Driver;
using MongoDB.Entities;
using PrivaPub.Domain.Privacy;
using PrivaPub.Models.User;
namespace PrivaPub.Domain.Moderation
{
// Who the staff's moderation holds back right now, in memory (completion program R6), for the places that decide one
// item at a time and must not ask the database each time: a token's persona, a delivery's signer and inbox, a post
// shown or not, a stream. The posts of a suspended or silenced account carry their own flags (Post.AuthorSuspended,
// AuthorLimited) for queries; a banned login's personas never do, since flagging all of them at once would pair them
// in stored data, so they are suspended here alone (Counted.HeldBackPersonas).
//
// Read at start, again after every act that changes it (AccountModeration, a ban, a removal) and when five minutes old;
// a process that never loaded it (the CLI) starts with nobody held back and loads it on first use.
public static class Restrictions
{
static readonly TimeSpan Staleness = TimeSpan.FromMinutes(5);
public sealed class Set
{
public static readonly Set Empty = new(new HashSet<string>(), new HashSet<string>(), new HashSet<string>(), new HashSet<string>());
public Set(IReadOnlySet<string> suspendedPersonas, IReadOnlySet<string> silencedPersonas, IReadOnlySet<string> suspendedActors,
IReadOnlySet<string> suspendedInboxes)
{
SuspendedPersonas = suspendedPersonas;
SilencedPersonas = silencedPersonas;
SuspendedActors = suspendedActors;
SuspendedInboxes = suspendedInboxes;
}
// suspended themselves, or held back with their login (banned or deleted)
public IReadOnlySet<string> SuspendedPersonas { get; }
public IReadOnlySet<string> SilencedPersonas { get; }
// accounts elsewhere the staff suspended: their actors' ids, and their own inboxes (never a shared one, which
// serves the rest of their server)
public IReadOnlySet<string> SuspendedActors { get; }
public IReadOnlySet<string> SuspendedInboxes { get; }
}
static volatile Set _current = Set.Empty;
static DateTime _loadedAt = DateTime.MinValue;
static int _reloading;
public static Set Current
{
get
{
RefreshIfStale();
return _current;
}
}
public static bool IsSuspended(string personaId) => !string.IsNullOrEmpty(personaId) && Current.SuspendedPersonas.Contains(personaId);
public static bool IsSilenced(string personaId) => !string.IsNullOrEmpty(personaId) && Current.SilencedPersonas.Contains(personaId);
public static bool IsSuspendedActor(string actorUri) => !string.IsNullOrEmpty(actorUri) && Current.SuspendedActors.Contains(actorUri);
public static bool IsSuspendedInbox(string inbox) => !string.IsNullOrEmpty(inbox) && Current.SuspendedInboxes.Contains(inbox);
public static async Task Reload(CancellationToken token = default)
{
var suspended = (await Counted.HeldBackPersonas(token)).ToHashSet(StringComparer.Ordinal);
var silenced = (await DB.Default.Find<Avatar>().Match(a => a.SilencedAt != null && a.DeletionAt == null)
.Project(a => a.Include(x => x.ID)).ExecuteAsync(token)).Select(a => a.ID).ToHashSet(StringComparer.Ordinal);
var actors = await DB.Default.Find<ForeignAvatar>().Match(f => f.SuspendedAt != null)
.Project(f => f.Include(x => x.ActorURI).Include(x => x.InboxURL).Include(x => x.SharedInboxURL)).ExecuteAsync(token);
// a single-user server's actor may name its server's shared inbox as its own: that one still serves the others there
var inboxes = actors.Where(a => !string.IsNullOrEmpty(a.InboxURL) && a.InboxURL != a.SharedInboxURL)
.Select(a => a.InboxURL).ToHashSet(StringComparer.Ordinal);
if (inboxes.Count > 0)
{
var candidates = inboxes.ToList();
var shared = await DB.Default.Find<ForeignAvatar>().Match(f => candidates.Contains(f.SharedInboxURL))
.Project(f => f.Include(x => x.SharedInboxURL)).ExecuteAsync(token);
inboxes.ExceptWith(shared.Select(f => f.SharedInboxURL));
}
_current = new Set(suspended, silenced,
actors.Select(a => a.ActorURI).Where(u => !string.IsNullOrEmpty(u)).ToHashSet(StringComparer.Ordinal),
inboxes);
_loadedAt = DateTime.UtcNow;
}
// what a reload says, without waiting for it where waiting is not possible
public static void ReloadSoon() => _loadedAt = DateTime.MinValue;
static void RefreshIfStale()
{
if (DateTime.UtcNow - _loadedAt < Staleness || Interlocked.Exchange(ref _reloading, 1) == 1)
return;
_ = Task.Run(async () =>
{
try
{
await Reload(CancellationToken.None);
}
catch (Exception ex)
{
Serilog.Log.ForContext(typeof(Restrictions)).Warning(ex, "The moderation's restrictions could not be reloaded");
}
finally
{
Interlocked.Exchange(ref _reloading, 0);
}
});
}
}
}