diff --git a/MiniUtils/Commands/ExperimentalFeaturesCommand.cs b/MiniUtils/Commands/ExperimentalFeaturesCommand.cs
new file mode 100644
index 0000000..de5d035
--- /dev/null
+++ b/MiniUtils/Commands/ExperimentalFeaturesCommand.cs
@@ -0,0 +1,68 @@
+using System.Globalization;
+using System.Net.Http.Json;
+using System.Text;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+using LibMatrix.EventTypes.Spec.State;
+using LibMatrix.Extensions;
+using LibMatrix.Helpers;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MiniUtils.Utilities;
+
+namespace SynapseDataMiner.Commands;
+
+public class ExperimentalFeaturesCommand(MscInfoProvider mscInfoProvider) : ICommand {
+ public string Name { get; } = "experimental";
+ public string[]? Aliases { get; } = [];
+ public string Description { get; } = "List experimental synapse features";
+ public bool Unlisted { get; } = false;
+
+ private static readonly Regex ExperimentalGetMultilineRegex = new("""experimental\.get\(\s*"(?<key>.+?)"(,\s*(?<defaultValue>.+?))?\s*\)""",
+ RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
+
+ private static readonly Regex MscNumberRegex = new(@"msc(?<id>\d+)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ public async Task Invoke(CommandContext ctx) {
+ var hc = new MatrixHttpClient();
+ var resp = await hc.GetAsync("https://raw.githubusercontent.com/element-hq/synapse/develop/synapse/config/experimental.py");
+ var data = await resp.Content.ReadAsStringAsync();
+
+ var msb = new MessageBuilder("m.notice");
+ List<SynapseFeature> features = [];
+
+ foreach (Match match in ExperimentalGetMultilineRegex.Matches(data)) {
+ var mscMatch = MscNumberRegex.Match(match.Groups["key"].Value);
+ features.Add(new() {
+ ConfigKey = match.Groups["key"].Value,
+ DefaultValue = match.Groups["defaultValue"]?.Value,
+ MscInfo = mscMatch.Success ? await mscInfoProvider.GetMscInfo(int.Parse(mscMatch.Groups["id"].Value)) : null
+ });
+ }
+
+ msb.WithTable(tb => {
+ tb.WithTitle("Available features", 2);
+ tb.WithRow(rb => {
+ rb.WithCell("Feature flag");
+ rb.WithCell("Description");
+ });
+
+ foreach (var feature in features.OrderBy(x => x.ConfigKey)) {
+ tb.WithRow(rb => {
+ rb.WithCell($"{feature.ConfigKey}<br/>Default: {feature.DefaultValue}");
+ rb.WithCell(feature.MscInfo is not null ? feature.MscInfo.ToHtml() : "No MSC info found");
+ });
+ }
+ });
+
+ await ctx.Room.SendMessageEventAsync(msb.Build());
+ Console.WriteLine(msb.Build().FormattedBody);
+ }
+
+ private class SynapseFeature {
+ public string ConfigKey { get; set; }
+ public string? DefaultValue { get; set; }
+ public MscInfoProvider.MscInfo? MscInfo { get; set; }
+ }
+
+
+}
\ No newline at end of file
diff --git a/MiniUtils/Commands/IgnoreCommand.cs b/MiniUtils/Commands/IgnoreCommand.cs
new file mode 100644
index 0000000..4b3fe86
--- /dev/null
+++ b/MiniUtils/Commands/IgnoreCommand.cs
@@ -0,0 +1,62 @@
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.Helpers;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MiniUtils.Classes;
+using MiniUtils.Services;
+
+namespace MiniUtils.Commands;
+
+public class IgnoreCommand(IgnoreListManager ignoreListManager) : ICommand {
+ public string Name => "ignore";
+
+ public string[]? Aliases => ["ignorelist"];
+
+ public string Description => "Manage ignore list";
+
+ public bool Unlisted => false;
+
+ public async Task Invoke(CommandContext ctx) {
+ var ignoreList = await ctx.Homeserver.GetAccountDataOrNullAsync<IgnoredUserListEventContentWithDisabled>(IgnoredUserListEventContent.EventId) ?? new();
+ if (ctx.Args.Length == 0)
+ await Summarize(ctx, ignoreList);
+ else if (ctx.Args is ["disable", "all"] or ["disableall"] or ["disall"]) {
+ var count = await ignoreListManager.DisableAll();
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.Recycle} {count}");
+ }
+ else if (ctx.Args is ["enable", "all"] or ["enableall"] or ["enall"]) {
+ var count = await ignoreListManager.EnableAll();
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.Bullseye} {count}");
+ }
+ else if (ctx.Args is ["disable", "joined"] or ["disablejoined"]) {
+ var count = await ignoreListManager.MoveList(false, (await ctx.Room.GetMembersListAsync("join")).Select(x => x.StateKey!));
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
+ }
+ else if (ctx.Args is ["enable", "joined"] or ["enablejoined"]) {
+ var count = await ignoreListManager.MoveList(true, (await ctx.Room.GetMembersListAsync("join")).Select(x => x.StateKey!));
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
+ }
+ else if (ctx.Args is ["disable", "local"] or ["disablelocal"] or ["disable", "room"] or ["disableroom"]) {
+ var count = await ignoreListManager.MoveList(false, (await ctx.Room.GetMembersListAsync()).Select(x => x.StateKey!));
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
+ }
+ else if (ctx.Args is ["enable", "local"] or ["enablelocal"] or ["enable", "room"] or ["enableroom"]) {
+ var count = await ignoreListManager.MoveList(true, (await ctx.Room.GetMembersListAsync()).Select(x => x.StateKey!));
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
+ }
+ else if (ctx.Args is ["disable", .. var itemsToDisable]) {
+ var count = await ignoreListManager.MoveList(false, itemsToDisable);
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
+ }
+ else if (ctx.Args is ["enable", .. var itemsToEnable]) {
+ var count = await ignoreListManager.MoveList(true, itemsToEnable);
+ await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.RightArrowWithTail} {count}");
+ }
+ }
+
+ private async Task Summarize(CommandContext ctx, IgnoredUserListEventContentWithDisabled ignoreList) {
+ var msb = new MessageBuilder()
+ .WithBody($"Ignored users: {ignoreList.IgnoredUsers.Count}").WithNewline()
+ .WithBody($"Disabled ignores: {ignoreList.DisabledIgnoredUsers.Count}").WithNewline();
+ await ctx.Room.SendMessageEventAsync(msb.Build());
+ }
+ }
\ No newline at end of file
diff --git a/MiniUtils/Commands/MscCommand.cs b/MiniUtils/Commands/MscCommand.cs
new file mode 100644
index 0000000..62f1bd7
--- /dev/null
+++ b/MiniUtils/Commands/MscCommand.cs
@@ -0,0 +1,24 @@
+using LibMatrix.Extensions;
+using LibMatrix.Helpers;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MiniUtils.Utilities;
+
+namespace MiniUtils.Commands;
+
+public class MscCommand(MscInfoProvider mscInfoProvider) : ICommand {
+ public string Name { get; } = "msc";
+ public string[]? Aliases { get; } = [];
+ public string Description { get; } = "Get MSC info";
+ public bool Unlisted { get; } = false;
+
+ public async Task Invoke(CommandContext ctx) {
+ var msb = new MessageBuilder("m.notice");
+ var id = int.Parse(ctx.Args[0]);
+ var mscInfo = await mscInfoProvider.GetMscInfo(id);
+
+ msb.WithBody(mscInfo?.ToHtml() ?? "No info found!");
+
+ await ctx.Reply(msb.Build());
+ Console.WriteLine(msb.Build().FormattedBody);
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils/Commands/RedactCommand.cs b/MiniUtils/Commands/RedactCommand.cs
new file mode 100644
index 0000000..cba06c9
--- /dev/null
+++ b/MiniUtils/Commands/RedactCommand.cs
@@ -0,0 +1,80 @@
+using System.Collections.Frozen;
+using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Filters;
+using LibMatrix.Helpers;
+using LibMatrix.RoomTypes;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MiniUtils.Classes;
+using MiniUtils.Services;
+
+namespace MiniUtils.Commands;
+
+public class RedactCommand(IgnoreListManager ignoreListManager) : ICommand {
+ public string Name => "redact";
+
+ public string[]? Aliases => [];
+
+ public string Description => "Redact all user's events";
+
+ public bool Unlisted => false;
+ private const string ThumbsUp = "\ud83d\udc4d\ufe0e";
+ private const string Recycle = "\u267b\ufe0e";
+ private const string Bullseye = "\u25ce\ufe0e";
+ private const string RightArrowWithTail = "\u21a3\ufe0e";
+
+ public async Task Invoke(CommandContext ctx) {
+ if (ctx.Args is ["banned"])
+ await RedactUsers(ctx, await ctx.Room.GetMemberIdsListAsync("ban"));
+ else if (ctx.Args is [.. var senders]) {
+ var sendersSet = senders.ToFrozenSet();
+ await RedactUsers(ctx, sendersSet);
+ }
+ }
+
+ private async Task RedactUsers(CommandContext ctx, FrozenSet<string> senders) {
+ var filter = new SyncFilter.EventFilter(senders: senders.ToList(), notTypes: ["m.room.redaction"]);
+ await ignoreListManager.MoveList(false, senders);
+ var count = 0;
+ List<Task> tasks = [];
+ await foreach (var resp in ctx.Room.GetManyMessagesAsync(filter: filter.ToJson(false, ignoreNull: true), chunkSize: 1000)) {
+ foreach (var chunk in resp.Chunk.Chunk(49)) {
+ foreach (var evt in chunk) {
+ if (!senders.Contains(evt.Sender!)) continue;
+ if (evt is { StateKey: not null, Type: not RoomMemberEventContent.EventId }) continue;
+ if (evt is { RawContent: null or { Count: 0 } }) continue;
+ tasks.Add(RedactEvent(ctx.Room, evt.EventId!));
+ count++;
+ }
+
+ if (tasks.Count > 0) {
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder()
+ .WithBody(
+ $"[{Emojis.Hourglass}] {Emojis.Recycle} {count} ({Emojis.Checkmark} {tasks.Count(t => t.IsCompletedSuccessfully)} {Emojis.Prohibited} {tasks.Count(t => t.IsFaulted)} {Emojis.Hourglass} {tasks.Count(t => t.Status == TaskStatus.Running)})")
+ .Build());
+ // await Task.WhenAll(tasks);
+ }
+ }
+ }
+
+ await Task.WhenAll(tasks);
+
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder().WithBody($"{Emojis.Recycle} {count}").Build());
+ // await ctx.Room.SendReactionAsync(ctx.MessageEvent.EventId!, $"{Emojis.Recycle} {count}");
+ }
+
+ private async Task RedactEvent(GenericRoom room, string eventId) {
+ bool success;
+ do {
+ try {
+ await room.RedactEventAsync(eventId);
+ success = true;
+ }
+ catch (Exception e) {
+ success = false;
+ Console.WriteLine($"Failed to redact event {eventId}: {e}");
+ }
+ } while (!success);
+ }
+}
\ No newline at end of file
diff --git a/MiniUtils/Commands/SpamCommand.cs b/MiniUtils/Commands/SpamCommand.cs
new file mode 100644
index 0000000..9f475eb
--- /dev/null
+++ b/MiniUtils/Commands/SpamCommand.cs
@@ -0,0 +1,44 @@
+using System.Collections.Frozen;
+using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.Filters;
+using LibMatrix.Helpers;
+using LibMatrix.RoomTypes;
+using LibMatrix.Utilities.Bot.Interfaces;
+using MiniUtils.Classes;
+using MiniUtils.Services;
+
+namespace MiniUtils.Commands;
+
+public class SpamCommand(IgnoreListManager ignoreListManager) : ICommand {
+ public string Name => "spam";
+
+ public string[]? Aliases => [];
+
+ public string Description => "Redact all user's events";
+
+ public bool Unlisted => false;
+
+ public async Task Invoke(CommandContext ctx) {
+ var tasks = Enumerable.Range(0, 10000)
+ .Select(i => SendMessage(ctx.Room, i.ToString()))
+ .ToList();
+ await Task.WhenAll(tasks);
+ await ctx.Room.SendMessageEventAsync(new MessageBuilder().WithBody($"{Emojis.Recycle}").Build());
+ }
+
+ private async Task SendMessage(GenericRoom room, string content) {
+ bool success;
+ do {
+ try {
+ await room.SendMessageEventAsync(new MessageBuilder().WithBody(content).Build());
+ success = true;
+ }
+ catch (Exception e) {
+ success = false;
+ Console.WriteLine($"Failed to send event {content}: {e}");
+ }
+ } while (!success);
+ }
+}
\ No newline at end of file
|