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]
|