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);
|