about summary refs log tree commit diff
path: root/MatrixContentFilter/Handlers
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-03-12 19:52:15 +0100
committerRory& <root@rory.gay>2025-03-12 19:52:15 +0100
commitbdf058ab5c936463a022f62ffbb55bb71c26e856 (patch)
tree69914accad80f10dd8d87e3738e820c58f042537 /MatrixContentFilter/Handlers
parentInitial commit (diff)
downloadMatrixContentFilter-master.tar.xz
More work HEAD master
Diffstat (limited to 'MatrixContentFilter/Handlers')
-rw-r--r--MatrixContentFilter/Handlers/Filters/Generic/BasicMessageTypeFilter.cs88
-rw-r--r--MatrixContentFilter/Handlers/Filters/Generic/GenericEventTypeFilter.cs86
-rw-r--r--MatrixContentFilter/Handlers/Filters/ImageFilter.cs35
-rw-r--r--MatrixContentFilter/Handlers/Filters/PendingInviteLimiter.cs92
-rw-r--r--MatrixContentFilter/Handlers/Filters/VideoFilter.cs28
-rw-r--r--MatrixContentFilter/Handlers/InviteHandler.cs33
6 files changed, 336 insertions, 26 deletions
diff --git a/MatrixContentFilter/Handlers/Filters/Generic/BasicMessageTypeFilter.cs b/MatrixContentFilter/Handlers/Filters/Generic/BasicMessageTypeFilter.cs
new file mode 100644

index 0000000..2a9c674 --- /dev/null +++ b/MatrixContentFilter/Handlers/Filters/Generic/BasicMessageTypeFilter.cs
@@ -0,0 +1,88 @@ +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; +using MatrixContentFilter.Abstractions; +using MatrixContentFilter.EventTypes; +using MatrixContentFilter.Services; +using MatrixContentFilter.Services.AsyncActionQueues; + +namespace MatrixContentFilter.Handlers.Filters.Generic; + +public abstract class BasicMessageTypeFilter( + ConfigurationService cfgService, + AuthenticatedHomeserverGeneric hs, + AsyncMessageQueue msgQueue, + InfoCacheService infoCache, + AbstractAsyncActionQueue actionQueue, + MatrixContentFilterMetrics metrics) + : IContentFilter { + private protected string MessageType = null!; + private protected string MessageTypeName = null!; + private protected string MessageTypeNamePlural = null!; + + public override Task ProcessSyncAsync(SyncResponse syncResponse) { + if (syncResponse.Rooms?.Join is null) return Task.CompletedTask; + var tasks = syncResponse.Rooms.Join.Select(ProcessRoomAsync); + return Task.WhenAll(tasks); + } + + private Task ProcessRoomAsync(KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure> syncRoom) { + var (roomId, roomData) = syncRoom; + if (roomId == cfgService.LogRoom.RoomId || roomId == cfgService.ControlRoom.RoomId) return Task.CompletedTask; + if (roomData.Timeline?.Events is null) return Task.CompletedTask; + var config = cfgService.RoomConfigurationOverrides.GetValueOrDefault(roomId)?.ImageFilter; + + var room = hs.GetRoom(roomId); + + var tasks = roomData.Timeline.Events.Select(msg => ProcessEventAsync(room, msg, config)); + return Task.WhenAll(tasks); + } + + public override Task ProcessEventListAsync(List<StateEventResponse> events) { + var tasks = events.GroupBy(x => x.RoomId).Select(async x => { + var room = hs.GetRoom(x.Key); + var config = cfgService.RoomConfigurationOverrides.GetValueOrDefault(x.Key)?.ImageFilter; + var tasks = x.Select(msg => ProcessEventAsync(room, msg, config)).ToList(); + await Task.WhenAll(tasks); + }); + + return Task.WhenAll(tasks); + } + + private async Task ProcessEventAsync(GenericRoom room, StateEventResponse msg, FilterConfiguration.BasicFilterConfiguration roomConfiguration) { + if (msg.Type != "m.room.message") return; + var content = msg.TypedContent as RoomMessageEventContent; + if (content?.MessageType != MessageType) return; + metrics.Increment(MessageTypeName + "_filter_processed_event_count"); + + await actionQueue.EqueueActionAsync(msg.EventId, async () => { + while (true) { + try { + Console.WriteLine("Redacting {0} message: {1}", MessageTypeName, msg.EventId); + await room.RedactEventAsync(msg.EventId ?? throw new ArgumentException("Event ID is null?"), $"Not allowed to send {MessageTypeNamePlural} in this room!"); + break; + } + catch (Exception e) { + msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice") + .WithBody($"Error redacting {MessageTypeName} message in {room.RoomId}!") + .WithCollapsibleSection("Error data", msb => msb.WithCodeBlock(e.ToString(), "csharp")) + .Build()); + } + } + + var displayName = await infoCache.GetDisplayNameAsync(room.RoomId, msg.Sender); + var roomName = await infoCache.GetRoomNameAsync(room.RoomId); + + msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice") + .WithBody($"{MessageTypeName[0].ToString().ToUpper() + MessageTypeName[1..]} sent by ").WithMention(msg.Sender, displayName).WithBody(" in ") + .WithMention(room.RoomId, roomName).WithBody(" was removed!").WithNewline() + .WithCollapsibleSection("Message data", msb => msb.WithCodeBlock(content.ToJson(ignoreNull: true), "json")) + .Build()); + }); + ActionCount++; + } +} \ No newline at end of file diff --git a/MatrixContentFilter/Handlers/Filters/Generic/GenericEventTypeFilter.cs b/MatrixContentFilter/Handlers/Filters/Generic/GenericEventTypeFilter.cs new file mode 100644
index 0000000..64e4cff --- /dev/null +++ b/MatrixContentFilter/Handlers/Filters/Generic/GenericEventTypeFilter.cs
@@ -0,0 +1,86 @@ +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; +using MatrixContentFilter.Abstractions; +using MatrixContentFilter.EventTypes; +using MatrixContentFilter.Services; +using MatrixContentFilter.Services.AsyncActionQueues; + +namespace MatrixContentFilter.Handlers.Filters.Generic; + +public abstract class GenericEventTypeFilter( + ConfigurationService cfgService, + AuthenticatedHomeserverGeneric hs, + AsyncMessageQueue msgQueue, + InfoCacheService infoCache, + AbstractAsyncActionQueue actionQueue, + MatrixContentFilterMetrics metrics) + : IContentFilter { + private protected abstract string? GetEventType(); + + public override Task ProcessSyncAsync(SyncResponse syncResponse) { + if (syncResponse.Rooms?.Join is null) return Task.CompletedTask; + var tasks = syncResponse.Rooms.Join.Select(ProcessRoomAsync); + return Task.WhenAll(tasks); + } + + private Task ProcessRoomAsync(KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure> syncRoom) { + var (roomId, roomData) = syncRoom; + if (roomId == cfgService.LogRoom.RoomId || roomId == cfgService.ControlRoom.RoomId) return Task.CompletedTask; + if (roomData.Timeline?.Events is null) return Task.CompletedTask; + var config = cfgService.RoomConfigurationOverrides.GetValueOrDefault(roomId)?.ImageFilter; + + var room = hs.GetRoom(roomId); + + var tasks = roomData.Timeline.Events.Select(msg => ProcessEventAsync(room, msg, config)); + return Task.WhenAll(tasks); + } + + public override Task ProcessEventListAsync(List<StateEventResponse> events) { + var tasks = events.GroupBy(x => x.RoomId).Select(async x => { + var room = hs.GetRoom(x.Key); + var config = cfgService.RoomConfigurationOverrides.GetValueOrDefault(x.Key)?.ImageFilter; + var tasks = x.Select(msg => ProcessEventAsync(room, msg, config)).ToList(); + await Task.WhenAll(tasks); + }); + + return Task.WhenAll(tasks); + } + + private async Task ProcessEventAsync(GenericRoom room, StateEventResponse msg, FilterConfiguration.BasicFilterConfiguration roomConfiguration) { + if (msg.Type != "m.room.message") return; + var content = msg.TypedContent as RoomMessageEventContent; + if (content?.MessageType != GetEventType()) return; + metrics.Increment(GetMetricsPrefix() + "_filter_processed_event_count"); + + await actionQueue.EqueueActionAsync(msg.EventId, async () => { + while (true) { + try { + Console.WriteLine("Redacting {0} message: {1}", MessageTypeName, msg.EventId); + await room.RedactEventAsync(msg.EventId ?? throw new ArgumentException("Event ID is null?"), $"Not allowed to send {MessageTypeNamePlural} in this room!"); + break; + } + catch (Exception e) { + msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice") + .WithBody($"Error redacting {MessageTypeName} message in {room.RoomId}!") + .WithCollapsibleSection("Error data", msb => msb.WithCodeBlock(e.ToString(), "csharp")) + .Build()); + } + } + + var displayName = await infoCache.GetDisplayNameAsync(room.RoomId, msg.Sender); + var roomName = await infoCache.GetRoomNameAsync(room.RoomId); + + msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice") + .WithBody($"{MessageTypeName[0].ToString().ToUpper() + MessageTypeName[1..]} sent by ").WithMention(msg.Sender, displayName).WithBody(" in ") + .WithMention(room.RoomId, roomName).WithBody(" was removed!").WithNewline() + .WithCollapsibleSection("Message data", msb => msb.WithCodeBlock(content.ToJson(ignoreNull: true), "json")) + .Build()); + }); + ActionCount++; + } +} \ No newline at end of file diff --git a/MatrixContentFilter/Handlers/Filters/ImageFilter.cs b/MatrixContentFilter/Handlers/Filters/ImageFilter.cs
index 13a68f9..7a298ce 100644 --- a/MatrixContentFilter/Handlers/Filters/ImageFilter.cs +++ b/MatrixContentFilter/Handlers/Filters/ImageFilter.cs
@@ -1,7 +1,3 @@ -using System.Runtime.Loader; -using System.Security.Cryptography; -using System.Text.Json.Nodes; -using ArcaneLibs.Collections; using ArcaneLibs.Extensions; using LibMatrix; using LibMatrix.EventTypes.Spec; @@ -21,46 +17,43 @@ public class ImageFilter( AuthenticatedHomeserverGeneric hs, AsyncMessageQueue msgQueue, InfoCacheService infoCache, - AbstractAsyncActionQueue actionQueue) + AbstractAsyncActionQueue actionQueue, + MatrixContentFilterMetrics metrics) : IContentFilter { - public override async Task ProcessSyncAsync(SyncResponse syncResponse) { - Console.WriteLine("Processing image filter"); - if (syncResponse.Rooms?.Join is null) return; + public override Task ProcessSyncAsync(SyncResponse syncResponse) { + if (syncResponse.Rooms?.Join is null) return Task.CompletedTask; var tasks = syncResponse.Rooms.Join.Select(ProcessRoomAsync); - await Task.WhenAll(tasks); + return Task.WhenAll(tasks); } - // private SemaphoreSlim semaphore = new(8, 8); - - private async Task ProcessRoomAsync(KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure> syncRoom) { + private Task ProcessRoomAsync(KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure> syncRoom) { var (roomId, roomData) = syncRoom; - if (roomId == cfgService.LogRoom.RoomId || roomId == cfgService.ControlRoom.RoomId) return; - if (roomData.Timeline?.Events is null) return; + if (roomId == cfgService.LogRoom.RoomId || roomId == cfgService.ControlRoom.RoomId) return Task.CompletedTask; + if (roomData.Timeline?.Events is null) return Task.CompletedTask; var config = cfgService.RoomConfigurationOverrides.GetValueOrDefault(roomId)?.ImageFilter; var room = hs.GetRoom(roomId); var tasks = roomData.Timeline.Events.Select(msg => ProcessEventAsync(room, msg, config)); - await Task.WhenAll(tasks); + return Task.WhenAll(tasks); } - public override async Task ProcessEventListAsync(List<StateEventResponse> events) { + public override Task ProcessEventListAsync(List<StateEventResponse> events) { var tasks = events.GroupBy(x => x.RoomId).Select(async x => { var room = hs.GetRoom(x.Key); var config = cfgService.RoomConfigurationOverrides.GetValueOrDefault(x.Key)?.ImageFilter; - var tasks = x.Select(msg => ProcessEventAsync(room, msg, config)); + var tasks = x.Select(msg => ProcessEventAsync(room, msg, config)).ToList(); await Task.WhenAll(tasks); }); - await Task.WhenAll(tasks); + return Task.WhenAll(tasks); } private async Task ProcessEventAsync(GenericRoom room, StateEventResponse msg, FilterConfiguration.BasicFilterConfiguration roomConfiguration) { if (msg.Type != "m.room.message") return; var content = msg.TypedContent as RoomMessageEventContent; if (content?.MessageType != "m.image") return; - - // await semaphore.WaitAsync(); + metrics.Increment("image_filter_processed_event_count"); await actionQueue.EqueueActionAsync(msg.EventId, async () => { while (true) { @@ -86,7 +79,5 @@ public class ImageFilter( .Build()); }); ActionCount++; - - // semaphore.Release(); } } \ No newline at end of file diff --git a/MatrixContentFilter/Handlers/Filters/PendingInviteLimiter.cs b/MatrixContentFilter/Handlers/Filters/PendingInviteLimiter.cs new file mode 100644
index 0000000..b1b1e8b --- /dev/null +++ b/MatrixContentFilter/Handlers/Filters/PendingInviteLimiter.cs
@@ -0,0 +1,92 @@ +using System.Collections.Concurrent; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec.State.RoomInfo; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; +using MatrixContentFilter.Abstractions; +using MatrixContentFilter.EventTypes; +using MatrixContentFilter.Services; +using MatrixContentFilter.Services.AsyncActionQueues; + +namespace MatrixContentFilter.Handlers.Filters; + +public class PendingInviteLimiter( + ConfigurationService cfgService, + AuthenticatedHomeserverGeneric hs, + AsyncMessageQueue msgQueue, + InfoCacheService infoCache, + AbstractAsyncActionQueue actionQueue, + MatrixContentFilterMetrics metrics) + : IContentFilter { + public override Task ProcessSyncAsync(SyncResponse syncResponse) { + if (syncResponse.Rooms?.Join is null) return Task.CompletedTask; + var tasks = syncResponse.Rooms.Join.Select(ProcessRoomAsync); + return Task.WhenAll(tasks); + } + + private Task ProcessRoomAsync(KeyValuePair<string, SyncResponse.RoomsDataStructure.JoinedRoomDataStructure> syncRoom) { + var (roomId, roomData) = syncRoom; + if (roomId == cfgService.LogRoom.RoomId || roomId == cfgService.ControlRoom.RoomId) return Task.CompletedTask; + if (roomData.Timeline?.Events is null) return Task.CompletedTask; + var config = cfgService.RoomConfigurationOverrides.GetValueOrDefault(roomId)?.InviteLimiter; + + var room = hs.GetRoom(roomId); + + var tasks = roomData.Timeline.Events.Select(msg => ProcessEventAsync(room, msg, config)).ToList(); + return Task.WhenAll(tasks); + } + + public override Task ProcessEventListAsync(List<StateEventResponse> events) { + var tasks = events.GroupBy(x => x.RoomId).Select(async x => { + var room = hs.GetRoom(x.Key); + var config = cfgService.RoomConfigurationOverrides.GetValueOrDefault(x.Key)?.InviteLimiter; + var tasks = x.Select(msg => ProcessEventAsync(room, msg, config)).ToList(); + await Task.WhenAll(tasks); + }).ToList(); + + return Task.WhenAll(tasks); + } + + // key format: roomid:sender + private ConcurrentDictionary<string, int> HeuristicInviteCount = new(); + private async Task ProcessEventAsync(GenericRoom room, StateEventResponse evt, FilterConfiguration.BasicLimiterConfiguration? roomConfiguration) { + if (evt.Type != "m.room.member") return; + var content = evt.TypedContent as RoomMemberEventContent; + if (content?.Membership != "invite") return; + metrics.Increment("pending_invite_limiter_processed_event_count"); + + var key = $"{evt.RoomId}:{evt.Sender}"; + HeuristicInviteCount.AddOrUpdate(key, 1, (_, count) => count + 1); + if (HeuristicInviteCount[key] > 5) { + await actionQueue.EqueueActionAsync(evt.EventId, async () => { + var displayName = await infoCache.GetDisplayNameAsync(room.RoomId, evt.Sender!); + var roomName = await infoCache.GetRoomNameAsync(room.RoomId); + + msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice") + .WithBody("Pending invite limiter heuristic tripped for ").WithMention(evt.Sender!).WithBody(" in ").WithMention(evt.RoomId, roomName).WithBody($" ({HeuristicInviteCount[key]})!").WithNewline() + .WithBody("Updating heuristics with real counts...") + .Build()); + + var invitedMembersByInviter = (await room.GetMembersListAsync(joinedOnly: false)) + .Where(x => x.ContentAs<RoomMemberEventContent>()!.Membership == "invite") + .GroupBy(x=>x.Sender!); + + foreach (var sender in invitedMembersByInviter) { + HeuristicInviteCount.AddOrUpdate($"{room.RoomId}:{sender.Key}", sender.Count(), (_, count) => count + sender.Count()); + msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice") + .WithBody("Updated heuristic count for ").WithMention(sender.Key).WithBody(" in ").WithMention(room.RoomId, roomName).WithBody($" ({HeuristicInviteCount[key]})!").WithNewline() + .Build()); + } + + // msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice") + // .WithBody("Invite from ").WithMention(evt.Sender, displayName).WithBody(" in ").WithMention(room.RoomId, roomName).WithBody(" was rejected!").WithNewline() + // .WithCollapsibleSection("Message data", msb => msb.WithCodeBlock(content.ToJson(ignoreNull: true), "json")) + // .Build()); + }); + } + ActionCount++; + } +} \ No newline at end of file diff --git a/MatrixContentFilter/Handlers/Filters/VideoFilter.cs b/MatrixContentFilter/Handlers/Filters/VideoFilter.cs new file mode 100644
index 0000000..16a8639 --- /dev/null +++ b/MatrixContentFilter/Handlers/Filters/VideoFilter.cs
@@ -0,0 +1,28 @@ +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.Homeservers; +using LibMatrix.Responses; +using LibMatrix.RoomTypes; +using MatrixContentFilter.Abstractions; +using MatrixContentFilter.EventTypes; +using MatrixContentFilter.Handlers.Filters.Generic; +using MatrixContentFilter.Services; +using MatrixContentFilter.Services.AsyncActionQueues; + +namespace MatrixContentFilter.Handlers.Filters; + +public class VideoFilter : BasicMessageTypeFilter { + public VideoFilter(ConfigurationService cfgService, + AuthenticatedHomeserverGeneric hs, + AsyncMessageQueue msgQueue, + InfoCacheService infoCache, + AbstractAsyncActionQueue actionQueue, + MatrixContentFilterMetrics metrics + ) : base(cfgService, hs, msgQueue, infoCache, actionQueue, metrics) { + MessageType = "m.video"; + MessageTypeName = "video"; + MessageTypeNamePlural = "videos"; + } +} \ No newline at end of file diff --git a/MatrixContentFilter/Handlers/InviteHandler.cs b/MatrixContentFilter/Handlers/InviteHandler.cs
index 75e5506..2883529 100644 --- a/MatrixContentFilter/Handlers/InviteHandler.cs +++ b/MatrixContentFilter/Handlers/InviteHandler.cs
@@ -1,21 +1,46 @@ using LibMatrix.EventTypes.Spec; using LibMatrix.Helpers; using LibMatrix.Utilities.Bot.Services; +using MatrixContentFilter.Services; +using Microsoft.Extensions.Logging; namespace MatrixContentFilter.Handlers; -public static class InviteHandler { - public static async Task HandleAsync(InviteHandlerHostedService.InviteEventArgs invite) { +public class InviteHandler(ILogger<InviteHandler> logger, ConfigurationService cfg, InfoCacheService infoCache) : InviteHandlerHostedService.IInviteHandler { + public async Task HandleInviteAsync(InviteHandlerHostedService.InviteEventArgs invite) { var room = invite.Homeserver.GetRoom(invite.RoomId); - if (!invite.MemberEvent.Sender!.EndsWith("rory.gay")) { + var controlRoom = invite.Homeserver.GetRoom(cfg.ControlRoom.RoomId); + var logRoom = invite.Homeserver.GetRoom(cfg.LogRoom.RoomId); + + await logRoom.SendMessageEventAsync(new MessageBuilder().WithBody($"Processing invite to {invite.RoomId}...").Build()); + + var roomName = invite.RoomId; + var inviterName = await infoCache.GetDisplayNameAsync(invite.RoomId, invite.MemberEvent.Sender!); + try { + roomName = await infoCache.GetRoomNameAsync(invite.RoomId); + } + catch (Exception) { + logger.LogWarning("Failed to get room name for {RoomId}", invite.RoomId); + } + + var controlRoomMembers = await controlRoom.GetMembersListAsync(joinedOnly: true); + + if (!controlRoomMembers.Any(m => m.StateKey != invite.MemberEvent.Sender)) { await room.LeaveAsync($"{invite.MemberEvent.Sender} is not allowed to invite this bot!"); + await controlRoom.SendMessageEventAsync(new MessageBuilder() + .WithColoredBody("#FF0000", msb => + msb.WithMention(invite.MemberEvent.Sender!, inviterName).WithBody(" attempted to invite the bot to ").WithMention(invite.RoomId, roomName).WithBody(", but is not a member of the control room!") + .WithNewline().WithBody("If you trust this room, the bot should be invited by anyone who can read this message!") + ) + .Build()); return; } try { await room.JoinAsync(reason: $"I was invited by {invite.MemberEvent.Sender}"); await room.SendMessageEventAsync(new RoomMessageEventContent("m.notice", "Hello! I've arrived!")); - } catch (Exception e) { + } + catch (Exception e) { var newroom = await invite.Homeserver.CreateRoom(new() { Name = $"Join error report", Invite = [invite.MemberEvent.Sender]