From 03fb16cc050b94fdc608898db3025cf43a78f437 Mon Sep 17 00:00:00 2001 From: Rory& Date: Thu, 1 May 2025 17:14:29 +0200 Subject: Move core logic to a separate library to facilitate embedding --- MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs | 17 ++++ MatrixAntiDmSpam.Core/InviteHandler.cs | 92 +++++++++++++++++++++ MatrixAntiDmSpam.Core/MatrixAntiDmSpam.Core.csproj | 19 +++++ MatrixAntiDmSpam.Core/PolicyExecutor.cs | 94 ++++++++++++++++++++++ MatrixAntiDmSpam.Core/PolicyListFetcher.cs | 91 +++++++++++++++++++++ MatrixAntiDmSpam.Core/PolicyStore.cs | 24 ++++++ MatrixAntiDmSpam.sln | 14 ++++ MatrixAntiDmSpam/AntiDmSpamConfiguration.cs | 15 ---- MatrixAntiDmSpam/InviteHandler.cs | 91 --------------------- MatrixAntiDmSpam/MatrixAntiDmSpam.csproj | 1 + MatrixAntiDmSpam/PolicyExecutor.cs | 92 --------------------- MatrixAntiDmSpam/PolicyListFetcher.cs | 89 -------------------- MatrixAntiDmSpam/PolicyStore.cs | 24 ------ MatrixAntiDmSpam/Program.cs | 2 +- 14 files changed, 353 insertions(+), 312 deletions(-) create mode 100644 MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs create mode 100644 MatrixAntiDmSpam.Core/InviteHandler.cs create mode 100644 MatrixAntiDmSpam.Core/MatrixAntiDmSpam.Core.csproj create mode 100644 MatrixAntiDmSpam.Core/PolicyExecutor.cs create mode 100644 MatrixAntiDmSpam.Core/PolicyListFetcher.cs create mode 100644 MatrixAntiDmSpam.Core/PolicyStore.cs delete mode 100644 MatrixAntiDmSpam/AntiDmSpamConfiguration.cs delete mode 100644 MatrixAntiDmSpam/InviteHandler.cs delete mode 100644 MatrixAntiDmSpam/PolicyExecutor.cs delete mode 100644 MatrixAntiDmSpam/PolicyListFetcher.cs delete mode 100644 MatrixAntiDmSpam/PolicyStore.cs diff --git a/MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs b/MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs new file mode 100644 index 0000000..fe68e8a --- /dev/null +++ b/MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; + +namespace MatrixAntiDmSpam.Core; + +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 PolicyLists { get; set; } + + public class PolicyRoomReference { + public required string Name { get; set; } + public required string RoomId { get; set; } + public required List Vias { get; set; } + } +} \ No newline at end of file diff --git a/MatrixAntiDmSpam.Core/InviteHandler.cs b/MatrixAntiDmSpam.Core/InviteHandler.cs new file mode 100644 index 0000000..41f4011 --- /dev/null +++ b/MatrixAntiDmSpam.Core/InviteHandler.cs @@ -0,0 +1,92 @@ +using System.Text.Json; +using ArcaneLibs; +using ArcaneLibs.Extensions; +using LibMatrix.Helpers; +using LibMatrix.RoomTypes; +using LibMatrix.Utilities.Bot.Interfaces; +using Microsoft.Extensions.Logging; + +namespace MatrixAntiDmSpam.Core; + +public class RoomInviteHandler(ILogger logger, AntiDmSpamConfiguration config) : IRoomInviteHandler { + public List Invites { get; } = []; + + public List> 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.Core/MatrixAntiDmSpam.Core.csproj b/MatrixAntiDmSpam.Core/MatrixAntiDmSpam.Core.csproj new file mode 100644 index 0000000..6e9363f --- /dev/null +++ b/MatrixAntiDmSpam.Core/MatrixAntiDmSpam.Core.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + preview + enable + enable + + + + + + + + + + + + diff --git a/MatrixAntiDmSpam.Core/PolicyExecutor.cs b/MatrixAntiDmSpam.Core/PolicyExecutor.cs new file mode 100644 index 0000000..4dcaf2b --- /dev/null +++ b/MatrixAntiDmSpam.Core/PolicyExecutor.cs @@ -0,0 +1,94 @@ +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; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MatrixAntiDmSpam.Core; + +public class PolicyExecutor( + ILogger 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() // 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.Core/PolicyListFetcher.cs b/MatrixAntiDmSpam.Core/PolicyListFetcher.cs new file mode 100644 index 0000000..b8f5518 --- /dev/null +++ b/MatrixAntiDmSpam.Core/PolicyListFetcher.cs @@ -0,0 +1,91 @@ +using LibMatrix.Filters; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.RoomTypes; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MatrixAntiDmSpam.Core; + +public class PolicyListFetcher(ILogger 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.Core/PolicyStore.cs b/MatrixAntiDmSpam.Core/PolicyStore.cs new file mode 100644 index 0000000..9c439fa --- /dev/null +++ b/MatrixAntiDmSpam.Core/PolicyStore.cs @@ -0,0 +1,24 @@ +using LibMatrix; +using LibMatrix.EventTypes.Spec.State.Policy; + +namespace MatrixAntiDmSpam.Core; + +public class PolicyStore { + public Dictionary AllPolicies { get; } = []; + public List> OnPolicyUpdated { get; } = []; + + public Task AddPoliciesAsync(IEnumerable 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.sln b/MatrixAntiDmSpam.sln index 296786a..143be0b 100644 --- a/MatrixAntiDmSpam.sln +++ b/MatrixAntiDmSpam.sln @@ -42,6 +42,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.TestDataGenerator EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{E526424D-ADC7-41DB-AD63-E359AC2954D9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MatrixAntiDmSpam.Core", "MatrixAntiDmSpam.Core\MatrixAntiDmSpam.Core.csproj", "{5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -280,6 +282,18 @@ Global {E526424D-ADC7-41DB-AD63-E359AC2954D9}.Release|x64.Build.0 = Release|Any CPU {E526424D-ADC7-41DB-AD63-E359AC2954D9}.Release|x86.ActiveCfg = Release|Any CPU {E526424D-ADC7-41DB-AD63-E359AC2954D9}.Release|x86.Build.0 = Release|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Debug|x64.Build.0 = Debug|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Debug|x86.Build.0 = Debug|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|Any CPU.Build.0 = Release|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|x64.ActiveCfg = Release|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|x64.Build.0 = Release|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|x86.ActiveCfg = Release|Any CPU + {5A4F5F8A-3638-4E3C-B20D-4F651EA165DD}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE 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 PolicyLists { get; set; } - - public class PolicyRoomReference { - public required string Name { get; set; } - public required string RoomId { get; set; } - public required List 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 logger, AntiDmSpamConfiguration config) : IRoomInviteHandler { - public List Invites { get; } = []; - - public List> 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 @@ + 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 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() // 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 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 AllPolicies { get; } = []; - public List> OnPolicyUpdated { get; } = []; - - public Task AddPoliciesAsync(IEnumerable 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); -- cgit 1.5.1