using MongoDB.Entities; using PrivaPub.Models.Federation; namespace PrivaPub.Federation.Moderation { public interface IDomainBlocks { DomainBlock Find(string host); bool IsSuspended(string host); Task Reload(CancellationToken token); } public class DomainBlocks : IDomainBlocks { static readonly TimeSpan Staleness = TimeSpan.FromMinutes(5); readonly ILogger _logger; IReadOnlyDictionary _blocks = new Dictionary(); DateTime _loadedAt = DateTime.MinValue; int _reloading; public DomainBlocks(ILogger logger) { _logger = logger; } public static string Normalise(string domain) => domain?.Trim().Trim('.').ToLowerInvariant(); public DomainBlock Find(string host) { RefreshIfStale(); host = Normalise(host); var blocks = _blocks; while (!string.IsNullOrEmpty(host)) { if (blocks.TryGetValue(host, out var block)) return block; var dot = host.IndexOf('.'); host = dot < 0 ? default : host[(dot + 1)..]; } return default; } public bool IsSuspended(string host) => Find(host)?.Severity == DomainBlockSeverity.Suspend; public async Task Reload(CancellationToken token) => Load(await DB.Default.Find().ExecuteAsync(token)); public void Load(IEnumerable blocks) { _blocks = blocks.Where(b => !string.IsNullOrEmpty(b.Domain)) .GroupBy(b => Normalise(b.Domain)) .ToDictionary(g => g.Key, g => g.First()); _loadedAt = DateTime.UtcNow; } 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) { _logger.LogWarning(ex, "Domain blocks could not be reloaded"); } finally { Interlocked.Exchange(ref _reloading, 0); } }); } } }