From 3e934eee892f69a8f78b94950993000522702769 Mon Sep 17 00:00:00 2001 From: "Emma [it/its]@Rory&" Date: Thu, 23 Nov 2023 05:42:33 +0100 Subject: Moderation bot work --- .../ModerationBot/Commands/BanMediaCommand.cs | 113 +++++++++++++++++++++ .../Commands/DbgAllRoomsArePolicyListsCommand.cs | 63 ++++++++++++ .../Commands/DbgDumpActivePoliciesCommand.cs | 43 ++++++++ .../Commands/DbgDumpAllStateTypesCommand.cs | 73 +++++++++++++ .../ModerationBot/Commands/JoinRoomCommand.cs | 54 ++++++++++ .../Commands/JoinSpaceMembersCommand.cs | 75 ++++++++++++++ 6 files changed, 421 insertions(+) create mode 100644 ExampleBots/ModerationBot/Commands/BanMediaCommand.cs create mode 100644 ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs create mode 100644 ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs create mode 100644 ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs create mode 100644 ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs create mode 100644 ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs (limited to 'ExampleBots/ModerationBot/Commands') diff --git a/ExampleBots/ModerationBot/Commands/BanMediaCommand.cs b/ExampleBots/ModerationBot/Commands/BanMediaCommand.cs new file mode 100644 index 0000000..21e0a94 --- /dev/null +++ b/ExampleBots/ModerationBot/Commands/BanMediaCommand.cs @@ -0,0 +1,113 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot.Interfaces; +using ModerationBot.AccountData; +using ModerationBot.StateEventTypes; +using ModerationBot.StateEventTypes.Policies.Implementations; + +namespace ModerationBot.Commands; + +public class BanMediaCommand(IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver, PolicyEngine engine) : ICommand { + public string Name { get; } = "banmedia"; + public string Description { get; } = "Create a policy banning a piece of media, must be used in reply to a message"; + + public async Task CanInvoke(CommandContext ctx) { + //check if user is admin in control room + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom); + var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); + if (!isAdmin) { + // await ctx.Reply("You do not have permission to use this command!"); + await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync( + new RoomMessageEventContent(body: $"User {ctx.MessageEvent.Sender} tried to use command {Name} but does not have permission!", messageType: "m.text")); + } + + return isAdmin; + } + + public async Task Invoke(CommandContext ctx) { + + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + var policyRoom = ctx.Homeserver.GetRoom(botData.DefaultPolicyRoom ?? botData.ControlRoom); + var logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom); + + //check if reply + var messageContent = ctx.MessageEvent.TypedContent as RoomMessageEventContent; + if (messageContent?.RelatesTo is { InReplyTo: not null }) { + try { + await logRoom.SendMessageEventAsync( + new RoomMessageEventContent( + body: $"User {MessageFormatter.HtmlFormatMention(ctx.MessageEvent.Sender)} is trying to ban media {messageContent!.RelatesTo!.InReplyTo!.EventId}", + messageType: "m.text")); + + //get replied message + var repliedMessage = await ctx.Room.GetEventAsync(messageContent.RelatesTo!.InReplyTo!.EventId); + + //check if recommendation is in list + if (ctx.Args.Length < 2) { + await ctx.Room.SendMessageEventAsync(MessageFormatter.FormatError("You must specify a recommendation type and reason!")); + return; + } + + var recommendation = ctx.Args[0]; + + if (recommendation is not ("ban" or "kick" or "mute" or "redact" or "spoiler" or "warn" or "warn_admins")) { + await ctx.Room.SendMessageEventAsync( + MessageFormatter.FormatError( + $"Invalid recommendation type {recommendation}, must be `warn_admins`, `warn`, `spoiler`, `redact`, `mute`, `kick` or `ban`!")); + return; + } + + //hash file + var mxcUri = (repliedMessage.TypedContent as RoomMessageEventContent).Url!; + var resolvedUri = await hsResolver.ResolveMediaUri(mxcUri.Split('/')[2], mxcUri); + var hashAlgo = SHA3_256.Create(); + var uriHash = hashAlgo.ComputeHash(mxcUri.AsBytes().ToArray()); + byte[]? fileHash = null; + + try { + fileHash = await hashAlgo.ComputeHashAsync(await ctx.Homeserver.ClientHttpClient.GetStreamAsync(resolvedUri)); + } + catch (Exception ex) { + await logRoom.SendMessageEventAsync( + MessageFormatter.FormatException($"Error calculating file hash for {mxcUri} via {mxcUri.Split('/')[2]}, retrying via {ctx.Homeserver.BaseUrl}...", + ex)); + try { + resolvedUri = await hsResolver.ResolveMediaUri(ctx.Homeserver.BaseUrl, mxcUri); + fileHash = await hashAlgo.ComputeHashAsync(await ctx.Homeserver.ClientHttpClient.GetStreamAsync(resolvedUri)); + } + catch (Exception ex2) { + await ctx.Room.SendMessageEventAsync(MessageFormatter.FormatException("Error calculating file hash", ex2)); + await logRoom.SendMessageEventAsync( + MessageFormatter.FormatException($"Error calculating file hash via {ctx.Homeserver.BaseUrl}!", ex2)); + } + } + + MediaPolicyFile policy; + await policyRoom.SendStateEventAsync("gay.rory.moderation.rule.media", Guid.NewGuid().ToString(), policy = new MediaPolicyFile { + Entity = Convert.ToBase64String(uriHash), + FileHash = Convert.ToBase64String(fileHash), + Reason = string.Join(' ', ctx.Args[1..]), + Recommendation = recommendation, + }); + + await ctx.Room.SendMessageEventAsync(MessageFormatter.FormatSuccessJson("Media policy created", policy)); + await logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccessJson("Media policy created", policy)); + } + catch (Exception e) { + await logRoom.SendMessageEventAsync(MessageFormatter.FormatException("Error creating policy", e)); + await ctx.Room.SendMessageEventAsync(MessageFormatter.FormatException("Error creating policy", e)); + await using var stream = new MemoryStream(e.ToString().AsBytes().ToArray()); + await logRoom.SendFileAsync("error.log.cs", stream); + } + } + else { + await ctx.Room.SendMessageEventAsync(MessageFormatter.FormatError("This command must be used in reply to a message!")); + } + } +} diff --git a/ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs b/ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs new file mode 100644 index 0000000..09d3caf --- /dev/null +++ b/ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs @@ -0,0 +1,63 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.RoomTypes; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot.Interfaces; +using ModerationBot.AccountData; +using ModerationBot.StateEventTypes; + +namespace ModerationBot.Commands; + +public class DbgAllRoomsArePolicyListsCommand + (IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver, PolicyEngine engine) : ICommand { + public string Name { get; } = "dbg-allroomsarepolicy"; + public string Description { get; } = "[Debug] mark all rooms as trusted policy rooms"; + private GenericRoom logRoom { get; set; } + + public async Task CanInvoke(CommandContext ctx) { +#if !DEBUG + return false; +#endif + + //check if user is admin in control room + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom); + var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); + if (!isAdmin) { + // await ctx.Reply("You do not have permission to use this command!"); + await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync( + new RoomMessageEventContent(body: $"User {ctx.MessageEvent.Sender} tried to use command {Name} but does not have permission!", messageType: "m.text")); + } + + return isAdmin; + } + + public async Task Invoke(CommandContext ctx) { + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom); + + var joinedRooms = await ctx.Homeserver.GetJoinedRooms(); + + await ctx.Homeserver.SetAccountDataAsync("gay.rory.moderation_bot.policy_lists", joinedRooms.ToDictionary(x => x.RoomId, x => new PolicyList() { + Trusted = true + })); + + await engine.ReloadActivePolicyLists(); + } + + private async Task JoinRoom(GenericRoom memberRoom, string reason, List servers) { + try { + await memberRoom.JoinAsync(servers.ToArray(), reason); + await logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joined room {memberRoom.RoomId}")); + } + catch (Exception e) { + await logRoom.SendMessageEventAsync(MessageFormatter.FormatException($"Failed to join {memberRoom.RoomId}", e)); + } + + return true; + } +} \ No newline at end of file diff --git a/ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs b/ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs new file mode 100644 index 0000000..395c87c --- /dev/null +++ b/ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs @@ -0,0 +1,43 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.RoomTypes; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot.Interfaces; +using ModerationBot.AccountData; +using ModerationBot.StateEventTypes; + +namespace ModerationBot.Commands; + +public class DbgDumpActivePoliciesCommand + (IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver, PolicyEngine engine) : ICommand { + public string Name { get; } = "dbg-dumppolicies"; + public string Description { get; } = "[Debug] Dump all active policies"; + private GenericRoom logRoom { get; set; } + + public async Task CanInvoke(CommandContext ctx) { +#if !DEBUG + return false; +#endif + + //check if user is admin in control room + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom); + var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); + if (!isAdmin) { + // await ctx.Reply("You do not have permission to use this command!"); + await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync( + new RoomMessageEventContent(body: $"User {ctx.MessageEvent.Sender} tried to use command {Name} but does not have permission!", messageType: "m.text")); + } + + return isAdmin; + } + + public async Task Invoke(CommandContext ctx) { + await ctx.Room.SendFileAsync("all.json", new MemoryStream(engine.ActivePolicies.ToJson().AsBytes().ToArray()), contentType: "application/json"); + await ctx.Room.SendFileAsync("by-type.json", new MemoryStream(engine.ActivePoliciesByType.ToJson().AsBytes().ToArray()), contentType: "application/json"); + } +} \ No newline at end of file diff --git a/ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs b/ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs new file mode 100644 index 0000000..e9a645e --- /dev/null +++ b/ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs @@ -0,0 +1,73 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.RoomTypes; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot.Interfaces; +using ModerationBot.AccountData; +using ModerationBot.StateEventTypes; + +namespace ModerationBot.Commands; + +public class DbgDumpAllStateTypesCommand + (IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver, PolicyEngine engine) : ICommand { + public string Name { get; } = "dbg-dumpstatetypes"; + public string Description { get; } = "[Debug] Dump all state types we can find"; + private GenericRoom logRoom { get; set; } + + public async Task CanInvoke(CommandContext ctx) { +#if !DEBUG + return false; +#endif + + //check if user is admin in control room + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom); + var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); + if (!isAdmin) { + // await ctx.Reply("You do not have permission to use this command!"); + await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync( + new RoomMessageEventContent(body: $"User {ctx.MessageEvent.Sender} tried to use command {Name} but does not have permission!", messageType: "m.text")); + } + + return isAdmin; + } + + public async Task Invoke(CommandContext ctx) { + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom); + + + var joinedRooms = await ctx.Homeserver.GetJoinedRooms(); + + var tasks = joinedRooms.Select(GetStateTypes).ToAsyncEnumerable(); + await foreach (var (room, (raw, html)) in tasks) { + await ctx.Room.SendMessageEventAsync(new RoomMessageEventContent("m.text") { + Body = $"States for {room.RoomId}:\n{raw}", + FormattedBody = $"States for {room.RoomId}:\n{html}", + Format = "org.matrix.custom.html" + }); + } + } + + private async Task<(GenericRoom room, (string raw, string html))> GetStateTypes(GenericRoom memberRoom) { + var states = await memberRoom.GetFullStateAsListAsync(); + + return (memberRoom, SummariseStateTypeCounts(states)); + } + + private static (string Raw, string Html) SummariseStateTypeCounts(IList states) { + string raw = "Count | State type | Mapped type", html = ""; + var groupedStates = states.GroupBy(x => x.Type).ToDictionary(x => x.Key, x => x.ToList()).OrderByDescending(x => x.Value.Count); + foreach (var (type, stateGroup) in groupedStates) { + raw += $"{stateGroup.Count} | {type} | {stateGroup[0].GetType.Name}"; + html += $""; + } + + html += "
CountState typeMapped type
{stateGroup.Count}{type}{stateGroup[0].GetType.Name}
"; + return (raw, html); + } +} \ No newline at end of file diff --git a/ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs b/ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs new file mode 100644 index 0000000..19a2c54 --- /dev/null +++ b/ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs @@ -0,0 +1,54 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot.Interfaces; +using ModerationBot.AccountData; +using ModerationBot.StateEventTypes; + +namespace ModerationBot.Commands; + +public class JoinRoomCommand(IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver, PolicyEngine engine) : ICommand { + public string Name { get; } = "join"; + public string Description { get; } = "Join arbitrary rooms"; + + public async Task CanInvoke(CommandContext ctx) { + //check if user is admin in control room + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom); + var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); + if (!isAdmin) { + // await ctx.Reply("You do not have permission to use this command!"); + await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync( + new RoomMessageEventContent(body: $"User {ctx.MessageEvent.Sender} tried to use command {Name} but does not have permission!", messageType: "m.text")); + } + + return isAdmin; + } + + public async Task Invoke(CommandContext ctx) { + + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + var policyRoom = ctx.Homeserver.GetRoom(botData.DefaultPolicyRoom ?? botData.ControlRoom); + var logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom); + + await logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joining room {ctx.Args[0]} with reason: {string.Join(' ', ctx.Args[1..])}")); + var roomId = ctx.Args[0]; + var servers = new List() {ctx.Homeserver.ServerName}; + if (roomId.StartsWith('[')) { + + } + + if (roomId.StartsWith('#')) { + var res = await ctx.Homeserver.ResolveRoomAliasAsync(roomId); + roomId = res.RoomId; + servers.AddRange(servers); + } + + await ctx.Homeserver.JoinRoomAsync(roomId, servers, string.Join(' ', ctx.Args[1..])); + await logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Resolved room {ctx.Args[0]} to {roomId} with servers: {string.Join(", ", servers)}")); + } +} diff --git a/ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs b/ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs new file mode 100644 index 0000000..c3b7d12 --- /dev/null +++ b/ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs @@ -0,0 +1,75 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using LibMatrix; +using LibMatrix.EventTypes.Spec; +using LibMatrix.Helpers; +using LibMatrix.RoomTypes; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot.Interfaces; +using ModerationBot.AccountData; +using ModerationBot.StateEventTypes; + +namespace ModerationBot.Commands; + +public class JoinSpaceMembersCommand(IServiceProvider services, HomeserverProviderService hsProvider, HomeserverResolverService hsResolver, PolicyEngine engine) : ICommand { + public string Name { get; } = "joinspacemembers"; + public string Description { get; } = "Join all rooms in space"; + private GenericRoom logRoom { get; set; } + + public async Task CanInvoke(CommandContext ctx) { + //check if user is admin in control room + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom); + var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban"); + if (!isAdmin) { + // await ctx.Reply("You do not have permission to use this command!"); + await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync( + new RoomMessageEventContent(body: $"User {ctx.MessageEvent.Sender} tried to use command {Name} but does not have permission!", messageType: "m.text")); + } + + return isAdmin; + } + + public async Task Invoke(CommandContext ctx) { + var botData = await ctx.Homeserver.GetAccountDataAsync("gay.rory.moderation_bot_data"); + logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom); + + await logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joining space children of {ctx.Args[0]} with reason: {string.Join(' ', ctx.Args[1..])}")); + var roomId = ctx.Args[0]; + var servers = new List() {ctx.Homeserver.ServerName}; + if (roomId.StartsWith('[')) { + + } + + if (roomId.StartsWith('#')) { + var res = await ctx.Homeserver.ResolveRoomAliasAsync(roomId); + roomId = res.RoomId; + servers.AddRange(servers); + } + + var room = ctx.Homeserver.GetRoom(roomId); + var tasks = new List>(); + await foreach (var memberRoom in room.AsSpace.GetChildrenAsync()) { + servers.Add(room.RoomId.Split(':', 2)[1]); + servers = servers.Distinct().ToList(); + tasks.Add(JoinRoom(memberRoom, string.Join(' ', ctx.Args[1..]), servers)); + } + + await foreach (var b in tasks.ToAsyncEnumerable()) { + await Task.Delay(50); + } + } + + private async Task JoinRoom(GenericRoom memberRoom, string reason, List servers) { + try { + await memberRoom.JoinAsync(servers.ToArray(), reason); + await logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joined room {memberRoom.RoomId}")); + } + catch (Exception e) { + await logRoom.SendMessageEventAsync(MessageFormatter.FormatException($"Failed to join {memberRoom.RoomId}", e)); + } + + return true; + } +} -- cgit 1.4.1