diff --git a/MatrixContentFilter/Abstractions/IContentFilter.cs b/MatrixContentFilter/Abstractions/IContentFilter.cs
index 108030b..8bd01ff 100644
--- a/MatrixContentFilter/Abstractions/IContentFilter.cs
+++ b/MatrixContentFilter/Abstractions/IContentFilter.cs
@@ -1,12 +1,15 @@
using System.Diagnostics;
using LibMatrix;
using LibMatrix.Responses;
-using MatrixContentFilter.EventTypes;
namespace MatrixContentFilter.Abstractions;
-public abstract class IContentFilter
-{
+public abstract class IContentFilter {
+ private protected abstract string GetFilterName();
+ private protected abstract string GetEventTypeName();
+ private protected abstract string? GetEventSubtypeName();
+ private protected abstract string? GetMetricsPrefix();
+
public virtual Task ProcessSyncAsync(SyncResponse syncEvent) {
var type = this.GetType().FullName;
Console.WriteLine($"WARNING: {type} does not implement ProcessSyncAsync(SyncResponse syncEvent)");
@@ -24,4 +27,6 @@ public abstract class IContentFilter
}
public int ActionCount { get; set; } = 0;
+
+
}
\ No newline at end of file
diff --git a/MatrixContentFilter/Commands/BanCommand.cs b/MatrixContentFilter/Commands/BanCommand.cs
new file mode 100644
index 0000000..0ae38e2
--- /dev/null
+++ b/MatrixContentFilter/Commands/BanCommand.cs
@@ -0,0 +1,97 @@
+using LibMatrix.Helpers;
+using LibMatrix.RoomTypes;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MatrixContentFilter.Services;
+using MatrixContentFilter.Services.AsyncActionQueues;
+
+namespace MatrixContentFilter.Commands;
+
+public class BanCommand(
+ ConfigurationService filterConfigService,
+ AsyncMessageQueue msgQueue,
+ InfoCacheService infoCache,
+ ConfigurationService cfgService,
+ AbstractAsyncActionQueue actionQueue
+) : ICommand {
+ public string Name { get; } = "ban";
+ public string[]? Aliases { get; } = [];
+ public string Description { get; } = "Ban user (see --help)";
+ public bool Unlisted { get; } = false;
+
+ public async Task Invoke(CommandContext ctx) {
+ bool HasMoreArgs(int index) => ctx.Args.Length > index + 1;
+ if (ctx.Args.Contains("--help") || ctx.Args.Length == 0) {
+ await SendHelp(ctx.Room);
+ return;
+ }
+
+ BanCommandArgs args = new() {
+ UserId = null,
+ Reason = null
+ };
+
+ // if (ctx.Args.Contains("--redact")) {
+ // var index = Array.IndexOf(ctx.Args, "--redact");
+ // if (HasMoreArgs(index)) {
+ // args.Redact = int.TryParse(ctx.Args[index + 1], out var redact) ? redact : 500;
+ // }
+ // }
+
+ for (int i = 0; i < ctx.Args.Length; i++) {
+ if (ctx.Args[i].StartsWith("--")) {
+ switch (ctx.Args[i]) {
+ case "--redact": {
+ if (HasMoreArgs(i)) {
+ // args.Redact = int.TryParse(ctx.Args[i + 1], out var redact) ? redact : 500;
+ if (int.TryParse(ctx.Args[i + 1], out var redact)) {
+ args.Redact = redact;
+ i++;
+ }
+ else args.Redact = 500;
+ }
+
+ break;
+ }
+ }
+ }
+ else if (args.UserId == null) {
+ args.UserId = ctx.Args[i];
+ }
+ else args.Reason += ctx.Args[i] + " ";
+ }
+ }
+
+ private async Task SendHelp(GenericRoom room) {
+ string[][] helpTable = [
+ ["--help", "", "Show this message"],
+ ["--redact", "count ?? 500", "Redact last N events from user"],
+ ["user_id", "required", "User ID to ban"],
+ ["reason", "required", "Reason for ban"],
+ ];
+ var msb = new MessageBuilder("m.notice")
+ .WithTable(tb => {
+ tb.WithTitle("Help for ban command", 3)
+ .WithTitle("Basic usage: ban [options] user_id reason", 3)
+ .WithRow(rb =>
+ rb.WithCell("Command")
+ .WithCell("Arguments")
+ .WithCell("Description")
+ );
+ foreach (var row in helpTable) {
+ tb.WithRow(rb =>
+ rb.WithCell(row[0])
+ .WithCell(row[1])
+ .WithCell(row[2])
+ );
+ }
+ });
+
+ await room.SendMessageEventAsync(msb.Build());
+ }
+
+ private struct BanCommandArgs {
+ public string UserId { get; set; }
+ public string? Reason { get; set; }
+ public int? Redact { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/MatrixContentFilter/Commands/CheckHistoryCommand.cs b/MatrixContentFilter/Commands/CheckHistoryCommand.cs
index 1e98545..4d34ee6 100644
--- a/MatrixContentFilter/Commands/CheckHistoryCommand.cs
+++ b/MatrixContentFilter/Commands/CheckHistoryCommand.cs
@@ -1,9 +1,7 @@
using LibMatrix.Helpers;
-using LibMatrix.Homeservers;
using LibMatrix.Utilities.Bot.Interfaces;
using MatrixContentFilter.Abstractions;
using MatrixContentFilter.Services;
-using Microsoft.Extensions.Logging;
namespace MatrixContentFilter.Commands;
diff --git a/MatrixContentFilter/Commands/ConfigureCommand.cs b/MatrixContentFilter/Commands/ConfigureCommand.cs
index 756fc14..472e387 100644
--- a/MatrixContentFilter/Commands/ConfigureCommand.cs
+++ b/MatrixContentFilter/Commands/ConfigureCommand.cs
@@ -1,4 +1,3 @@
-using System.Text;
using LibMatrix.EventTypes.Spec;
using LibMatrix.Utilities.Bot.Commands;
using LibMatrix.Utilities.Bot.Interfaces;
diff --git a/MatrixContentFilter/Commands/DumpEventCommand.cs b/MatrixContentFilter/Commands/DumpEventCommand.cs
index 5131e19..ebffcd4 100644
--- a/MatrixContentFilter/Commands/DumpEventCommand.cs
+++ b/MatrixContentFilter/Commands/DumpEventCommand.cs
@@ -1,14 +1,8 @@
using ArcaneLibs.Extensions;
-using LibMatrix.EventTypes.Spec;
-using LibMatrix.EventTypes.Spec.State;
-using LibMatrix.Filters;
using LibMatrix.Helpers;
-using LibMatrix.Homeservers;
using LibMatrix.Utilities.Bot.Interfaces;
-using MatrixContentFilter.Abstractions;
using MatrixContentFilter.Services;
using MatrixContentFilter.Services.AsyncActionQueues;
-using Microsoft.Extensions.Logging;
namespace MatrixContentFilter.Commands;
diff --git a/MatrixContentFilter/Commands/GetConfigCommand.cs b/MatrixContentFilter/Commands/GetConfigCommand.cs
index bac00ca..ebd059c 100644
--- a/MatrixContentFilter/Commands/GetConfigCommand.cs
+++ b/MatrixContentFilter/Commands/GetConfigCommand.cs
@@ -1,12 +1,8 @@
-using System.Text;
using ArcaneLibs.Attributes;
using ArcaneLibs.Extensions;
-using LibMatrix.EventTypes.Spec;
using LibMatrix.Helpers;
-using LibMatrix.Utilities.Bot.Commands;
using LibMatrix.Utilities.Bot.Interfaces;
using MatrixContentFilter.EventTypes;
-using Microsoft.Extensions.DependencyInjection;
namespace MatrixContentFilter.Commands;
diff --git a/MatrixContentFilter/Commands/NewRoomCommand.cs b/MatrixContentFilter/Commands/NewRoomCommand.cs
index b8becd4..916649e 100644
--- a/MatrixContentFilter/Commands/NewRoomCommand.cs
+++ b/MatrixContentFilter/Commands/NewRoomCommand.cs
@@ -1,12 +1,4 @@
-using System.Text;
-using ArcaneLibs.Attributes;
-using ArcaneLibs.Extensions;
-using LibMatrix.EventTypes.Spec;
-using LibMatrix.Helpers;
-using LibMatrix.Utilities.Bot.Commands;
using LibMatrix.Utilities.Bot.Interfaces;
-using MatrixContentFilter.EventTypes;
-using Microsoft.Extensions.DependencyInjection;
namespace MatrixContentFilter.Commands;
diff --git a/MatrixContentFilter/Commands/RedactCommand.cs b/MatrixContentFilter/Commands/RedactCommand.cs
index 6b2f8b6..9d45f18 100644
--- a/MatrixContentFilter/Commands/RedactCommand.cs
+++ b/MatrixContentFilter/Commands/RedactCommand.cs
@@ -1,14 +1,11 @@
using ArcaneLibs.Extensions;
using LibMatrix.EventTypes.Spec;
-using LibMatrix.EventTypes.Spec.State;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Filters;
using LibMatrix.Helpers;
-using LibMatrix.Homeservers;
using LibMatrix.Utilities.Bot.Interfaces;
-using MatrixContentFilter.Abstractions;
using MatrixContentFilter.Services;
using MatrixContentFilter.Services.AsyncActionQueues;
-using Microsoft.Extensions.Logging;
namespace MatrixContentFilter.Commands;
@@ -54,11 +51,11 @@ public class RedactCommand(
Key = "\u23f3" //hour glass emoji
}
});
-
- await foreach (var resp in ctx.Room.GetManyMessagesAsync(limit: count, chunkSize: Math.Min(count, 250)
- ,filter: new SyncFilter.RoomFilter.StateFilter(types: [RoomMemberEventContent.EventId, RoomMessageEventContent.EventId], senders: [mxid])
+
+ await foreach (var resp in ctx.Room.GetManyMessagesAsync(limit: count, chunkSize: Math.Min(count, 250),
+ filter: new SyncFilter.RoomFilter.StateFilter(types: [RoomMemberEventContent.EventId, RoomMessageEventContent.EventId], senders: [mxid])
.ToJson(indent: false, ignoreNull: true).UrlEncode())
- ) {
+ ) {
foreach (var msg in resp.Chunk) {
if (msg.Sender != mxid) continue;
if (msg is not { Type: RoomMemberEventContent.EventId or RoomMessageEventContent.EventId }) continue;
@@ -85,7 +82,7 @@ public class RedactCommand(
.Build());
});
}
-
+
await ctx.Room.RedactEventAsync(hourglassReaction.EventId);
await ctx.Room.SendTimelineEventAsync("m.reaction", new RoomMessageReactionEventContent() {
RelatesTo = new() {
diff --git a/MatrixContentFilter/EventTypes/FilterConfiguration.cs b/MatrixContentFilter/EventTypes/FilterConfiguration.cs
index 8e3b900..78e164a 100644
--- a/MatrixContentFilter/EventTypes/FilterConfiguration.cs
+++ b/MatrixContentFilter/EventTypes/FilterConfiguration.cs
@@ -28,11 +28,26 @@ public class FilterConfiguration : EventContent {
[FriendlyName(Name = "Links")]
public BasicFilterConfiguration? UrlFilter { get; set; }
+ [JsonPropertyName("invite_limiter")]
+ [FriendlyName(Name = "Pending invite limits")]
+ public BasicLimiterConfiguration? InviteLimiter { get; set; }
+
[JsonPropertyName("ignored_users")]
[FriendlyName(Name = "Ignored Users")]
public List<string>? IgnoredUsers { get; set; }
-
-
+
+
+ public class BasicLimiterConfiguration {
+ [JsonPropertyName("heuristic_trip_point")]
+ public int? HeuristicTripPoint { get; set; }
+
+ [JsonPropertyName("max_count")]
+ public int? MaxCount { get; set; }
+
+ [JsonPropertyName("ignored_users")]
+ public List<string>? IgnoredUsers { get; set; }
+ }
+
public class BasicFilterConfiguration {
[JsonPropertyName("allowed")]
public bool? Allowed { get; set; }
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]
diff --git a/MatrixContentFilter/MatrixContentFilter.csproj b/MatrixContentFilter/MatrixContentFilter.csproj
index ea0ddd1..1a26b13 100644
--- a/MatrixContentFilter/MatrixContentFilter.csproj
+++ b/MatrixContentFilter/MatrixContentFilter.csproj
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net8.0</TargetFramework>
+ <TargetFramework>net9.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -22,7 +22,12 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
+ <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
+ <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
+ <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
+ <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.9.0" />
+ <PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.9.0" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings*.json">
diff --git a/MatrixContentFilter/MatrixContentFilterConfiguration.cs b/MatrixContentFilter/MatrixContentFilterConfiguration.cs
index 57537f0..fb7e8f3 100644
--- a/MatrixContentFilter/MatrixContentFilterConfiguration.cs
+++ b/MatrixContentFilter/MatrixContentFilterConfiguration.cs
@@ -10,9 +10,24 @@ public class MatrixContentFilterConfiguration {
public string AppMode { get; set; } = "bot";
public string AsyncQueueImplementation { get; set; } = "lifo";
-
+ public SanityCheckConfiguration SanityCheck { get; set; } = new();
+ public OpenTelemetryConfiguration OpenTelemetry { get; set; } = new();
+
public class ConcurrencyLimitsConfiguration {
public int Redactions { get; set; } = 1;
public int LogMessages { get; set; } = 1;
}
+
+ public class SanityCheckConfiguration {
+ public bool Enabled { get; set; } = false;
+ public int MaxConcurrency { get; set; } = 1;
+ public TimeSpan Interval { get; set; } = TimeSpan.FromMinutes(5);
+ }
+
+ public class OpenTelemetryConfiguration {
+ public bool Enabled { get; set; } = false;
+ public string Endpoint { get; set; }
+ public string ServiceName { get; set; }
+ public string Environment { get; set; }
+ }
}
diff --git a/MatrixContentFilter/Program.cs b/MatrixContentFilter/Program.cs
index 7eec930..b849131 100644
--- a/MatrixContentFilter/Program.cs
+++ b/MatrixContentFilter/Program.cs
@@ -1,3 +1,4 @@
+using ArcaneLibs.Extensions;
using MatrixContentFilter;
using MatrixContentFilter.Handlers;
using LibMatrix.Services;
@@ -8,63 +9,91 @@ using MatrixContentFilter.Services;
using MatrixContentFilter.Services.AsyncActionQueues;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Metrics;
using Microsoft.Extensions.Hosting;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
var builder = Host.CreateDefaultBuilder(args);
-builder.ConfigureHostOptions(host => {
- host.ServicesStartConcurrently = true;
- host.ServicesStopConcurrently = true;
- host.ShutdownTimeout = TimeSpan.FromSeconds(5);
-});
-
if (Environment.GetEnvironmentVariable("MATRIXCONTENTFILTER_APPSETTINGS_PATH") is string path)
builder.ConfigureAppConfiguration(x => x.AddJsonFile(path));
var host = builder.ConfigureServices((ctx, services) => {
- var config = new MatrixContentFilterConfiguration(ctx.Configuration);
- services.AddSingleton<MatrixContentFilterConfiguration>(config);
+ var config = new MatrixContentFilterConfiguration(ctx.Configuration);
+ Console.WriteLine("Configuration: {0}", config.ToJson());
+ services.AddSingleton<MatrixContentFilterConfiguration>(config);
+
+ services.AddMetrics(m => m.AddDebugConsole());
+ services.AddSingleton<MatrixContentFilterMetrics>();
+
+ if (config.OpenTelemetry.Enabled) {
+ services.AddOpenTelemetry()
+ .ConfigureResource(resource => resource.AddService("MatrixContentFilter"))
+ .WithTracing(tracing => tracing
+ .AddAspNetCoreInstrumentation()
+ .AddConsoleExporter())
+ .WithMetrics(metrics => metrics
+ .AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation()
+ .AddConsoleExporter());
+ }
+
+ services.AddRoryLibMatrixServices(new() {
+ AppName = "MatrixContentFilter"
+ });
- services.AddRoryLibMatrixServices(new() {
- AppName = "MatrixContentFilter"
- });
+ services.AddMatrixBot().AddCommandHandler().DiscoverAllCommands()
+ .WithInviteHandler<InviteHandler>()
+ .WithCommandResultHandler(CommandResultHandler.HandleAsync);
- services.AddMatrixBot().AddCommandHandler().DiscoverAllCommands()
- .WithInviteHandler(InviteHandler.HandleAsync)
- .WithCommandResultHandler(CommandResultHandler.HandleAsync);
+ services.AddSingleton<InfoCacheService>();
- services.AddSingleton<InfoCacheService>();
+ services.AddSingleton<IContentFilter, ImageFilter>();
+ services.AddSingleton<IContentFilter, VideoFilter>();
+ services.AddSingleton<IContentFilter, PendingInviteLimiter>();
- services.AddSingleton<IContentFilter, ImageFilter>();
+ services.AddSingleton<ConfigurationService>();
+ services.AddSingleton<IHostedService, ConfigurationService>(s => s.GetRequiredService<ConfigurationService>());
+ services.AddSingleton<AsyncMessageQueue>();
+ services.AddSingleton<IHostedService, AsyncMessageQueue>(s => s.GetRequiredService<AsyncMessageQueue>());
- services.AddSingleton<ConfigurationService>();
- services.AddSingleton<IHostedService, ConfigurationService>(s => s.GetRequiredService<ConfigurationService>());
- services.AddSingleton<AsyncMessageQueue>();
- services.AddSingleton<IHostedService, AsyncMessageQueue>(s => s.GetRequiredService<AsyncMessageQueue>());
+ switch (config.AppMode) {
+ case "bot":
+ services.AddHostedService<MatrixContentFilterBot>();
+ if (config.SanityCheck.Enabled)
+ services.AddHostedService<BotModeSanityCheckService>();
+ break;
+ default:
+ throw new NotSupportedException($"Unknown app mode: {config.AppMode}");
+ }
- switch (config.AppMode) {
- case "bot":
- services.AddHostedService<MatrixContentFilterBot>();
- // services.AddHostedService<BotModeSanityCheckService>();
- break;
- default:
- throw new NotSupportedException($"Unknown app mode: {config.AppMode}");
- }
-
- switch (config.AsyncQueueImplementation) {
- case "lifo":
- services.AddSingleton<LiFoAsyncActionQueue>();
- services.AddSingleton<AbstractAsyncActionQueue, LiFoAsyncActionQueue>(s => s.GetRequiredService<LiFoAsyncActionQueue>());
- services.AddSingleton<IHostedService, LiFoAsyncActionQueue>(s => s.GetRequiredService<LiFoAsyncActionQueue>());
- break;
- case "fifo":
- services.AddSingleton<FiFoAsyncActionQueue>();
- services.AddSingleton<AbstractAsyncActionQueue, FiFoAsyncActionQueue>(s => s.GetRequiredService<FiFoAsyncActionQueue>());
- services.AddSingleton<IHostedService, FiFoAsyncActionQueue>(s => s.GetRequiredService<FiFoAsyncActionQueue>());
- break;
- default:
- throw new NotSupportedException($"Unknown async queue implementation: {config.AsyncQueueImplementation}");
- }
-}).UseConsoleLifetime().Build();
+ switch (config.AsyncQueueImplementation) {
+ case "lifo":
+ services.AddSingleton<LiFoAsyncActionQueue>();
+ services.AddSingleton<AbstractAsyncActionQueue, LiFoAsyncActionQueue>(s => s.GetRequiredService<LiFoAsyncActionQueue>());
+ services.AddSingleton<IHostedService, LiFoAsyncActionQueue>(s => s.GetRequiredService<LiFoAsyncActionQueue>());
+ break;
+ case "fifo":
+ services.AddSingleton<FiFoAsyncActionQueue>();
+ services.AddSingleton<AbstractAsyncActionQueue, FiFoAsyncActionQueue>(s => s.GetRequiredService<FiFoAsyncActionQueue>());
+ services.AddSingleton<IHostedService, FiFoAsyncActionQueue>(s => s.GetRequiredService<FiFoAsyncActionQueue>());
+ break;
+ default:
+ throw new NotSupportedException($"Unknown async queue implementation: {config.AsyncQueueImplementation}");
+ }
+ })
+ // .ConfigureLogging((ctx, lb) => {
+ // lb.AddOpenTelemetry(options => {
+ // options
+ // .SetResourceBuilder(
+ // ResourceBuilder.CreateDefault()
+ // .AddService("MatrixContentFilter"))
+ // .AddConsoleExporter();
+ // });
+ // })
+ .UseConsoleLifetime().Build();
await host.RunAsync();
\ No newline at end of file
diff --git a/MatrixContentFilter/Properties/launchSettings.json b/MatrixContentFilter/Properties/launchSettings.json
index 997e294..b0aa56b 100644
--- a/MatrixContentFilter/Properties/launchSettings.json
+++ b/MatrixContentFilter/Properties/launchSettings.json
@@ -21,6 +21,13 @@
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Local"
}
+ },
+ "MatrixUnitTests": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "mut"
+ }
}
}
}
diff --git a/MatrixContentFilter/Services/AsyncActionQueues/AbstractionAsyncActionQueue.cs b/MatrixContentFilter/Services/AsyncActionQueues/AbstractionAsyncActionQueue.cs
index f4c559c..945d264 100644
--- a/MatrixContentFilter/Services/AsyncActionQueues/AbstractionAsyncActionQueue.cs
+++ b/MatrixContentFilter/Services/AsyncActionQueues/AbstractionAsyncActionQueue.cs
@@ -1,7 +1,6 @@
using System.Collections.Concurrent;
using System.Threading.Channels;
using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
namespace MatrixContentFilter.Services.AsyncActionQueues;
diff --git a/MatrixContentFilter/Services/AsyncActionQueues/FiFoAsyncActionQueue.cs b/MatrixContentFilter/Services/AsyncActionQueues/FiFoAsyncActionQueue.cs
index 3d7c90d..8c67c77 100644
--- a/MatrixContentFilter/Services/AsyncActionQueues/FiFoAsyncActionQueue.cs
+++ b/MatrixContentFilter/Services/AsyncActionQueues/FiFoAsyncActionQueue.cs
@@ -1,6 +1,4 @@
-using System.Collections.Concurrent;
using System.Threading.Channels;
-using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MatrixContentFilter.Services.AsyncActionQueues;
diff --git a/MatrixContentFilter/Services/BotModeSanityCheckService.cs b/MatrixContentFilter/Services/BotModeSanityCheckService.cs
index 55fe9e8..0a99ca1 100644
--- a/MatrixContentFilter/Services/BotModeSanityCheckService.cs
+++ b/MatrixContentFilter/Services/BotModeSanityCheckService.cs
@@ -1,13 +1,8 @@
-using System.Diagnostics;
-using ArcaneLibs;
using ArcaneLibs.Extensions;
-using LibMatrix;
using LibMatrix.Filters;
using LibMatrix.Helpers;
using LibMatrix.Homeservers;
-using LibMatrix.Responses;
using MatrixContentFilter.Abstractions;
-using MatrixContentFilter.Handlers.Filters;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -18,19 +13,22 @@ public class BotModeSanityCheckService(
AuthenticatedHomeserverGeneric hs,
ConfigurationService filterConfigService,
IEnumerable<IContentFilter> filters,
- AsyncMessageQueue msgQueue
+ AsyncMessageQueue msgQueue,
+ MatrixContentFilterConfiguration cfg
) : BackgroundService {
/// <summary>Triggered when the application host is ready to start the service.</summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
protected override async Task ExecuteAsync(CancellationToken cancellationToken) {
+ var semaphore = new SemaphoreSlim(cfg.SanityCheck.MaxConcurrency);
while (!cancellationToken.IsCancellationRequested) {
- await Task.Delay(10000, cancellationToken);
+ await Task.Delay(cfg.SanityCheck.Interval, cancellationToken);
var rooms = await hs.GetJoinedRooms();
rooms.RemoveAll(x => x.RoomId == filterConfigService.LogRoom.RoomId);
rooms.RemoveAll(x => x.RoomId == filterConfigService.ControlRoom.RoomId);
var timelineFilter = new SyncFilter.RoomFilter.StateFilter(notTypes: ["m.room.redaction"], limit: 5000);
var timelines = rooms.Select(async x => {
+ await semaphore.WaitAsync(cancellationToken);
var room = hs.GetRoom(x.RoomId);
// var sync = await room.GetMessagesAsync(null, 1500, filter: timelineFilter.ToJson(ignoreNull: true, indent: false).UrlEncode());
var iter = room.GetManyMessagesAsync(null, 5000, filter: timelineFilter.ToJson(ignoreNull: true, indent: false).UrlEncode(), chunkSize: 250);
@@ -49,6 +47,8 @@ public class BotModeSanityCheckService(
await tasks;
}
+
+ semaphore.Release();
}).ToList();
await Task.WhenAll(timelines);
}
diff --git a/MatrixContentFilter/Services/ConfigurationService.cs b/MatrixContentFilter/Services/ConfigurationService.cs
index f83c89a..cd2a93d 100644
--- a/MatrixContentFilter/Services/ConfigurationService.cs
+++ b/MatrixContentFilter/Services/ConfigurationService.cs
@@ -1,6 +1,6 @@
using ArcaneLibs.Extensions;
using LibMatrix;
-using LibMatrix.EventTypes.Spec.State;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Helpers;
using LibMatrix.Homeservers;
using LibMatrix.Responses;
@@ -28,7 +28,7 @@ public class ConfigurationService(ILogger<ConfigurationService> logger, Authenti
await foreach (var sync in syncHelper.EnumerateSyncAsync(stoppingToken).WithCancellation(stoppingToken)) {
if (sync is { AccountData: null, Rooms: null }) continue;
- logger.LogInformation("Received configuration update: {syncData}", sync.ToJson(ignoreNull: true));
+ logger.LogInformation("Received configuration update...");
await OnSyncReceived(sync);
}
}
@@ -165,6 +165,11 @@ public class ConfigurationService(ILogger<ConfigurationService> logger, Authenti
_filterConfiguration.UrlFilter ??= new();
_filterConfiguration.UrlFilter.IgnoredUsers ??= Log("url_filter->ignored_users", (List<string>) []);
_filterConfiguration.UrlFilter.Allowed ??= Log("url_filter->allowed", false);
+
+ _filterConfiguration.InviteLimiter ??= new();
+ _filterConfiguration.InviteLimiter.IgnoredUsers ??= Log("invite_limiter->ignored_users", (List<string>) []);
+ _filterConfiguration.InviteLimiter.HeuristicTripPoint ??= Log("invite_limiter->heuristic_trip_point", 5);
+ _filterConfiguration.InviteLimiter.MaxCount ??= Log("invite_limiter->max_count", 15);
if (changes.Count > 0) {
await hs.SetAccountDataAsync(FilterConfiguration.EventId, _filterConfiguration);
diff --git a/MatrixContentFilter/Services/InfoCacheService.cs b/MatrixContentFilter/Services/InfoCacheService.cs
index 974e873..5f7ab10 100644
--- a/MatrixContentFilter/Services/InfoCacheService.cs
+++ b/MatrixContentFilter/Services/InfoCacheService.cs
@@ -1,5 +1,6 @@
using ArcaneLibs.Collections;
-using LibMatrix.EventTypes.Spec.State;
+using LibMatrix;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Homeservers;
namespace MatrixContentFilter.Services;
@@ -11,8 +12,14 @@ public class InfoCacheService(AuthenticatedHomeserverGeneric hs) {
public async Task<string> GetDisplayNameAsync(string roomId, string userId) =>
await DisplayNameCache.GetOrAdd($"{roomId}\t{userId}", async () => {
var room = hs.GetRoom(roomId);
- var userState = await room.GetStateAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, userId);
- if (!string.IsNullOrWhiteSpace(userState?.DisplayName)) return userState.DisplayName;
+ try {
+ var userState = await room.GetStateOrNullAsync<RoomMemberEventContent>(RoomMemberEventContent.EventId, userId);
+ if (!string.IsNullOrWhiteSpace(userState?.DisplayName)) return userState.DisplayName;
+ }
+ catch (MatrixException e) {
+ if (e is not { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND or MatrixException.ErrorCodes.M_FORBIDDEN })
+ throw;
+ }
var user = await hs.GetProfileAsync(userId);
if (!string.IsNullOrWhiteSpace(user?.DisplayName)) return user.DisplayName;
diff --git a/MatrixContentFilter/Services/MatrixContentFilterBot.cs b/MatrixContentFilter/Services/MatrixContentFilterBot.cs
index 321cdd4..12fa5f6 100644
--- a/MatrixContentFilter/Services/MatrixContentFilterBot.cs
+++ b/MatrixContentFilter/Services/MatrixContentFilterBot.cs
@@ -6,7 +6,6 @@ using LibMatrix.Helpers;
using LibMatrix.Homeservers;
using LibMatrix.Responses;
using MatrixContentFilter.Abstractions;
-using MatrixContentFilter.Handlers.Filters;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -49,7 +48,7 @@ public class MatrixContentFilterBot(
var syncFilter = new SyncFilter() {
Room = new() {
NotRooms = [filterConfigService.LogRoom.RoomId],
- Timeline = new(notTypes: ["m.room.redaction"])
+ Timeline = new(notTypes: ["m.room.redaction"], limit: 5000)
}
};
syncHelper = new SyncHelper(hs, logger) {
@@ -114,7 +113,7 @@ public class MatrixContentFilterBot(
var room = hs.GetRoom(roomId);
if (roomData.Timeline?.Limited == true) {
msgQueue.EnqueueMessageAsync(filterConfigService.LogRoom, new MessageBuilder("m.notice")
- .WithColoredBody("FF0000", $"Room {roomId} has limited timeline, fetching! The room may be getting spammed?")
+ .WithColoredBody("#FF0000", $"Room {roomId} has limited timeline, fetching! The room may be getting spammed?")
.Build());
roomData.Timeline.Events ??= [];
var newEvents = await room.GetMessagesAsync(roomData.Timeline.PrevBatch ?? "", 500, filter: timelineFilter.ToJson(ignoreNull: true, indent: false));
diff --git a/MatrixContentFilter/Services/MatrixContentFilterMetrics.cs b/MatrixContentFilter/Services/MatrixContentFilterMetrics.cs
new file mode 100644
index 0000000..ff88cad
--- /dev/null
+++ b/MatrixContentFilter/Services/MatrixContentFilterMetrics.cs
@@ -0,0 +1,16 @@
+using System.Collections.Frozen;
+using System.Diagnostics.Metrics;
+
+namespace MatrixContentFilter.Services;
+
+public class MatrixContentFilterMetrics {
+ private readonly Meter meter = new Meter("MatrixContentFilter");
+ private FrozenDictionary<string, Counter<int>> _counters = FrozenDictionary<string, Counter<int>>.Empty;
+
+ public void Increment(string counter, int value = 1) {
+ if(!_counters.TryGetValue(counter, out var c)) {
+ c = meter.CreateCounter<int>(counter);
+ _counters = _counters.Concat([new KeyValuePair<string, Counter<int>>(counter, c)]).ToFrozenDictionary();
+ }
+ }
+}
\ No newline at end of file
diff --git a/MatrixContentFilter/appsettings.Development.json b/MatrixContentFilter/appsettings.Development.json
index 29f9c88..b01a410 100644
--- a/MatrixContentFilter/appsettings.Development.json
+++ b/MatrixContentFilter/appsettings.Development.json
@@ -15,10 +15,26 @@
"Prefix": "?"
},
"MatrixContentFilter": {
+ // App mode: bot or appservice
+ "AppMode": "bot",
+ // whether to use LiFo or FiFo for task queuing
+ "AsyncQueueImplementation": "lifo",
// List of people who should be invited to the control room
"Admins": [
"@emma:conduit.rory.gay",
"@emma:rory.gay"
- ]
+ ],
+ // The maximum number of redactions that can be performed concurrently
+ "ConcurrencyLimits": {
+ "Redactions": 2,
+ "LogMessages": 2
+ },
+ // Whether bot mode should be ammended with a cronjob to check for missed events
+ // Keep in mind this may add a lot of load to the server
+ "SanityCheck": {
+ "Enabled": false,
+ "MaxConcurrency": 1,
+ "Interval": "00:05:00"
+ }
}
}
|