about summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-05-02 16:33:05 +0200
committerRory& <root@rory.gay>2025-05-02 16:33:05 +0200
commit32a474ae32b4e1da099176d779e030dfbf70ed5a (patch)
tree475d0a5bf4d8b1babc66f327498b420530af11cf
parentMove core logic to a separate library to facilitate embedding (diff)
downloadMatrixAntiDmSpam-32a474ae32b4e1da099176d779e030dfbf70ed5a.tar.xz
Add support for ignoring users, change internal API
m---------LibMatrix0
-rw-r--r--MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs2
-rw-r--r--MatrixAntiDmSpam.Core/Classes/MADSIgnoreMetadataContent.cs25
-rw-r--r--MatrixAntiDmSpam.Core/PolicyExecutor.cs88
-rw-r--r--MatrixAntiDmSpam.Core/PolicyStore.cs104
-rw-r--r--MatrixAntiDmSpam.Core/RoomInviteHandler.cs (renamed from MatrixAntiDmSpam.Core/InviteHandler.cs)0
-rw-r--r--MatrixAntiDmSpam.sln.DotSettings.user1
-rw-r--r--MatrixAntiDmSpam/Program.cs2
-rw-r--r--MatrixAntiDmSpam/appsettings.Development.json6
9 files changed, 211 insertions, 17 deletions
diff --git a/LibMatrix b/LibMatrix
-Subproject 1c30aec46b495f1da87c3a6adbda3e19e014b55
+Subproject 2fde2d5f961eabf3167280ba55786cdb6b38f2c
diff --git a/MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs b/MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs

index fe68e8a..bd06db3 100644 --- a/MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs +++ b/MatrixAntiDmSpam.Core/AntiDmSpamConfiguration.cs
@@ -6,6 +6,8 @@ public class AntiDmSpamConfiguration { public AntiDmSpamConfiguration(IConfiguration config) => config.GetRequiredSection("AntiDmSpam").Bind(this); public string? LogRoom { get; set; } public bool LogInviteDataAsFile { get; set; } + public bool IgnoreBannedUsers { get; set; } + public bool ReportBlockedInvites { get; set; } public required List<PolicyRoomReference> PolicyLists { get; set; } diff --git a/MatrixAntiDmSpam.Core/Classes/MADSIgnoreMetadataContent.cs b/MatrixAntiDmSpam.Core/Classes/MADSIgnoreMetadataContent.cs new file mode 100644
index 0000000..c9ccdb1 --- /dev/null +++ b/MatrixAntiDmSpam.Core/Classes/MADSIgnoreMetadataContent.cs
@@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace MatrixAntiDmSpam.Core.Classes; + +public class MadsIgnoreMetadataContent { + public const string EventId = "gay.rory.MatrixAntiDmSpam.ignore_metadata"; + + // Whether the ignore entry already existed, if true, do not remove when no matching policies exist + [JsonPropertyName("was_user_added")] + public required bool WasUserAdded { get; set; } + + [JsonPropertyName("policies")] + public required List<PolicyEventReference> Policies { get; set; } + + public class PolicyEventReference { + [JsonPropertyName("room_id")] + public required string RoomId { get; set; } + + [JsonPropertyName("type")] + public required string Type { get; set; } + + [JsonPropertyName("state_key")] + public required string StateKey { get; set; } + } +} \ No newline at end of file diff --git a/MatrixAntiDmSpam.Core/PolicyExecutor.cs b/MatrixAntiDmSpam.Core/PolicyExecutor.cs
index 4dcaf2b..23d70a8 100644 --- a/MatrixAntiDmSpam.Core/PolicyExecutor.cs +++ b/MatrixAntiDmSpam.Core/PolicyExecutor.cs
@@ -1,11 +1,14 @@ using System.Diagnostics; using ArcaneLibs.Attributes; using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; using LibMatrix.EventTypes.Spec.State.Policy; using LibMatrix.Helpers; using LibMatrix.Homeservers; using LibMatrix.RoomTypes; using LibMatrix.Utilities.Bot.Interfaces; +using MatrixAntiDmSpam.Core.Classes; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -21,7 +24,11 @@ public class PolicyExecutor( public Task StartAsync(CancellationToken cancellationToken) { roomInviteHandler.OnInviteReceived.Add(CheckPoliciesAgainstInvite); - policyStore.OnPolicyUpdated.Add(CheckPolicyAgainstOutstandingInvites); + policyStore.OnPolicyAdded.Add(CheckPolicyAgainstOutstandingInvites); + if (config.IgnoreBannedUsers) { + policyStore.OnPoliciesChanged.Add(UpdateIgnoreList); + } + return Task.CompletedTask; } @@ -29,6 +36,76 @@ public class PolicyExecutor( return Task.CompletedTask; } +#region Feature: Manage ignore list + + private async Task UpdateIgnoreList(( + List<StateEventResponse> NewPolicies, + List<(StateEventResponse Old, StateEventResponse New)> UpdatedPolicies, + List<(StateEventResponse Old, StateEventResponse New)> RemovedPolicies) updates + ) { + var ignoreListContent = await homeserver.GetIgnoredUserListAsync(); + + foreach (var newEvent in updates.NewPolicies) { + var content = newEvent.TypedContent as PolicyRuleEventContent; + if (content.Entity is null || content.IsGlobRule()) continue; + if (content.GetNormalizedRecommendation() != "m.ban") continue; + + var policyEventReference = new MadsIgnoreMetadataContent.PolicyEventReference() { + Type = newEvent.Type, + RoomId = newEvent.RoomId, + StateKey = newEvent.StateKey! + }; + + if (ignoreListContent.IgnoredUsers.TryGetValue(content.Entity, out var existingRule)) { + if (existingRule.AdditionalData?.ContainsKey(MadsIgnoreMetadataContent.EventId) ?? false) { + var existingMetadata = existingRule.GetAdditionalData<MadsIgnoreMetadataContent>(MadsIgnoreMetadataContent.EventId); + existingMetadata.Policies.Add(policyEventReference); + } + else { + existingRule.AdditionalData ??= new(); + existingRule.AdditionalData.Add(MadsIgnoreMetadataContent.EventId, new MadsIgnoreMetadataContent { + WasUserAdded = true, + Policies = [policyEventReference] + }); + } + } + else { + ignoreListContent.IgnoredUsers[content.Entity] = new() { + AdditionalData = new() { + [MadsIgnoreMetadataContent.EventId] = new MadsIgnoreMetadataContent { + WasUserAdded = false, + Policies = [policyEventReference] + } + } + }; + } + } + + foreach (var (previousEvent, newEvent) in updates.RemovedPolicies) { + if (previousEvent.Type != UserPolicyRuleEventContent.EventId) continue; + var previousContent = previousEvent.ContentAs<UserPolicyRuleEventContent>(); + + if (previousContent.Entity is null || previousContent.IsGlobRule()) continue; + if (previousContent.GetNormalizedRecommendation() != "m.ban") continue; + + var ignoreList = await homeserver.GetIgnoredUserListAsync(); + if (ignoreList.IgnoredUsers.TryGetValue(previousContent.Entity, out var existingRule)) { + if (existingRule.AdditionalData?.ContainsKey(MadsIgnoreMetadataContent.EventId) ?? false) { + var existingMetadata = existingRule.GetAdditionalData<MadsIgnoreMetadataContent>(MadsIgnoreMetadataContent.EventId); + existingMetadata.Policies.RemoveAll(x => x.Type == previousEvent.Type && x.RoomId == previousEvent.RoomId && x.StateKey == previousEvent.StateKey); + if (!existingMetadata.WasUserAdded) + ignoreList.IgnoredUsers.Remove(previousContent.Entity); + } + } + } + + await homeserver.SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoreListContent); + } + +#endregion + +#region Feature: Reject invites + private Task CheckPoliciesAgainstInvite(RoomInviteContext invite) { logger.LogInformation("Checking policies against invite"); var sw = Stopwatch.StartNew(); @@ -44,9 +121,9 @@ public class PolicyExecutor( return Task.CompletedTask; } - private async Task CheckPolicyAgainstOutstandingInvites(PolicyRuleEventContent policy) { + private async Task CheckPolicyAgainstOutstandingInvites(StateEventResponse newEvent) { var tasks = roomInviteHandler.Invites - .Select(invite => CheckPolicyAgainstInvite(invite, policy)) + .Select(invite => CheckPolicyAgainstInvite(invite, newEvent)) .Where(x => x is not null) .Cast<Task>() // from Task? .ToList(); @@ -54,7 +131,8 @@ public class PolicyExecutor( await Task.WhenAll(tasks); } - private Task? CheckPolicyAgainstInvite(RoomInviteContext invite, PolicyRuleEventContent policy) { + private Task? CheckPolicyAgainstInvite(RoomInviteContext invite, StateEventResponse policyEvent) { + var policy = policyEvent.TypedContent as PolicyRuleEventContent ?? throw new InvalidOperationException("Policy is null"); if (policy.Recommendation != "m.ban") return null; var policyMatches = false; @@ -91,4 +169,6 @@ public class PolicyExecutor( } }); } + +#endregion } \ No newline at end of file diff --git a/MatrixAntiDmSpam.Core/PolicyStore.cs b/MatrixAntiDmSpam.Core/PolicyStore.cs
index 9c439fa..0dfd490 100644 --- a/MatrixAntiDmSpam.Core/PolicyStore.cs +++ b/MatrixAntiDmSpam.Core/PolicyStore.cs
@@ -1,24 +1,104 @@ +using System.Diagnostics; using LibMatrix; using LibMatrix.EventTypes.Spec.State.Policy; +using Microsoft.Extensions.Logging; namespace MatrixAntiDmSpam.Core; -public class PolicyStore { - public Dictionary<string, PolicyRuleEventContent> AllPolicies { get; } = []; - public List<Func<PolicyRuleEventContent, Task>> OnPolicyUpdated { get; } = []; +public class PolicyStore(ILogger<PolicyStore> logger) { + public Dictionary<string, StateEventResponse> AllPolicies { get; } = []; - public Task AddPoliciesAsync(IEnumerable<StateEventResponse> events) => Task.WhenAll(events.Select(AddPolicyAsync).ToList()); +#region Single policy events - 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); + /// <summary> + /// Fired when any policy event is received + /// </summary> + public List<Func<StateEventResponse, Task>> OnPolicyReceived { get; } = []; - foreach (var callback in OnPolicyUpdated) { - await callback(policy); + /// <summary> + /// Fired when a new policy is added + /// </summary> + public List<Func<StateEventResponse, Task>> OnPolicyAdded { get; } = []; + + /// <summary> + /// Fired when a policy is updated, without being removed. + /// </summary> + public List<Func<StateEventResponse, StateEventResponse, Task>> OnPolicyUpdated { get; } = []; + + /// <summary> + /// Fired when a policy is removed. + /// </summary> + public List<Func<StateEventResponse, StateEventResponse, Task>> OnPolicyRemoved { get; } = []; + +#endregion + +#region Bulk policy events + + /// <summary> + /// Fired when any policy event is received + /// </summary> + public List<Func<(List<StateEventResponse> NewPolicies, + List<(StateEventResponse Old, StateEventResponse New)> UpdatedPolicies, + List<(StateEventResponse Old, StateEventResponse New)> RemovedPolicies), Task>> OnPoliciesChanged { get; } = []; + +#endregion + + public async Task AddPoliciesAsync(IEnumerable<StateEventResponse> events) { + var policyEvents = events + .Where(evt => evt.TypedContent is PolicyRuleEventContent) + .ToList(); + List<StateEventResponse> newPolicies = new(); + List<(StateEventResponse Old, StateEventResponse New)> updatedPolicies = new(); + List<(StateEventResponse Old, StateEventResponse New)> removedPolicies = new(); + + var sw = Stopwatch.StartNew(); + try { + foreach (var evt in policyEvents) { + var eventKey = $"{evt.RoomId}:{evt.Type}:{evt.StateKey}"; + if (evt.TypedContent is not PolicyRuleEventContent policy) continue; + if (policy.GetNormalizedRecommendation() is "m.ban") { + if (AllPolicies.TryGetValue(eventKey, out var oldContent)) + updatedPolicies.Add((oldContent, evt)); + else + newPolicies.Add(evt); + AllPolicies[eventKey] = evt; + } + else if (AllPolicies.Remove(eventKey, out var oldContent)) + removedPolicies.Add((oldContent, evt)); } } + catch (Exception e) { + Console.WriteLine(e); + } + + logger.LogInformation("Processed {Count} policies in {Elapsed}", policyEvents.Count, sw.Elapsed); + + + // execute all the callbacks in parallel, as much as possible... + await Task.WhenAll( + Task.WhenAll( + policyEvents.Select(evt => + Task.WhenAll(OnPolicyReceived.Select(callback => callback(evt)).ToList()) + ) + ), + Task.WhenAll( + newPolicies.Select(evt => + Task.WhenAll(OnPolicyAdded.Select(callback => callback(evt)).ToList()) + ) + ), + Task.WhenAll( + updatedPolicies.Select(evt => + Task.WhenAll(OnPolicyUpdated.Select(callback => callback(evt.Old, evt.New)).ToList()) + ) + ), + Task.WhenAll( + removedPolicies.Select(evt => + Task.WhenAll(OnPolicyRemoved.Select(callback => callback(evt.Old, evt.New)).ToList()) + ) + ), + Task.WhenAll(OnPoliciesChanged.Select(callback => callback((newPolicies, updatedPolicies, removedPolicies))).ToList()) + ); } + + private async Task AddPolicyAsync(StateEventResponse evt) { } } \ No newline at end of file diff --git a/MatrixAntiDmSpam.Core/InviteHandler.cs b/MatrixAntiDmSpam.Core/RoomInviteHandler.cs
index 41f4011..41f4011 100644 --- a/MatrixAntiDmSpam.Core/InviteHandler.cs +++ b/MatrixAntiDmSpam.Core/RoomInviteHandler.cs
diff --git a/MatrixAntiDmSpam.sln.DotSettings.user b/MatrixAntiDmSpam.sln.DotSettings.user
index 73d444d..159add8 100644 --- a/MatrixAntiDmSpam.sln.DotSettings.user +++ b/MatrixAntiDmSpam.sln.DotSettings.user
@@ -2,6 +2,7 @@ <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AExceptionDispatchInfo_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F97_003Ffb30ee1f_003FExceptionDispatchInfo_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHashtable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003Fd2_003F20632896_003FHashtable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F65_003Fb77a719c_003FList_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> + <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff1b929573c264d7a81f261ae2f951019d19e00_003F2f_003F707c45aa_003FList_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObjectExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F8fd5e96d6574456095123be1ecfbdfa914200_003Fe2_003F3561a383_003FObjectExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AServiceCollectionHostedServiceExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ff13297a632424a6abffea4dd75a36a75d128_003Ff9_003Fb35aae11_003FServiceCollectionHostedServiceExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStackFrameIterator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fea51ca5e833244688d7ca912cfc70784d19c00_003F86_003F8b4aa64e_003FStackFrameIterator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> diff --git a/MatrixAntiDmSpam/Program.cs b/MatrixAntiDmSpam/Program.cs
index fc1d603..935e23f 100644 --- a/MatrixAntiDmSpam/Program.cs +++ b/MatrixAntiDmSpam/Program.cs
@@ -15,7 +15,7 @@ builder.Services.AddHostedService<PolicyExecutor>(); builder.Services.AddSingleton<PolicyStore>(); -MatrixHttpClient.LogRequests = false; +// MatrixHttpClient.LogRequests = false; var host = builder.Build(); host.Run(); \ No newline at end of file diff --git a/MatrixAntiDmSpam/appsettings.Development.json b/MatrixAntiDmSpam/appsettings.Development.json
index fd582b4..4cfb975 100644 --- a/MatrixAntiDmSpam/appsettings.Development.json +++ b/MatrixAntiDmSpam/appsettings.Development.json
@@ -34,8 +34,14 @@ } }, "AntiDmSpam": { + // Whether invites should be logged to a room. "LogRoom": "!GrLSwdAkdrvfMrRYKR:rory.gay", "LogInviteDataAsFile": true, + // Whether to report users and rooms when an invite is blocked. + "ReportBlockedInvites": true, + // WARNING: If you're a room moderator, this will cause your client to not receive events from ignored users! + "IgnoreBannedUsers": true, + // Policy lists to follow "PolicyLists": [ { "Name": "Community Moderation Effort",