From 4f0236d766e05afa3eb6157a337100ca32f67588 Mon Sep 17 00:00:00 2001 From: Katrix Date: Wed, 23 Nov 2022 09:48:24 +0100 Subject: [PATCH 1/3] feat(bot): Case insensitive proxy tags matching (#490) --- PluralKit.Bot/CommandMeta/CommandTree.cs | 2 ++ PluralKit.Bot/Commands/Checks.cs | 2 +- PluralKit.Bot/Commands/Config.cs | 30 +++++++++++++++++++ PluralKit.Bot/Proxy/ProxyMatcher.cs | 8 ++--- PluralKit.Bot/Proxy/ProxyService.cs | 5 ++-- PluralKit.Bot/Proxy/ProxyTagParser.cs | 13 +++++--- .../Database/Functions/MessageContext.cs | 1 + .../Database/Functions/functions.sql | 9 ++++-- PluralKit.Core/Database/Migrations/31.sql | 5 ++++ .../Database/Utils/DatabaseMigrator.cs | 2 +- .../Models/Patch/SystemConfigPatch.cs | 8 +++++ PluralKit.Core/Models/SystemConfig.cs | 3 ++ PluralKit.Tests/ProxyTagParserTests.cs | 19 +++++++++--- docs/content/command-list.md | 1 + 14 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 PluralKit.Core/Database/Migrations/31.sql diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 691b0828..b129beb4 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -511,6 +511,8 @@ public partial class CommandTree return ctx.Execute(null, m => m.GroupDefaultPrivacy(ctx)); if (ctx.MatchMultiple(new[] { "show" }, new[] { "private" }) || ctx.Match("sp")) return ctx.Execute(null, m => m.ShowPrivateInfo(ctx)); + if (ctx.MatchMultiple(new[] { "proxy" }, new[] { "case" })) + return ctx.Execute(null, m => m.CaseSensitiveProxyTags(ctx)); // todo: maybe add the list of configuration keys here? return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands config` for the list of possible config settings."); diff --git a/PluralKit.Bot/Commands/Checks.cs b/PluralKit.Bot/Commands/Checks.cs index f4d37a27..eae1c140 100644 --- a/PluralKit.Bot/Commands/Checks.cs +++ b/PluralKit.Bot/Commands/Checks.cs @@ -243,7 +243,7 @@ public class Checks try { _proxy.ShouldProxy(channel, msg, context); - _matcher.TryMatch(context, autoproxySettings, members, out var match, msg.Content, msg.Attachments.Length > 0, true); + _matcher.TryMatch(context, autoproxySettings, members, out var match, msg.Content, msg.Attachments.Length > 0, true, ctx.Config.CaseSensitiveProxyTags); await ctx.Reply("I'm not sure why this message was not proxied, sorry."); } diff --git a/PluralKit.Bot/Commands/Config.cs b/PluralKit.Bot/Commands/Config.cs index e6591e90..589d77ca 100644 --- a/PluralKit.Bot/Commands/Config.cs +++ b/PluralKit.Bot/Commands/Config.cs @@ -88,6 +88,13 @@ public class Config Limits.MaxGroupCount.ToString() )); + items.Add(new( + "Case sensitive proxy tags", + "If proxy tags should be case sensitive", + EnabledDisabled(ctx.Config.CaseSensitiveProxyTags), + "enabled" + )); + await ctx.Paginate( items.ToAsyncEnumerable(), items.Count, @@ -383,4 +390,27 @@ public class Config await ctx.Reply("Private information will now be **hidden** when looking up your own info. Use the `-private` flag to show it."); } } + + public async Task CaseSensitiveProxyTags(Context ctx) + { + if (!ctx.HasNext()) + { + if (ctx.Config.CaseSensitiveProxyTags) { await ctx.Reply("Proxy tags are currently case sensitive"); } + else { await ctx.Reply("Proxy tags are currently case insensitive"); } + return; + } + + if (ctx.MatchToggle(true)) + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = true }); + + await ctx.Reply("Proxy tags are now case sensitive"); + } + else + { + await ctx.Repository.UpdateSystemConfig(ctx.System.Id, new() { CaseSensitiveProxyTags = false }); + + await ctx.Reply("Proxy tags are now case insensitive"); + } + } } \ No newline at end of file diff --git a/PluralKit.Bot/Proxy/ProxyMatcher.cs b/PluralKit.Bot/Proxy/ProxyMatcher.cs index 87cbf329..6afc847a 100644 --- a/PluralKit.Bot/Proxy/ProxyMatcher.cs +++ b/PluralKit.Bot/Proxy/ProxyMatcher.cs @@ -20,17 +20,17 @@ public class ProxyMatcher public bool TryMatch(MessageContext ctx, AutoproxySettings settings, IReadOnlyCollection members, out ProxyMatch match, string messageContent, - bool hasAttachments, bool allowAutoproxy) + bool hasAttachments, bool allowAutoproxy, bool caseSensitive) { - if (TryMatchTags(members, messageContent, hasAttachments, out match)) return true; + if (TryMatchTags(members, messageContent, hasAttachments, caseSensitive, out match)) return true; if (allowAutoproxy && TryMatchAutoproxy(ctx, settings, members, messageContent, out match)) return true; return false; } private bool TryMatchTags(IReadOnlyCollection members, string messageContent, bool hasAttachments, - out ProxyMatch match) + bool caseSensitive, out ProxyMatch match) { - if (!_parser.TryMatch(members, messageContent, out match)) return false; + if (!_parser.TryMatch(members, messageContent, caseSensitive, out match)) return false; // Edge case: If we got a match with blank inner text, we'd normally just send w/ attachments // However, if there are no attachments, the user probably intended something else, so we "un-match" and proceed to autoproxy diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index abdd4180..7f07c2bc 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -78,7 +78,7 @@ public class ProxyService members = (await _repo.GetProxyMembers(message.Author.Id, message.GuildId!.Value)).ToList(); if (!_matcher.TryMatch(ctx, autoproxySettings, members, out var match, message.Content, message.Attachments.Length > 0, - allowAutoproxy)) return false; + allowAutoproxy, ctx.CaseSensitiveProxyTags)) return false; // this is hopefully temporary, so not putting it into a separate method if (message.Content != null && message.Content.Length > 2000) @@ -208,8 +208,9 @@ public class ProxyService "Proxying was disabled in this channel by a server administrator (via the proxy blacklist)."); var autoproxySettings = await _repo.GetAutoproxySettings(ctx.SystemId.Value, msg.Guild!.Value, null); + var config = await _repo.GetSystemConfig(ctx.SystemId.Value); var prevMatched = _matcher.TryMatch(ctx, autoproxySettings, members, out var prevMatch, originalMsg.Content, - originalMsg.Attachments.Length > 0, false); + originalMsg.Attachments.Length > 0, false, ctx.CaseSensitiveProxyTags); var match = new ProxyMatch { diff --git a/PluralKit.Bot/Proxy/ProxyTagParser.cs b/PluralKit.Bot/Proxy/ProxyTagParser.cs index 2c748f9c..5dc27cec 100644 --- a/PluralKit.Bot/Proxy/ProxyTagParser.cs +++ b/PluralKit.Bot/Proxy/ProxyTagParser.cs @@ -10,7 +10,7 @@ public class ProxyTagParser private readonly Regex prefixPattern = new(@"^<(?:@!?|#|@&|a?:[\d\w_]+?:)\d+>"); private readonly Regex suffixPattern = new(@"<(?:@!?|#|@&|a?:[\d\w_]+?:)\d+>$"); - public bool TryMatch(IEnumerable members, string? input, out ProxyMatch result) + public bool TryMatch(IEnumerable members, string? input, bool caseSensitive, out ProxyMatch result) { result = default; @@ -42,7 +42,7 @@ public class ProxyTagParser if (tag.Suffix == ">" && suffixPattern.IsMatch(input)) continue; // Can we match with these tags? - if (TryMatchTagsInner(input, tag, out result.Content)) + if (TryMatchTagsInner(input, tag, caseSensitive, out result.Content)) { // If we extracted a leading mention before, add that back now if (leadingMention != null) result.Content = $"{leadingMention} {result.Content}"; @@ -56,7 +56,7 @@ public class ProxyTagParser return false; } - private bool TryMatchTagsInner(string input, ProxyTag tag, out string inner) + private bool TryMatchTagsInner(string input, ProxyTag tag, bool caseSensitive, out string inner) { inner = ""; @@ -64,9 +64,14 @@ public class ProxyTagParser var prefix = tag.Prefix ?? ""; var suffix = tag.Suffix ?? ""; + var comparision = caseSensitive + ? StringComparison.CurrentCulture + : StringComparison.CurrentCultureIgnoreCase; + // Check if our input starts/ends with the tags var isMatch = input.Length >= prefix.Length + suffix.Length - && input.StartsWith(prefix) && input.EndsWith(suffix); + && input.StartsWith(prefix, comparision) + && input.EndsWith(suffix, comparision); // Special case: image-only proxies + proxy tags with spaces // Trim everything, then see if we have a "contentless tag pair" (normally disallowed, but OK if we have an attachment) diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index 70db64f6..c4eff586 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -28,4 +28,5 @@ public class MessageContext public string? SystemAvatar { get; } public bool AllowAutoproxy { get; } public int? LatchTimeout { get; } + public bool CaseSensitiveProxyTags { get; } } \ No newline at end of file diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index 80445c64..0b9f3a76 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -14,12 +14,14 @@ tag_enabled bool, system_avatar text, allow_autoproxy bool, - latch_timeout integer + latch_timeout integer, + case_sensitive_proxy_tags bool ) as $$ -- CTEs to query "static" (accessible only through args) data with - system as (select systems.*, system_config.latch_timeout, system_guild.tag as guild_tag, system_guild.tag_enabled as tag_enabled, allow_autoproxy as account_autoproxy from accounts + system as (select systems.*, system_config.latch_timeout, system_guild.tag as guild_tag, system_guild.tag_enabled as tag_enabled, + allow_autoproxy as account_autoproxy, system_config.case_sensitive_proxy_tags from accounts left join systems on systems.id = accounts.system left join system_config on system_config.system = accounts.system left join system_guild on system_guild.system = accounts.system and system_guild.guild = guild_id @@ -40,7 +42,8 @@ as $$ coalesce(system.tag_enabled, true) as tag_enabled, system.avatar_url as system_avatar, system.account_autoproxy as allow_autoproxy, - system.latch_timeout as latch_timeout + system.latch_timeout as latch_timeout, + system.case_sensitive_proxy_tags as case_sensitive_proxy_tags -- We need a "from" clause, so we just use some bogus data that's always present -- This ensure we always have exactly one row going forward, so we can left join afterwards and still get data from (select 1) as _placeholder diff --git a/PluralKit.Core/Database/Migrations/31.sql b/PluralKit.Core/Database/Migrations/31.sql new file mode 100644 index 00000000..57cba248 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/31.sql @@ -0,0 +1,5 @@ +-- schema version 31 + +alter table system_config add column case_sensitive_proxy_tags boolean not null default true; + +update info set schema_version = 31; \ No newline at end of file diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs index e34b4b90..d610e094 100644 --- a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs +++ b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs @@ -9,7 +9,7 @@ namespace PluralKit.Core; internal class DatabaseMigrator { private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files - private const int TargetSchemaVersion = 30; + private const int TargetSchemaVersion = 31; private readonly ILogger _logger; public DatabaseMigrator(ILogger logger) diff --git a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs index f56f8225..cc3ad00d 100644 --- a/PluralKit.Core/Models/Patch/SystemConfigPatch.cs +++ b/PluralKit.Core/Models/Patch/SystemConfigPatch.cs @@ -17,6 +17,7 @@ public class SystemConfigPatch: PatchObject public Partial MemberLimitOverride { get; set; } public Partial GroupLimitOverride { get; set; } public Partial DescriptionTemplates { get; set; } + public Partial CaseSensitiveProxyTags { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper @@ -29,6 +30,7 @@ public class SystemConfigPatch: PatchObject .With("member_limit_override", MemberLimitOverride) .With("group_limit_override", GroupLimitOverride) .With("description_templates", DescriptionTemplates) + .With("case_sensitive_proxy_tags", CaseSensitiveProxyTags) ); public new void AssertIsValid() @@ -78,6 +80,9 @@ public class SystemConfigPatch: PatchObject if (DescriptionTemplates.IsPresent) o.Add("description_templates", JArray.FromObject(DescriptionTemplates.Value)); + if (CaseSensitiveProxyTags.IsPresent) + o.Add("case_sensitive_proxy_tags", CaseSensitiveProxyTags.Value); + return o; } @@ -103,6 +108,9 @@ public class SystemConfigPatch: PatchObject if (o.ContainsKey("description_templates")) patch.DescriptionTemplates = o.Value("description_templates").Select(x => x.Value()).ToArray(); + if (o.ContainsKey("case_sensitive_proxy_tags")) + patch.CaseSensitiveProxyTags = o.Value("case_sensitive_proxy_tags"); + return patch; } } \ No newline at end of file diff --git a/PluralKit.Core/Models/SystemConfig.cs b/PluralKit.Core/Models/SystemConfig.cs index a3e0b960..22957918 100644 --- a/PluralKit.Core/Models/SystemConfig.cs +++ b/PluralKit.Core/Models/SystemConfig.cs @@ -18,6 +18,8 @@ public class SystemConfig public ICollection DescriptionTemplates { get; } public DateTimeZone Zone => DateTimeZoneProviders.Tzdb.GetZoneOrNull(UiTz); + + public bool CaseSensitiveProxyTags { get; set; } } public static class SystemConfigExt @@ -34,6 +36,7 @@ public static class SystemConfigExt o.Add("show_private_info", cfg.ShowPrivateInfo); o.Add("member_limit", cfg.MemberLimitOverride ?? Limits.MaxMemberCount); o.Add("group_limit", cfg.GroupLimitOverride ?? Limits.MaxGroupCount); + o.Add("case_sensitive_proxy_tags", cfg.CaseSensitiveProxyTags); o.Add("description_templates", JArray.FromObject(cfg.DescriptionTemplates)); diff --git a/PluralKit.Tests/ProxyTagParserTests.cs b/PluralKit.Tests/ProxyTagParserTests.cs index 060c80ec..2a646436 100644 --- a/PluralKit.Tests/ProxyTagParserTests.cs +++ b/PluralKit.Tests/ProxyTagParserTests.cs @@ -9,9 +9,10 @@ namespace PluralKit.Tests; public class ProxyTagParserTests { internal static ProxyMatch AssertMatch(IEnumerable members, string input, string? name = null, - string? prefix = null, string? suffix = null, string? content = null) + string? prefix = null, string? suffix = null, string? content = null, + bool caseSensitive = true) { - Assert.True(new ProxyTagParser().TryMatch(members, input, out var result)); + Assert.True(new ProxyTagParser().TryMatch(members, input, caseSensitive, out var result)); if (name != null) Assert.Equal(name, result.Member.Name); if (prefix != null) Assert.Equal(prefix, result.ProxyTags?.Prefix); if (suffix != null) Assert.Equal(suffix, result.ProxyTags?.Suffix); @@ -19,9 +20,9 @@ public class ProxyTagParserTests return result; } - internal static void AssertNoMatch(IEnumerable members, string? input) + internal static void AssertNoMatch(IEnumerable members, string? input, bool caseSensitive = true) { - Assert.False(new ProxyTagParser().TryMatch(members, input, out _)); + Assert.False(new ProxyTagParser().TryMatch(members, input, caseSensitive, out _)); } public class Basics @@ -46,6 +47,16 @@ public class ProxyTagParserTests public void StringWithTagsMatch(string input) => AssertMatch(members, input); + [Theory] + [InlineData("a:tag with lowercase prefix")] + public void StringWithLowercaseUsingDefaultConfigMatchesNothing(string input) => + AssertNoMatch(members, input); + + [Theory] + [InlineData("a:tag with lowercase prefix")] + public void StringWithLowercaseUsingCaseInsensitiveConfigMatches(string input) => + AssertMatch(members, input, caseSensitive: false); + [Theory] [InlineData("[john's tags]", "John")] [InlineData("{bob's tags}", "Bob")] diff --git a/docs/content/command-list.md b/docs/content/command-list.md index be8d8423..b96e3861 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -117,6 +117,7 @@ Some arguments indicate the use of specific Discord features. These include: - `pk;config ping ` - Changes your system's ping preferences. - `pk;config autoproxy timeout [|off|reset]` - Sets the latch timeout duration for your system. - `pk;config autoproxy account [on|off]` - Toggles autoproxy globally for the current account. +- `pk;config proxy case [on|off]` - Toggles case sensitive proxy tags for your system. ## Server owner commands *(all commands here require Manage Server permission)* From 0d27e669c8947ddeb053570f3788e3d03ccdfe46 Mon Sep 17 00:00:00 2001 From: Iris System Date: Wed, 23 Nov 2022 23:53:21 +1300 Subject: [PATCH 2/3] feat(bot): add admin commands for hid rerolls --- PluralKit.Bot/CommandMeta/CommandTree.cs | 6 ++ PluralKit.Bot/Commands/Admin.cs | 71 ++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index b129beb4..da051b20 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -117,6 +117,12 @@ public partial class CommandTree await ctx.Execute(Admin, a => a.UpdateMemberId(ctx)); else if (ctx.Match("ugid", "updategroupid")) await ctx.Execute(Admin, a => a.UpdateGroupId(ctx)); + else if (ctx.Match("rsid", "rerollsystemid")) + await ctx.Execute(Admin, a => a.RerollSystemId(ctx)); + else if (ctx.Match("rmid", "rerollmemberid")) + await ctx.Execute(Admin, a => a.RerollMemberId(ctx)); + else if (ctx.Match("rgid", "rerollgroupid")) + await ctx.Execute(Admin, a => a.RerollGroupId(ctx)); else if (ctx.Match("uml", "updatememberlimit")) await ctx.Execute(Admin, a => a.SystemMemberLimit(ctx)); else if (ctx.Match("ugl", "updategrouplimit")) diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index 3a0d3823..d5a2ed0f 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -1,5 +1,8 @@ using System.Text.RegularExpressions; +using Dapper; +using SqlKata; + using PluralKit.Core; namespace PluralKit.Bot; @@ -87,6 +90,74 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`)."); } + public async Task RerollSystemId(Context ctx) + { + ctx.AssertBotAdmin(); + + var target = await ctx.MatchSystem(); + if (target == null) + throw new PKError("Unknown system."); + + if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll")) + throw new PKError("ID change cancelled."); + + var query = new Query("systems").AsUpdate(new + { + hid = new UnsafeLiteral("find_free_system_hid()"), + }) + .Where("id", target.Id); + + var newHid = await ctx.Database.QueryFirst(query, "returning hid"); + await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`)."); + } + + public async Task RerollMemberId(Context ctx) + { + ctx.AssertBotAdmin(); + + var target = await ctx.MatchMember(); + if (target == null) + throw new PKError("Unknown member."); + + if (!await ctx.PromptYesNo( + $"Reroll member ID for **{target.NameFor(LookupContext.ByNonOwner)}** (`{target.Hid}`)?", + "Reroll" + )) + throw new PKError("ID change cancelled."); + + var query = new Query("members").AsUpdate(new + { + hid = new UnsafeLiteral("find_free_member_hid()"), + }) + .Where("id", target.Id); + + var newHid = await ctx.Database.QueryFirst(query, "returning hid"); + await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`)."); + } + + public async Task RerollGroupId(Context ctx) + { + ctx.AssertBotAdmin(); + + var target = await ctx.MatchGroup(); + if (target == null) + throw new PKError("Unknown group."); + + if (!await ctx.PromptYesNo($"Reroll group ID for **{target.Name}** (`{target.Hid}`)?", + "Change" + )) + throw new PKError("ID change cancelled."); + + var query = new Query("groups").AsUpdate(new + { + hid = new UnsafeLiteral("find_free_group_hid()"), + }) + .Where("id", target.Id); + + var newHid = await ctx.Database.QueryFirst(query, "returning hid"); + await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`)."); + } + public async Task SystemMemberLimit(Context ctx) { ctx.AssertBotAdmin(); From e65c2de5423f4103f80b8562be16bf30c3564354 Mon Sep 17 00:00:00 2001 From: Iris System Date: Wed, 23 Nov 2022 23:54:21 +1300 Subject: [PATCH 3/3] feat(bot): add admin command for account recovery --- PluralKit.Bot/CommandMeta/CommandTree.cs | 2 + PluralKit.Bot/Commands/Admin.cs | 55 +++++++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index da051b20..0d5eb3d7 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -127,6 +127,8 @@ public partial class CommandTree await ctx.Execute(Admin, a => a.SystemMemberLimit(ctx)); else if (ctx.Match("ugl", "updategrouplimit")) await ctx.Execute(Admin, a => a.SystemGroupLimit(ctx)); + else if (ctx.Match("sr", "systemrecover")) + await ctx.Execute(Admin, a => a.SystemRecover(ctx)); else await ctx.Reply($"{Emojis.Error} Unknown command."); } diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index d5a2ed0f..374ab485 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -3,6 +3,9 @@ using System.Text.RegularExpressions; using Dapper; using SqlKata; +using Myriad.Rest; +using Myriad.Types; + using PluralKit.Core; namespace PluralKit.Bot; @@ -10,10 +13,12 @@ namespace PluralKit.Bot; public class Admin { private readonly BotConfig _botConfig; + private readonly DiscordApiClient _rest; - public Admin(BotConfig botConfig) + public Admin(BotConfig botConfig, DiscordApiClient rest) { _botConfig = botConfig; + _rest = rest; } public async Task UpdateSystemId(Context ctx) @@ -213,4 +218,52 @@ public class Admin await ctx.Repository.UpdateSystemConfig(target.Id, new SystemConfigPatch { GroupLimitOverride = newLimit }); await ctx.Reply($"{Emojis.Success} Group limit updated."); } + + public async Task SystemRecover(Context ctx) + { + ctx.AssertBotAdmin(); + + var rerollToken = ctx.MatchFlag("rt", "reroll-token"); + + var systemToken = ctx.PopArgument(); + var systemId = await ctx.Database.Execute(conn => conn.QuerySingleOrDefaultAsync( + "select id from systems where token = @token", + new { token = systemToken } + )); + + if (systemId == null) + throw new PKError("Could not retrieve a system with that token."); + + var account = await ctx.MatchUser(); + if (account == null) + throw new PKError("You must pass an account to associate the system with (either ID or @mention)."); + + var existingAccount = await ctx.Repository.GetSystemByAccount(account.Id); + if (existingAccount != null) + throw Errors.AccountInOtherSystem(existingAccount); + + var system = await ctx.Repository.GetSystem(systemId.Value!); + + if (!await ctx.PromptYesNo($"Associate account {account.NameAndMention()} with system `{system.Hid}`?", "Recover account")) + throw new PKError("System recovery cancelled."); + + await ctx.Repository.AddAccount(system.Id, account.Id); + if (rerollToken) + await ctx.Repository.UpdateSystem(system.Id, new SystemPatch { Token = StringUtils.GenerateToken() }); + + if ((await ctx.BotPermissions).HasFlag(PermissionSet.ManageMessages)) + await _rest.DeleteMessage(ctx.Message); + + await ctx.Reply(null, new Embed + { + Title = "System recovered", + Description = $"{account.NameAndMention()} has been linked to system `{system.Hid}`.", + Fields = new Embed.Field[] + { + new Embed.Field("Token rerolled?", rerollToken ? "yes" : "no", true), + new Embed.Field("Actioned by", ctx.Author.NameAndMention(), true), + }, + Color = DiscordUtils.Green, + }); + } } \ No newline at end of file