about summary refs log tree commit diff
path: root/MiniUtils
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2025-05-13 08:17:26 +0200
committerRory& <root@rory.gay>2025-05-13 08:17:26 +0200
commitddb5a61b0fd234b436c0406ab57e38d67429c6b8 (patch)
treebd2bc4c20aab27135ebaa55e1f9193f22e7d73cc /MiniUtils
downloadMiniUtils-ddb5a61b0fd234b436c0406ab57e38d67429c6b8.tar.xz
Initial commit
Diffstat (limited to 'MiniUtils')
-rw-r--r--MiniUtils/Classes/Emojis.cs17
-rw-r--r--MiniUtils/Classes/IgnoreListEventContentWithDisabled.cs9
-rw-r--r--MiniUtils/Commands/ExperimentalFeaturesCommand.cs68
-rw-r--r--MiniUtils/Commands/IgnoreCommand.cs62
-rw-r--r--MiniUtils/Commands/MscCommand.cs24
-rw-r--r--MiniUtils/Commands/RedactCommand.cs80
-rw-r--r--MiniUtils/Commands/SpamCommand.cs44
-rw-r--r--MiniUtils/MiniUtils.csproj24
-rw-r--r--MiniUtils/MiniUtilsConfiguration.cs6
-rw-r--r--MiniUtils/MiniUtilsWorker.cs13
-rw-r--r--MiniUtils/Program.cs29
-rw-r--r--MiniUtils/Properties/launchSettings.json19
-rw-r--r--MiniUtils/Services/IgnoreListManager.cs67
-rw-r--r--MiniUtils/Utilities/MscInfoProvider.cs91
-rw-r--r--MiniUtils/appsettings.Development.json53
-rw-r--r--MiniUtils/appsettings.json8
-rwxr-xr-xMiniUtils/crossplat-build.sh11
17 files changed, 625 insertions, 0 deletions
diff --git a/MiniUtils/Classes/Emojis.cs b/MiniUtils/Classes/Emojis.cs
new file mode 100644

index 0000000..aef8904 --- /dev/null +++ b/MiniUtils/Classes/Emojis.cs
@@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MiniUtils.Classes; + +[SuppressMessage("ReSharper", "UnusedMember.Local")] +public class Emojis { + // Useful page: https://www.compart.com/en/unicode + + public const string ThumbsUp = "\ud83d\udc4d\ufe0e"; + public const string Recycle = "\u267b\ufe0e"; + public const string Bullseye = "\u25ce\ufe0e"; + public const string RightArrowWithTail = "\u21a3\ufe0e"; + public const string Prohibited = "\ud83d\udec7\ufe0e"; + public const string Wastebasket = "\ud83d\uddd1\ufe0e"; + public const string Hourglass = "\u231b\ufe0e"; + public const string Checkmark = "\u2705\ufe0e"; +} \ No newline at end of file diff --git a/MiniUtils/Classes/IgnoreListEventContentWithDisabled.cs b/MiniUtils/Classes/IgnoreListEventContentWithDisabled.cs new file mode 100644
index 0000000..7aaeb0a --- /dev/null +++ b/MiniUtils/Classes/IgnoreListEventContentWithDisabled.cs
@@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; +using LibMatrix.EventTypes.Spec; + +namespace MiniUtils.Classes; + +public class IgnoredUserListEventContentWithDisabled : IgnoredUserListEventContent { + [JsonPropertyName("gay.rory.MiniUtils.disabled_ignored_users")] + public Dictionary<string, IgnoredUserContent> DisabledIgnoredUsers { get; set; } = new(); +} \ No newline at end of file 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 diff --git a/MiniUtils/MiniUtils.csproj b/MiniUtils/MiniUtils.csproj new file mode 100644
index 0000000..f7af751 --- /dev/null +++ b/MiniUtils/MiniUtils.csproj
@@ -0,0 +1,24 @@ +<Project Sdk="Microsoft.NET.Sdk.Worker"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + </PropertyGroup> + + <PropertyGroup Condition="'$(Configuration)' == 'Release'"> + <Optimize>true</Optimize> +<!-- <RunAOTCompilation>true</RunAOTCompilation>--> +<!-- <PublishTrimmed>true</PublishTrimmed>--> + <PublishSingleFile>true</PublishSingleFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.2"/> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\LibMatrix\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" /> + <ProjectReference Include="..\MiniUtils.Core\MiniUtils.Core.csproj" /> + </ItemGroup> +</Project> diff --git a/MiniUtils/MiniUtilsConfiguration.cs b/MiniUtils/MiniUtilsConfiguration.cs new file mode 100644
index 0000000..292c12e --- /dev/null +++ b/MiniUtils/MiniUtilsConfiguration.cs
@@ -0,0 +1,6 @@ +namespace MiniUtils; + +public class MiniUtilsConfiguration { + public MiniUtilsConfiguration(IConfiguration config) => config.GetRequiredSection("MiniUtils").Bind(this); + public string? GithubToken { get; set; } +} \ No newline at end of file diff --git a/MiniUtils/MiniUtilsWorker.cs b/MiniUtils/MiniUtilsWorker.cs new file mode 100644
index 0000000..38d5aef --- /dev/null +++ b/MiniUtils/MiniUtilsWorker.cs
@@ -0,0 +1,13 @@ +using LibMatrix.Homeservers; + +namespace MiniUtils; + +public class MiniUtilsWorker(ILogger<MiniUtilsWorker> logger, AuthenticatedHomeserverGeneric homeserver) : IHostedService { + public async Task StartAsync(CancellationToken cancellationToken) { + logger.LogError("Logged in as {mxid}", homeserver.UserId); + } + + public async Task StopAsync(CancellationToken cancellationToken) { + + } +} \ No newline at end of file diff --git a/MiniUtils/Program.cs b/MiniUtils/Program.cs new file mode 100644
index 0000000..37308ea --- /dev/null +++ b/MiniUtils/Program.cs
@@ -0,0 +1,29 @@ +using LibMatrix.Extensions; +using LibMatrix.Services; +using LibMatrix.Utilities.Bot; +using MiniUtils; +using MiniUtils.Core; +using MiniUtils.Services; +using MiniUtils.Utilities; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddSingleton<MiniUtilsConfiguration>(); +builder.Services.AddSingleton<MscInfoProvider>(); +builder.Services.AddSingleton<IgnoreListManager>(); +builder.Services.AddRoryLibMatrixServices() + .AddMatrixBot() + // .WithInviteHandler<RoomInviteHandler>(); + .AddCommandHandler() + .DiscoverAllCommands(); + +// builder.Services.AddHostedService<PolicyListFetcher>(); +// builder.Services.AddHostedService<PolicyExecutor>(); +builder.Services.AddHostedService<MiniUtilsWorker>(); + +// builder.Services.AddSingleton<PolicyStore>(); + +// MatrixHttpClient.LogRequests = false; + +var host = builder.Build(); +host.Run(); \ No newline at end of file diff --git a/MiniUtils/Properties/launchSettings.json b/MiniUtils/Properties/launchSettings.json new file mode 100644
index 0000000..a98ac4d --- /dev/null +++ b/MiniUtils/Properties/launchSettings.json
@@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { +// "Development": { +// "commandName": "Project", +// "dotnetRunMessages": true, +// "environmentVariables": { +// "DOTNET_ENVIRONMENT": "Development" +// } +// }, + "Local": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Local" + } + } + } +} diff --git a/MiniUtils/Services/IgnoreListManager.cs b/MiniUtils/Services/IgnoreListManager.cs new file mode 100644
index 0000000..c42dd02 --- /dev/null +++ b/MiniUtils/Services/IgnoreListManager.cs
@@ -0,0 +1,67 @@ +using LibMatrix.EventTypes.Spec; +using LibMatrix.Homeservers; +using MiniUtils.Classes; + +namespace MiniUtils.Services; + +public class IgnoreListManager(AuthenticatedHomeserverGeneric homeserver) { + private static readonly SemaphoreSlim Lock = new(1, 1); + + public async Task<int> DisableAll() { + await Lock.WaitAsync(); + var ignoreList = await homeserver.GetAccountDataOrNullAsync<IgnoredUserListEventContentWithDisabled>(IgnoredUserListEventContent.EventId) ?? new(); + if (ignoreList.IgnoredUsers.Count == 0) { + Lock.Release(); + return 0; + } + + foreach (var ignore in ignoreList.IgnoredUsers) { + ignoreList.DisabledIgnoredUsers.Add(ignore.Key, ignore.Value); + } + + int count = ignoreList.DisabledIgnoredUsers.Count; + + ignoreList.IgnoredUsers.Clear(); + await homeserver.SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoreList); + Lock.Release(); + return count; + } + + public async Task<int> EnableAll() { + await Lock.WaitAsync(); + var ignoreList = await homeserver.GetAccountDataOrNullAsync<IgnoredUserListEventContentWithDisabled>(IgnoredUserListEventContent.EventId) ?? new(); + if (ignoreList.DisabledIgnoredUsers.Count == 0) { + Lock.Release(); + return 0; + } + + foreach (var ignore in ignoreList.DisabledIgnoredUsers) { + ignoreList.IgnoredUsers.Add(ignore.Key, ignore.Value); + } + + var count = ignoreList.IgnoredUsers.Count; + ignoreList.DisabledIgnoredUsers.Clear(); + await homeserver.SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoreList); + Lock.Release(); + return count; + } + + public async Task<int> MoveList(bool enable, IEnumerable<string> items) { + await Lock.WaitAsync(); + var ignoreList = await homeserver.GetAccountDataOrNullAsync<IgnoredUserListEventContentWithDisabled>(IgnoredUserListEventContent.EventId) ?? new(); + var fromDict = enable ? ignoreList.DisabledIgnoredUsers : ignoreList.IgnoredUsers; + var toDict = enable ? ignoreList.IgnoredUsers : ignoreList.DisabledIgnoredUsers; + var moved = 0; + foreach (var item in items) { + if (fromDict.Remove(item, out var value)) { + toDict.Add(item, value); + moved++; + } + } + + if (moved > 0) + await homeserver.SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoreList); + Lock.Release(); + return moved; + } +} \ No newline at end of file diff --git a/MiniUtils/Utilities/MscInfoProvider.cs b/MiniUtils/Utilities/MscInfoProvider.cs new file mode 100644
index 0000000..74788dc --- /dev/null +++ b/MiniUtils/Utilities/MscInfoProvider.cs
@@ -0,0 +1,91 @@ +using System.Globalization; +using System.Net.Http.Json; +using System.Text; +using System.Text.Json.Nodes; +using LibMatrix.Extensions; + +namespace MiniUtils.Utilities; + +public class MscInfoProvider(MiniUtilsConfiguration config) { + private static Dictionary<int, MscInfo> _mscCache = new(); + private static Dictionary<int, DateTime> _mscCacheExpiry = new(); + private static long RatelimitRemaining = 60; + private static DateTime RatelimitReset = DateTime.UtcNow; + public async Task<MscInfo?> GetMscInfo(int id) { + var hc = new MatrixHttpClient() { + DefaultRequestHeaders = { + { "User-Agent", "SynapseDataMiner" } + } + }; + + if(!string.IsNullOrWhiteSpace(config.GithubToken)) + hc.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.GithubToken}"); + + var response = await hc.GetAsync($"https://api.github.com/repos/matrix-org/matrix-spec-proposals/pulls/{id}"); + if(response.Headers.Contains("X-RateLimit-Remaining")) { + RatelimitRemaining = long.Parse(response.Headers.GetValues("X-RateLimit-Remaining").First()); + } + + if (response.Headers.Contains("X-RateLimit-Reset")) { + var reset = long.Parse(response.Headers.GetValues("X-RateLimit-Reset").First()); + RatelimitReset = DateTimeOffset.FromUnixTimeSeconds(reset).UtcDateTime; + } + + if (!response.IsSuccessStatusCode) { + return _mscCache.GetValueOrDefault(id); + } + + var data = await response.Content.ReadFromJsonAsync<JsonObject>(); + var info = new MscInfo() { + Id = id, + Author = data?["user"]?["login"]?.ToString(), + Title = data?["title"]?.ToString(), + Url = data?["html_url"]?.ToString(), + State = data?["state"]?.ToString(), + Labels = data?["labels"]?.AsArray().Select(x => new MscInfo.LabelInfo() { + Name = x["name"]?.ToString(), + Color = x["color"]?.ToString() + }).ToList() ?? [] + }; + _mscCache[id] = info; + _mscCacheExpiry[id] = DateTime.UtcNow.AddMinutes(60); + return info; + } + + public class MscInfo { + public int Id { get; set; } + public string Title { get; set; } + public string State { get; set; } + public string Author { get; set; } + public string Url { get; set; } + public List<LabelInfo> Labels { get; set; } + + public class LabelInfo { + public string Name { get; set; } + public string Color { get; set; } + } + + public string ToHtml() { + var sb = new StringBuilder(); + sb.Append($"<a href=\"{Url}\">{Title}</a> by {Author} ({State})<br/>"); + + foreach (var label in Labels) { + sb.Append($"<font color=\"#{label.Color}\">" + + $"<span data-mx-bg-color=\"#{label.Color}\" data-mx-color=\"{GetContrastingForegroundColor(label.Color)}\">{label.Name}</span>" + + $"</font> \n"); + } + + return sb + "\n"; + } + + private static string GetContrastingForegroundColor(string backgroundColor) { + var color = backgroundColor.Replace("#", ""); + var r = int.Parse(color.Substring(0, 2), NumberStyles.HexNumber); + var g = int.Parse(color.Substring(2, 2), NumberStyles.HexNumber); + var b = int.Parse(color.Substring(4, 2), NumberStyles.HexNumber); + // var brightness = Math.Sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); + var brightness = Math.Sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); + return brightness < 130 ? "#ffffff" : "#000000"; + } + } +} \ No newline at end of file diff --git a/MiniUtils/appsettings.Development.json b/MiniUtils/appsettings.Development.json new file mode 100644
index 0000000..4cfb975 --- /dev/null +++ b/MiniUtils/appsettings.Development.json
@@ -0,0 +1,53 @@ +{ + // Don't touch this unless you know what you're doing: + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "LibMatrixBot": { + // Homeserver to connect to. + // Note: Homeserver resolution is applied here, but a direct base URL can be used. + "Homeserver": "rory.gay", + + // Absolute path to the file containing the access token + "AccessTokenPath": "/home/Rory/matrix_access_token", + "InviteHandler": { + "SyncConfiguration": { + // How long to wait until the sync request times out + // "Timeout": 300000, + + // Minimum sync interval, useful if you want to have less traffic than a normal client. + "MinimumSyncTime": "00:00:10.000", + + // What presence value to set + // Defaults to "online" if null or not set + // "Presence": "online", + + // Filter to apply to the sync request. Useful if you want custom data to be sent. + // "Filter": { }, + + // Whether to initial sync on startup - very useful in development, or just to be sure. + "InitialSyncOnStartup": true + } + } + }, + "AntiDmSpam": { + // Whether invites should be logged to a room. + "LogRoom": "!GrLSwdAkdrvfMrRYKR:rory.gay", + "LogInviteDataAsFile": true, + // Whether to report users and rooms when an invite is blocked. + "ReportBlockedInvites": true, + // WARNING: If you're a room moderator, this will cause your client to not receive events from ignored users! + "IgnoreBannedUsers": true, + // Policy lists to follow + "PolicyLists": [ + { + "Name": "Community Moderation Effort", + "RoomId": "!fTjMjIzNKEsFlUIiru:neko.dev", + "Vias": [ "rory.gay" ] + } + ] + } +} \ No newline at end of file diff --git a/MiniUtils/appsettings.json b/MiniUtils/appsettings.json new file mode 100644
index 0000000..b2dcdb6 --- /dev/null +++ b/MiniUtils/appsettings.json
@@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/MiniUtils/crossplat-build.sh b/MiniUtils/crossplat-build.sh new file mode 100755
index 0000000..a325f72 --- /dev/null +++ b/MiniUtils/crossplat-build.sh
@@ -0,0 +1,11 @@ +#!/bin/sh +for arch in {x64,arm64} +do + for platform in {osx,win,linux} + do + set -x + dotnet publish -c Release -r ${platform}-${arch} --nologo --property AssemblyName="$(basename $PWD)-${platform}-${arch}" -o bin/release + set +x + done +done +wait