DomainBlock (domain, severity, reject-media, public and private comment)
covers the domain and its subdomains. Admins manage them under
/clientapi/admin/domainblocks/{list,insert,delete}; the set is kept in
memory, reloaded on every change and at most five minutes stale.
A suspended domain is refused by FederationHttp.IsAllowed, so nothing is
fetched from it and no job delivers to it, and the inbox drops its
activities with a 202 before fetching any key. Reject-media strips the
attachments of posts from that domain. Silence is recorded for the
timelines and notifications that arrive in P1.2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
78 lines
1.9 KiB
C#
78 lines
1.9 KiB
C#
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<DomainBlocks> _logger;
|
|
IReadOnlyDictionary<string, DomainBlock> _blocks = new Dictionary<string, DomainBlock>();
|
|
DateTime _loadedAt = DateTime.MinValue;
|
|
int _reloading;
|
|
|
|
public DomainBlocks(ILogger<DomainBlocks> 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)
|
|
{
|
|
var blocks = await DB.Default.Find<DomainBlock>().ExecuteAsync(token);
|
|
_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);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|