about summary refs log tree commit diff
path: root/MatrixAntiDmSpam
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-05-01 17:14:29 +0200
committerRory& <root@rory.gay>2025-05-01 17:14:29 +0200
commit03fb16cc050b94fdc608898db3025cf43a78f437 (patch)
tree4446a03b234f43f61e245a0d454d6d0dd875ca46 /MatrixAntiDmSpam
parentFix readme (diff)
downloadMatrixAntiDmSpam-03fb16cc050b94fdc608898db3025cf43a78f437.tar.xz
Move core logic to a separate library to facilitate embedding
Diffstat (limited to 'MatrixAntiDmSpam')
-rw-r--r--MatrixAntiDmSpam/AntiDmSpamConfiguration.cs15
-rw-r--r--MatrixAntiDmSpam/InviteHandler.cs91
-rw-r--r--MatrixAntiDmSpam/MatrixAntiDmSpam.csproj1
-rw-r--r--MatrixAntiDmSpam/PolicyExecutor.cs92
-rw-r--r--MatrixAntiDmSpam/PolicyListFetcher.cs89
-rw-r--r--MatrixAntiDmSpam/PolicyStore.cs24
-rw-r--r--MatrixAntiDmSpam/Program.cs2
7 files changed, 2 insertions, 312 deletions
diff --git a/MatrixAntiDmSpam/AntiDmSpamConfiguration.cs b/MatrixAntiDmSpam/AntiDmSpamConfiguration.cs
deleted file mode 100644

index fc99f57..0000000 --- a/MatrixAntiDmSpam/AntiDmSpamConfiguration.cs +++ /dev/null
@@ -1,15 +0,0 @@ -namespace MatrixAntiDmSpam; - -public class AntiDmSpamConfiguration { - public AntiDmSpamConfiguration(IConfiguration config) => config.GetRequiredSection("AntiDmSpam").Bind(this); - public string? LogRoom { get; set; } - public bool LogInviteDataAsFile { get; set; } - - public required List<PolicyRoomReference> PolicyLists { get; set; } - - public class PolicyRoomReference { - public required string Name { get; set; } - public required string RoomId { get; set; } - public required List<string> Vias { get; set; } - } -} \ No newline at end of file diff --git a/MatrixAntiDmSpam/InviteHandler.cs b/MatrixAntiDmSpam/InviteHandler.cs deleted file mode 100644
index 26a88c7..0000000 --- a/MatrixAntiDmSpam/InviteHandler.cs +++ /dev/null
@@ -1,91 +0,0 @@ -using System.Text.Json; -using ArcaneLibs; -using ArcaneLibs.Extensions; -using LibMatrix.Helpers; -using LibMatrix.RoomTypes; -using LibMatrix.Utilities.Bot.Interfaces; - -namespace MatrixAntiDmSpam; - -public class RoomInviteHandler(ILogger<RoomInviteHandler> logger, AntiDmSpamConfiguration config) : IRoomInviteHandler { - public List<RoomInviteContext> Invites { get; } = []; - - public List<Func<RoomInviteContext, Task>> OnInviteReceived { get; set; } = []; - - private GenericRoom? LogRoom { get; set; } - - public async Task HandleInviteAsync(RoomInviteContext invite) { - if (!string.IsNullOrWhiteSpace(config.LogRoom)) - LogRoom = invite.Homeserver.GetRoom(config.LogRoom); - - Invites.Add(invite); - await LogInvite(invite); - foreach (var handler in OnInviteReceived) { - try { - await handler.Invoke(invite); - } - catch (Exception e) { - if (!string.IsNullOrWhiteSpace(config.LogRoom)) { - var logRoom = invite.Homeserver.GetRoom(config.LogRoom); - await logRoom.SendMessageEventAsync( - new MessageBuilder() - .WithBody($"Failed to execute invite handler {handler}...").WithNewline() - .WithCollapsibleSection("Stack trace", msb => msb.WithCodeBlock(e.ToString(), "cs")) - .Build() - ); - } - } - } - } - - private async Task LogInvite(RoomInviteContext invite) { - logger.LogInformation("Received invite to {} from {}", invite.RoomId, invite.MemberEvent.Sender); - - if (LogRoom is null) return; - var inviteData = invite.InviteData.ToJsonUtf8Bytes(ignoreNull: true); - - var inviterNameTask = invite.TryGetInviterNameAsync(); - var roomNameTask = invite.TryGetRoomNameAsync(); - var inviteDataFileUriTask = invite.Homeserver.UploadFile(invite.RoomId + ".json", inviteData, "application/json"); - logger.LogInformation("Uploaded invite data ({}) to {}", Util.BytesToString(inviteData.Length), await inviteDataFileUriTask); - - await Task.WhenAll(inviterNameTask, roomNameTask, inviteDataFileUriTask); - - var message = new MessageBuilder() - .WithBody("Received invite to ").WithMention(invite.RoomId, await roomNameTask).WithBody(" from ").WithMention(invite.MemberEvent.Sender!, await inviterNameTask) - .Build(); - - message.AdditionalData!["gay.rory.invite_logger.invite_data_uri"] = JsonDocument.Parse($"\"{await inviteDataFileUriTask}\"").RootElement; - - await LogRoom.SendMessageEventAsync(message); - - if (config.LogInviteDataAsFile) { - await LogRoom.SendMessageEventAsync(new() { - MessageType = "m.file", - Body = invite.RoomId + ".json", - FileName = invite.RoomId + ".json", - Url = await inviteDataFileUriTask, - FileInfo = new() { Size = inviteData.Length, MimeType = "application/json" } - }); - } - } - - public async Task RejectInvite(RoomInviteContext invite, MessageBuilder reason) { - if (LogRoom is not null) - _ = LogRoom.SendMessageEventAsync(reason.Build()); - - try { - await invite.Homeserver.GetRoom(invite.RoomId).LeaveAsync(); - } - catch (Exception e) { - if (LogRoom is not null) - await LogRoom.SendMessageEventAsync( - new MessageBuilder().WithColoredBody("#FF8800", $"Failed to leave {invite.RoomId}:").WithNewline() - .WithCodeBlock(e.ToString(), "cs") - .Build() - ); - } - - Invites.Remove(invite); - } -} \ No newline at end of file diff --git a/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj b/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj
index 703a461..596d203 100644 --- a/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj +++ b/MatrixAntiDmSpam/MatrixAntiDmSpam.csproj
@@ -20,5 +20,6 @@ <ItemGroup> <ProjectReference Include="..\LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" /> + <ProjectReference Include="..\MatrixAntiDmSpam.Core\MatrixAntiDmSpam.Core.csproj" /> </ItemGroup> </Project> diff --git a/MatrixAntiDmSpam/PolicyExecutor.cs b/MatrixAntiDmSpam/PolicyExecutor.cs deleted file mode 100644
index 65e7f9f..0000000 --- a/MatrixAntiDmSpam/PolicyExecutor.cs +++ /dev/null
@@ -1,92 +0,0 @@ -using System.Diagnostics; -using ArcaneLibs.Attributes; -using ArcaneLibs.Extensions; -using LibMatrix.EventTypes.Spec.State.Policy; -using LibMatrix.Helpers; -using LibMatrix.Homeservers; -using LibMatrix.RoomTypes; -using LibMatrix.Utilities.Bot.Interfaces; - -namespace MatrixAntiDmSpam; - -public class PolicyExecutor( - ILogger<PolicyExecutor> logger, - AntiDmSpamConfiguration config, - RoomInviteHandler roomInviteHandler, - PolicyStore policyStore, - AuthenticatedHomeserverGeneric homeserver) : IHostedService { - private readonly GenericRoom? _logRoom = string.IsNullOrWhiteSpace(config.LogRoom) ? null : homeserver.GetRoom(config.LogRoom); - - public Task StartAsync(CancellationToken cancellationToken) { - roomInviteHandler.OnInviteReceived.Add(CheckPoliciesAgainstInvite); - policyStore.OnPolicyUpdated.Add(CheckPolicyAgainstOutstandingInvites); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) { - return Task.CompletedTask; - } - - private Task CheckPoliciesAgainstInvite(RoomInviteContext invite) { - logger.LogInformation("Checking policies against invite"); - var sw = Stopwatch.StartNew(); - - // Technically not required, but helps with scaling against millions of policies - Parallel.ForEach(policyStore.AllPolicies.Values, (policy, loopState, idx) => { - if (CheckPolicyAgainstInvite(invite, policy) is not null) { - logger.LogInformation("Found matching policy after {} iterations ({})", idx, sw.Elapsed); - loopState.Break(); - } - }); - - return Task.CompletedTask; - } - - private async Task CheckPolicyAgainstOutstandingInvites(PolicyRuleEventContent policy) { - var tasks = roomInviteHandler.Invites - .Select(invite => CheckPolicyAgainstInvite(invite, policy)) - .Where(x => x is not null) - .Cast<Task>() // from Task? - .ToList(); - - await Task.WhenAll(tasks); - } - - private Task? CheckPolicyAgainstInvite(RoomInviteContext invite, PolicyRuleEventContent policy) { - if (policy.Recommendation != "m.ban") return null; - - var policyMatches = false; - switch (policy) { - case UserPolicyRuleEventContent userPolicy: - policyMatches = userPolicy.EntityMatches(invite.MemberEvent.Sender!); - break; - case ServerPolicyRuleEventContent serverPolicy: - policyMatches = serverPolicy.EntityMatches(invite.MemberEvent.Sender!); - break; - case RoomPolicyRuleEventContent roomPolicy: - policyMatches = roomPolicy.EntityMatches(invite.RoomId); - break; - default: - if (_logRoom is not null) - _ = _logRoom.SendMessageEventAsync(new MessageBuilder().WithColoredBody("#FF0000", "Unknown policy type " + policy.GetType().FullName).Build()); - break; - } - - if (!policyMatches) return null; - logger.LogWarning("Rejecting invite to {}, matching {} {}", invite.RoomId, policy.GetType().GetFriendlyName(), policy.ToJson(ignoreNull: true)); - - return Task.Run(async () => { - if (_logRoom is not null) { - string roomName = await invite.TryGetRoomNameAsync(); - - await roomInviteHandler.RejectInvite(invite, new MessageBuilder() - .WithColoredBody("#FF0000", - cb => cb.WithBody("Rejecting invite to ").WithMention(invite.RoomId, roomName) - .WithBody($", matching {policy.GetType().GetFriendlyName().ToLowerInvariant()}.") - .WithNewline()) - .WithCollapsibleSection("Policy JSON", cb => cb.WithCodeBlock(policy.ToJson(ignoreNull: true), "json")) - ); - } - }); - } -} \ No newline at end of file diff --git a/MatrixAntiDmSpam/PolicyListFetcher.cs b/MatrixAntiDmSpam/PolicyListFetcher.cs deleted file mode 100644
index 723550f..0000000 --- a/MatrixAntiDmSpam/PolicyListFetcher.cs +++ /dev/null
@@ -1,89 +0,0 @@ -using LibMatrix.Filters; -using LibMatrix.Helpers; -using LibMatrix.Homeservers; -using LibMatrix.RoomTypes; - -namespace MatrixAntiDmSpam; - -public class PolicyListFetcher(ILogger<PolicyListFetcher> logger, AntiDmSpamConfiguration config, AuthenticatedHomeserverGeneric homeserver, PolicyStore policyStore) - : IHostedService { - private CancellationTokenSource _cts = new(); - - public async Task StartAsync(CancellationToken cancellationToken) { - // _ = Enumerable.Range(0, 10_000_000).Select(x => { - // policyStore.AllPolicies.Add(Guid.NewGuid().ToString(), new UserPolicyRuleEventContent() { - // Entity = Guid.NewGuid().ToString() + x, - // Reason = "meow " + x, - // Recommendation = "m.ban" - // }); - // return 0; - // }).ToList(); - - logger.LogInformation("Starting policy list fetcher"); - await EnsurePolicyListsJoined(); - _ = SyncPolicyLists(); - } - - public async Task StopAsync(CancellationToken cancellationToken) { - logger.LogInformation("Stopping policy list fetcher"); - await _cts.CancelAsync(); - } - - private async Task EnsurePolicyListsJoined() { - var joinedRooms = await homeserver.GetJoinedRooms(); - var expectedPolicyRooms = config.PolicyLists; - var missingRooms = expectedPolicyRooms.Where(room => !joinedRooms.Any(r => r.RoomId == room.RoomId)).ToList(); - - await Task.WhenAll(missingRooms.Select(async room => { - logger.LogInformation("Joining policy list room {}", room.RoomId); - await homeserver.GetRoom(room.RoomId).JoinAsync(room.Vias); - }).ToList()); - } - - private async Task SyncPolicyLists() { - var syncHelper = new SyncHelper(homeserver, logger) { - Timeout = 30_000, - MinimumDelay = TimeSpan.FromSeconds(3), - FilterId = (await homeserver.UploadFilterAsync(new SyncFilter() { - AccountData = SyncFilter.EventFilter.Empty, - Presence = SyncFilter.EventFilter.Empty, - Room = new SyncFilter.RoomFilter { - AccountData = SyncFilter.RoomFilter.StateFilter.Empty, - Ephemeral = SyncFilter.RoomFilter.StateFilter.Empty, - State = new SyncFilter.RoomFilter.StateFilter(types: PolicyRoom.SpecPolicyEventTypes.ToList()), - Timeline = new SyncFilter.RoomFilter.StateFilter(types: PolicyRoom.SpecPolicyEventTypes.ToList()), - Rooms = config.PolicyLists.Select(x => x.RoomId).ToList(), - IncludeLeave = false - } - })).FilterId - }; - - await foreach (var syncResponse in syncHelper.EnumerateSyncAsync(_cts.Token)) { - if (_cts.IsCancellationRequested) return; - - if (syncResponse is { Rooms.Join.Count: > 0 }) { - foreach (var (roomId, data) in syncResponse.Rooms.Join) { - if (!config.PolicyLists.Any(x => x.RoomId == roomId)) continue; - - if (data.State?.Events is null) - data.State = new() { Events = [] }; - - if (data.Timeline is { Events.Count: > 0 }) { - data.State.Events.AddRange(data.Timeline.Events); - data.Timeline = null; - } - } - - var newPolicies = syncResponse.Rooms!.Join!.SelectMany(x => x.Value.State!.Events!) - .Where(x => PolicyRoom.SpecPolicyEventTypes.Contains(x.Type)) - .ToList(); - - logger.LogWarning("Received non-empty sync response with {}/{}/{} rooms, resulting in {} policies", syncResponse.Rooms?.Join?.Count, - syncResponse.Rooms?.Invite?.Count, - syncResponse.Rooms?.Leave?.Count, newPolicies.Count); - - await policyStore.AddPoliciesAsync(newPolicies); - } - } - } -} \ No newline at end of file diff --git a/MatrixAntiDmSpam/PolicyStore.cs b/MatrixAntiDmSpam/PolicyStore.cs deleted file mode 100644
index 863bc92..0000000 --- a/MatrixAntiDmSpam/PolicyStore.cs +++ /dev/null
@@ -1,24 +0,0 @@ -using LibMatrix; -using LibMatrix.EventTypes.Spec.State.Policy; - -namespace MatrixAntiDmSpam; - -public class PolicyStore { - public Dictionary<string, PolicyRuleEventContent> AllPolicies { get; } = []; - public List<Func<PolicyRuleEventContent, Task>> OnPolicyUpdated { get; } = []; - - public Task AddPoliciesAsync(IEnumerable<StateEventResponse> events) => Task.WhenAll(events.Select(AddPolicyAsync).ToList()); - - public async Task AddPolicyAsync(StateEventResponse evt) { - var eventKey = $"{evt.RoomId}:{evt.Type}:{evt.StateKey}"; - if (evt.TypedContent is PolicyRuleEventContent policy) { - if (policy.Recommendation == "m.ban") - AllPolicies[eventKey] = policy; - else AllPolicies.Remove(eventKey); - - foreach (var callback in OnPolicyUpdated) { - await callback(policy); - } - } - } -} \ No newline at end of file diff --git a/MatrixAntiDmSpam/Program.cs b/MatrixAntiDmSpam/Program.cs
index e3eb037..fc1d603 100644 --- a/MatrixAntiDmSpam/Program.cs +++ b/MatrixAntiDmSpam/Program.cs
@@ -1,7 +1,7 @@ using LibMatrix.Extensions; using LibMatrix.Services; using LibMatrix.Utilities.Bot; -using MatrixAntiDmSpam; +using MatrixAntiDmSpam.Core; var builder = Host.CreateApplicationBuilder(args);