diff --git a/MatrixContentFilter/Commands/CheckHistoryCommand.cs b/MatrixContentFilter/Commands/CheckHistoryCommand.cs
new file mode 100644
index 0000000..1e98545
--- /dev/null
+++ b/MatrixContentFilter/Commands/CheckHistoryCommand.cs
@@ -0,0 +1,74 @@
+using LibMatrix.Helpers;
+using LibMatrix.Homeservers;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MatrixContentFilter.Abstractions;
+using MatrixContentFilter.Services;
+using Microsoft.Extensions.Logging;
+
+namespace MatrixContentFilter.Commands;
+
+public class CheckHistoryCommand(
+ ConfigurationService filterConfigService,
+ IEnumerable<IContentFilter> filters,
+ AsyncMessageQueue msgQueue,
+ InfoCacheService infoCache
+) : ICommand {
+ public string Name { get; } = "checkhistory";
+ public string[]? Aliases { get; } = ["check"];
+ public string Description { get; } = "Re-apply filters to last x messages (default: 100)";
+ public bool Unlisted { get; } = false;
+
+ public async Task Invoke(CommandContext ctx) {
+ var count = 100;
+ if (ctx.Args.Length > 0) {
+ if (!int.TryParse(ctx.Args[0], out count)) {
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder("m.notice").WithBody($"'{count}' is not a valid number!").Build());
+ return;
+ }
+ }
+
+ msgQueue.EnqueueMessageAsync(filterConfigService.LogRoom,
+ new MessageBuilder("m.notice").WithBody($"Re-applying filters to last {count} messages in ")
+ .WithMention(ctx.Room.RoomId, await infoCache.GetRoomNameAsync(ctx.Room.RoomId)).Build());
+
+ await foreach (var resp in ctx.Room.GetManyMessagesAsync(limit: count, chunkSize: Math.Min(count, 250))) {
+ foreach (var filter in filters) {
+ await filter.ProcessEventListAsync(resp.Chunk);
+ }
+ }
+ }
+
+ // /// <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) {
+ // while (!cancellationToken.IsCancellationRequested) {
+ // await Task.Delay(10000, 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 => {
+ // 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);
+ // await foreach (var sync in iter) {
+ // var tasks = Parallel.ForEachAsync(filters, async (filter, ct) => {
+ // try {
+ // Console.WriteLine("Processing filter {0} (sanity check, chunk[s={1}])", filter.GetType().FullName, sync.Chunk.Count);
+ // await filter.ProcessEventListAsync(sync.Chunk);
+ // }
+ // catch (Exception e) {
+ // logger.LogError(e, "Error processing sync with filter {filter}", filter.GetType().FullName);
+ // msgQueue.EnqueueMessageAsync(filterConfigService.LogRoom, new MessageBuilder("m.notice")
+ // .WithBody($"Error processing sync with filter {filter.GetType().FullName}: {e.Message}").Build());
+ // }
+ // });
+ //
+ // await tasks;
+ // }
+ // }).ToList();
+ // await Task.WhenAll(timelines);
+ // }
+ // }
+}
\ No newline at end of file
diff --git a/MatrixContentFilter/Commands/ConfigureCommand.cs b/MatrixContentFilter/Commands/ConfigureCommand.cs
new file mode 100644
index 0000000..756fc14
--- /dev/null
+++ b/MatrixContentFilter/Commands/ConfigureCommand.cs
@@ -0,0 +1,38 @@
+using System.Text;
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.Utilities.Bot.Commands;
+using LibMatrix.Utilities.Bot.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace MatrixContentFilter.Commands;
+
+public class ConfigureCommand(IServiceProvider svcs) : ICommandGroup {
+ public string Name { get; } = "configure";
+ public string[]? Aliases { get; } = ["config", "cfg"];
+ public string Description { get; }
+ public bool Unlisted { get; } = true;
+
+ public async Task Invoke(CommandContext ctx) {
+ var commands = svcs.GetServices<ICommand>().Where(x => x.GetType().IsAssignableTo(typeof(ICommand<>).MakeGenericType(GetType()))).ToList();
+
+ if (ctx.Args.Length == 0) {
+ await ctx.Room.SendMessageEventAsync(HelpCommand.GenerateCommandList(commands).Build());
+ }
+ else {
+ var subcommand = ctx.Args[0];
+ var command = commands.FirstOrDefault(x => x.Name == subcommand || x.Aliases?.Contains(subcommand) == true);
+ if (command == null) {
+ await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent("m.notice", "Unknown subcommand"));
+ return;
+ }
+
+ await command.Invoke(new CommandContext {
+ Room = ctx.Room,
+ MessageEvent = ctx.MessageEvent,
+ CommandName = ctx.CommandName,
+ Args = ctx.Args.Skip(1).ToArray(),
+ Homeserver = ctx.Homeserver
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/MatrixContentFilter/Commands/ConfigureSubCommands/ControlRoomConfigureSubcommand.cs b/MatrixContentFilter/Commands/ConfigureSubCommands/ControlRoomConfigureSubcommand.cs
new file mode 100644
index 0000000..5ff4f9d
--- /dev/null
+++ b/MatrixContentFilter/Commands/ConfigureSubCommands/ControlRoomConfigureSubcommand.cs
@@ -0,0 +1,18 @@
+using LibMatrix.Helpers;
+using LibMatrix.Utilities.Bot.Interfaces;
+
+namespace MatrixContentFilter.Commands.ConfigureSubCommands;
+
+public class ControlRoomConfigureSubCommand : ICommand<ConfigureCommand> {
+ public string Name { get; } = "controlroom";
+ public string[]? Aliases { get; }
+ public string Description { get; } = "Configure the control room";
+ public bool Unlisted { get; }
+
+ public async Task Invoke(CommandContext ctx) {
+ if (ctx.Args.Length == 0) {
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder("m.notice").WithBody("meow").Build());
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/MatrixContentFilter/Commands/DumpEventCommand.cs b/MatrixContentFilter/Commands/DumpEventCommand.cs
new file mode 100644
index 0000000..5131e19
--- /dev/null
+++ b/MatrixContentFilter/Commands/DumpEventCommand.cs
@@ -0,0 +1,31 @@
+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;
+
+public class DumpEventCommand(
+ ConfigurationService filterConfigService,
+ AsyncMessageQueue msgQueue,
+ InfoCacheService infoCache,
+ ConfigurationService cfgService,
+ AbstractAsyncActionQueue actionQueue
+) : ICommand {
+ public string Name { get; } = "dump";
+ public string[]? Aliases { get; } = [];
+ public string Description { get; } = "Dump event by ID";
+ public bool Unlisted { get; } = false;
+
+ public async Task Invoke(CommandContext ctx) {
+ var evt = await ctx.Room.GetEventAsync(ctx.Args[0]);
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder("m.notice").WithBody(evt.ToJson(ignoreNull: true)).Build());
+ }
+}
\ No newline at end of file
diff --git a/MatrixContentFilter/Commands/GetConfigCommand.cs b/MatrixContentFilter/Commands/GetConfigCommand.cs
new file mode 100644
index 0000000..bac00ca
--- /dev/null
+++ b/MatrixContentFilter/Commands/GetConfigCommand.cs
@@ -0,0 +1,56 @@
+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;
+
+public class GetConfigCommand(IServiceProvider svcs) : ICommand {
+ public string Name { get; } = "getconfig";
+ public string[]? Aliases { get; } = [];
+ public string Description { get; } = "Get the current configuration, optionally takes a room ID";
+ public bool Unlisted { get; } = false;
+
+ public async Task Invoke(CommandContext ctx) {
+ var room = ctx.Room;
+ if (ctx.Args.Length > 0) {
+ try {
+ room = ctx.Homeserver.GetRoom(ctx.Args[0]);
+ }
+ catch {
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder("m.notice").WithBody("Invalid room ID").Build());
+ return;
+ }
+ }
+
+ var defaults = await ctx.Homeserver.GetAccountDataAsync<FilterConfiguration>(FilterConfiguration.EventId);
+ var config = await room.GetRoomAccountDataOrNullAsync<FilterConfiguration>(FilterConfiguration.EventId);
+ var msb = new MessageBuilder("m.notice").WithColoredBody("#FFCC00", "Default configuration:")
+ .WithTable(tb => {
+ foreach (var prop in defaults.GetType().GetProperties()) {
+ var key = prop.GetFriendlyName();
+ var val = prop.GetValue(defaults);
+
+ tb = tb.WithRow(rb => {
+ rb.WithCell(key);
+ rb.WithCell(val?.ToJson() ?? "null");
+ });
+ }
+ });
+
+ if (config == null) {
+ msb = msb.WithBody("No configuration set for this room, using defaults");
+ }
+ else {
+ msb = msb.WithBody("Room overrides (additive):")
+ .WithCodeBlock(config.ToJson(ignoreNull: true), "json");
+ }
+
+ await room.SendMessageEventAsync(msb.Build());
+ }
+}
\ No newline at end of file
diff --git a/MatrixContentFilter/Commands/NewRoomCommand.cs b/MatrixContentFilter/Commands/NewRoomCommand.cs
new file mode 100644
index 0000000..b8becd4
--- /dev/null
+++ b/MatrixContentFilter/Commands/NewRoomCommand.cs
@@ -0,0 +1,24 @@
+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;
+
+public class NewRoomCommand(IServiceProvider svcs) : ICommand {
+ public string Name { get; } = "newroom";
+ public string[]? Aliases { get; } = ["nr"];
+ public string Description { get; } = "Create a new room";
+ public bool Unlisted { get; } = false;
+
+ public async Task Invoke(CommandContext ctx) {
+ await ctx.Homeserver.CreateRoom(new() {
+ Invite = [ctx.MessageEvent.Sender!]
+ });
+ }
+}
\ No newline at end of file
diff --git a/MatrixContentFilter/Commands/RedactCommand.cs b/MatrixContentFilter/Commands/RedactCommand.cs
new file mode 100644
index 0000000..6b2f8b6
--- /dev/null
+++ b/MatrixContentFilter/Commands/RedactCommand.cs
@@ -0,0 +1,99 @@
+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;
+
+public class RedactCommand(
+ ConfigurationService filterConfigService,
+ AsyncMessageQueue msgQueue,
+ InfoCacheService infoCache,
+ ConfigurationService cfgService,
+ AbstractAsyncActionQueue actionQueue
+) : ICommand {
+ public string Name { get; } = "redact";
+ public string[]? Aliases { get; } = [];
+ public string Description { get; } = "Redact last x messages from user (default: 500)";
+ public bool Unlisted { get; } = false;
+
+ public async Task Invoke(CommandContext ctx) {
+ var count = 500;
+
+ if (ctx.Args.Length == 0) {
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder("m.notice")
+ .WithBody("Please provide a user ID to redact messages from. (Make sure that it isn't formatted! Do not autocomplete!)").Build());
+ return;
+ }
+
+ var mxid = ctx.Args[0];
+ if (ctx.Args.Length > 1) {
+ if (!int.TryParse(ctx.Args[1], out count)) {
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder("m.notice").WithBody($"'{count}' is not a valid number!").Build());
+ return;
+ }
+ }
+
+ var displayName = await infoCache.GetDisplayNameAsync(ctx.Room.RoomId, ctx.MessageEvent.Sender);
+ var roomName = await infoCache.GetRoomNameAsync(ctx.Room.RoomId);
+
+ msgQueue.EnqueueMessageAsync(filterConfigService.LogRoom,
+ new MessageBuilder("m.notice").WithBody($"Removing last {count} messages from ").WithMention(mxid)
+ .WithBody(" in ").WithMention(ctx.Room.RoomId, await infoCache.GetRoomNameAsync(ctx.Room.RoomId)).Build());
+ var hourglassReaction = await ctx.Room.SendTimelineEventAsync("m.reaction", new RoomMessageReactionEventContent() {
+ RelatesTo = new() {
+ EventId = ctx.MessageEvent.EventId,
+ RelationType = "m.annotation",
+ 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])
+ .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;
+ if (msg.RawContent is not { Count: > 0 }) continue;
+
+ await actionQueue.EqueueActionAsync(msg.EventId, async () => {
+ while (true) {
+ try {
+ await ctx.Room.RedactEventAsync(msg.EventId ?? throw new ArgumentException("Event ID is null?"), "Message removed by moderator.");
+ break;
+ }
+ catch (Exception e) {
+ msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice")
+ .WithBody($"Error redacting message in {ctx.Room.RoomId}!")
+ .WithCollapsibleSection("Error data", msb => msb.WithCodeBlock(e.ToString(), "csharp"))
+ .Build());
+ }
+ }
+
+ msgQueue.EnqueueMessageAsync(cfgService.LogRoom, new MessageBuilder("m.notice")
+ .WithBody($"Message sent by ").WithMention(msg.Sender, displayName).WithBody(" in ").WithMention(ctx.Room.RoomId, roomName)
+ .WithBody(" was removed in request by ").WithMention(ctx.Room.RoomId, roomName).WithBody("!").WithNewline()
+ .WithCollapsibleSection("Message data", msb => msb.WithCodeBlock(msg.RawContent.ToJson(ignoreNull: true), "json"))
+ .Build());
+ });
+ }
+
+ await ctx.Room.RedactEventAsync(hourglassReaction.EventId);
+ await ctx.Room.SendTimelineEventAsync("m.reaction", new RoomMessageReactionEventContent() {
+ RelatesTo = new() {
+ EventId = ctx.MessageEvent.EventId,
+ RelationType = "m.annotation",
+ Key = "\u2714\ufe0f" //check mark emoji
+ }
+ });
+ }
+ }
+}
\ No newline at end of file
|