From 3dacdb6c7e9db52f57f67bc237df97f7b976fa7c Mon Sep 17 00:00:00 2001
From: Pythonic-Rainbow <wongchingho0@gmail.com>
Date: Sat, 7 Sep 2024 17:56:27 +0800
Subject: [PATCH] [Bot] Upgrade MemberConverter

It now accepts alias and discord user mention
---
 .github/CONTRIBUTING.md        |  7 ++-
 Bot/.gitignore                 |  2 +
 Bot/Discord/Cmds.cs            | 14 +++---
 Bot/Discord/MemberConverter.cs | 83 ++++++++++++++++++++++++++++------
 Bot/Discord/TypeConverters.cs  | 13 ++++++
 Bot/Regexes.cs                 |  9 ++++
 Bot/Sql/Db.cs                  | 10 +++-
 7 files changed, 115 insertions(+), 23 deletions(-)
 create mode 100644 Bot/Discord/TypeConverters.cs
 create mode 100644 Bot/Regexes.cs

diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 6f63919..4aa7264 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -2,6 +2,7 @@
 1. [Prerequisites](#prerequisites)
 2. [Structure Overview](#overview)
 3. [Coding Guidelines](#guidelines)
+4. [Dev Notes](#devnotes)
 
 <a name="prerequisites"></a>
 # Prerequisites
@@ -93,4 +94,8 @@ private List<uint> FindFactors(uint number) { }
 private async Task ReportToCIA() { }
 public uint ComputeFactorial(uint number) { }
 public async Task ProcessPayment() { }
-```
\ No newline at end of file
+```
+
+<a name="devnotes"></a>
+# Dev Notes
+1. Use `Coc.GetMember` or `Coc.TryGetMember` if you want to check whether a player tag is currently in the clan.
\ No newline at end of file
diff --git a/Bot/.gitignore b/Bot/.gitignore
index 41ab7f6..099ff68 100644
--- a/Bot/.gitignore
+++ b/Bot/.gitignore
@@ -8,6 +8,8 @@ Hyperstellar.db
 Hyperstellar
 hyperstellar-token.txt
 
+.idea
+
 # User-specific files
 *.rsuser
 *.suo
diff --git a/Bot/Discord/Cmds.cs b/Bot/Discord/Cmds.cs
index 61f3fed..9b806ef 100644
--- a/Bot/Discord/Cmds.cs
+++ b/Bot/Discord/Cmds.cs
@@ -11,7 +11,7 @@ namespace Hyperstellar.Discord;
 public class Cmds : InteractionModuleBase
 {
     [RequireOwner]
-    [SlashCommand("shutdown", "Shuts down the bot")]
+    [SlashCommand("shutdown", "[Owner] Shuts down the bot")]
     public async Task ShutdownAsync(bool commit = true)
     {
         await RespondAsync("Ok", ephemeral: true);
@@ -23,7 +23,7 @@ public async Task ShutdownAsync(bool commit = true)
     }
 
     [RequireOwner]
-    [SlashCommand("commit", "Commits db")]
+    [SlashCommand("commit", "[Owner] Commits db")]
     public async Task CommitAsync()
     {
         Db.Commit();
@@ -31,7 +31,7 @@ public async Task CommitAsync()
     }
 
     [RequireOwner]
-    [SlashCommand("admin", "Makes the Discord user an admin")] // Maybe rename to addadmin
+    [SlashCommand("admin", "[Owner] Makes the Discord user an admin")] // Maybe rename to addadmin
     public async Task AdminAsync(SocketGuildUser user)
     {
         bool success = Db.AddAdmin(user.Id);
@@ -46,7 +46,7 @@ public async Task AdminAsync(SocketGuildUser user)
     }
 
     [RequireAdmin]
-    [SlashCommand("alt", "Links an alt to a main")]
+    [SlashCommand("alt", "[Admin] Links an alt to a main")]
     public async Task AltAsync(Member alt, Member main)
     {
         if (alt.CocId == main.CocId)
@@ -72,8 +72,8 @@ public async Task AltAsync(Member alt, Member main)
     }
 
     [RequireAdmin]
-    [SlashCommand("link", "[Admin] Links a Discord account to a Main")]
-    public async Task LinkAsync(Member coc, IGuildUser discord)
+    [SlashCommand("discord", "[Admin] Links a Discord account to a Main")]
+    public async Task DiscordAsync(Member coc, IGuildUser discord)
     {
         Main? main = coc.TryToMain();
         if (main == null)
@@ -131,7 +131,7 @@ public async Task InfoAsync(Member clanMember)
     }
 
     [RequireAdmin]
-    [SlashCommand("alias", "Sets an alias for a Coc member")]
+    [SlashCommand("alias", "[Admin] Sets an alias for a Coc member")]
     public async Task AliasAsync(string alias, Member member)
     {
         bool success = Db.AddAlias(alias, member);
diff --git a/Bot/Discord/MemberConverter.cs b/Bot/Discord/MemberConverter.cs
index 75de7a9..1fbba22 100644
--- a/Bot/Discord/MemberConverter.cs
+++ b/Bot/Discord/MemberConverter.cs
@@ -1,7 +1,10 @@
-using Discord;
+using System.Text.RegularExpressions;
+using ClashOfClans.Models;
+using Discord;
 using Discord.Interactions;
 using Hyperstellar.Clash;
 using Hyperstellar.Sql;
+using Type = System.Type;
 
 namespace Hyperstellar.Discord;
 
@@ -12,22 +15,74 @@ internal sealed class MemberConverter : TypeConverter
     public override Task<TypeConverterResult> ReadAsync(IInteractionContext context, IApplicationCommandInteractionDataOption option, IServiceProvider services)
     {
         string input = (string)option.Value;
-        // Console.WriteLine($"\"{input}\"");
-        Member? member = Db.GetMember(input);
-        if (member == null)
+
+        // Check whether input matches a Discord user mention
+        Match dUserMentionMatch = Regexes.DiscordUserMention().Match(input);
+        if (dUserMentionMatch.Success)
         {
-            string? id = Coc.GetMemberId(input);
-            if (id != null)
+            // Extract uid from mention
+            ReadOnlySpan<char> captured = dUserMentionMatch.ValueSpan;  // <@123>
+            ulong uid = Convert.ToUInt64(captured[2..^1].ToString());  // 123
+
+            // Checks whether this Discord user is linked to a main
+            Main? main = Db.GetMainByDiscord(uid);
+            if (main == null)
             {
-                member = Db.GetMember(id);
+                return TypeConverters.Error("This Discord user isn't linked to any CoC account.");
             }
+
+            // REMOVE THIS AFTER DB REDESIGN - SKIPPING THE CHECK BELOW BECUZ FOR NOW, IN DB = MUST BE IN CLAN
+            Member sqlMember = Db.GetMember(main.MainId)!;
+            return TypeConverters.Success(sqlMember);
+
+            /*
+            // Check whether the main is still in the clan
+            string cocId = main.MainId;
+            ClanMember? member = Coc.TryGetMember(cocId);
+
+            return member == null
+                ? TypeConverters.Error("The main of this Discord user isn't in the clan.")
+                : Task.FromResult(TypeConverterResult.FromSuccess(member));
+            */
+        }
+
+        // Check whether input matches an alias
+        CocMemberAlias? dbAlias = Db.TryGetAlias(input);
+        if (dbAlias != null)
+        {
+            // Check whether the coc account of the alias is still in the clan
+            string aliasCocId = dbAlias.CocId;
+            ClanMember? aliasClanMember = Coc.TryGetMember(aliasCocId);
+            Member sqlMember = Db.GetMember(aliasCocId)!;
+
+            return aliasClanMember == null
+                ? TypeConverters.Error("The player of this alias isn't in the clan.")
+                : TypeConverters.Success(sqlMember);
         }
-        return member == null
-            ? Task.FromResult(TypeConverterResult.FromError(
-                InteractionCommandError.ConvertFailed,
-                @$"Invalid `{option.Name}`. To specify a clan member:
-* Enter his name (非速本主義Arkyo), alias (Arkyo) or ID with # (#28QL0CJV2)
-* Mention his Discord (@Dim) __which will refer to his main__"))
-            : Task.FromResult(TypeConverterResult.FromSuccess(member));
+
+        // Check whether input is the name of a clan member
+        string? cocId = Coc.GetMemberId(input);
+        if (cocId != null)
+        {
+            Member sqlMember = Db.GetMember(cocId)!;
+            return TypeConverters.Success(sqlMember);
+        }
+
+        // Check whether input is the tag of a clan member
+        ClanMember? member = Coc.TryGetMember(input);
+        if (member != null)
+        {
+            Member sqlMember = Db.GetMember(input)!;
+            return TypeConverters.Success(sqlMember);
+        }
+
+        return TypeConverters.Error(
+            $"""
+             Invalid `{option.Name}`. To specify a clan member:
+             * Enter his name (非速本主義Arkyo), alias (arkyo) or ID with # (#28QL0CJV2)
+             * Mention his Discord (@Dim) __which will refer to his main__
+             """);
     }
+
+
 }
diff --git a/Bot/Discord/TypeConverters.cs b/Bot/Discord/TypeConverters.cs
new file mode 100644
index 0000000..842a8ac
--- /dev/null
+++ b/Bot/Discord/TypeConverters.cs
@@ -0,0 +1,13 @@
+using Discord.Interactions;
+
+namespace Hyperstellar.Discord;
+
+// Util class for other type converters in this package
+internal static class TypeConverters
+{
+    // Shorthand for generating an error response
+    internal static Task<TypeConverterResult> Error(string reason) => Task.FromResult(TypeConverterResult.FromError(InteractionCommandError.ConvertFailed, reason));
+
+    // Shorthand for generating a successful conversion
+    internal static Task<TypeConverterResult> Success(object value) => Task.FromResult(TypeConverterResult.FromSuccess(value));
+}
diff --git a/Bot/Regexes.cs b/Bot/Regexes.cs
new file mode 100644
index 0000000..3de8d57
--- /dev/null
+++ b/Bot/Regexes.cs
@@ -0,0 +1,9 @@
+using System.Text.RegularExpressions;
+
+namespace Hyperstellar;
+
+internal static partial class Regexes
+{
+    [GeneratedRegex("^<@\\d{1,20}>$")]
+    internal static partial Regex DiscordUserMention();
+}
diff --git a/Bot/Sql/Db.cs b/Bot/Sql/Db.cs
index a888986..56cf20a 100644
--- a/Bot/Sql/Db.cs
+++ b/Bot/Sql/Db.cs
@@ -27,6 +27,8 @@ internal static bool AddMembers(string[] members)
 
     internal static Main? GetMain(string id) => s_db.Table<Main>().FirstOrDefault(d => d.MainId == id);
 
+    internal static Main? GetMainByDiscord(ulong uid) => s_db.Table<Main>().FirstOrDefault(m => m.Discord == uid);
+
     internal static IEnumerable<Main> GetDonations() => s_db.Table<Main>();
 
     internal static bool UpdateMain(Main main) => s_db.Update(main) == 1;
@@ -41,11 +43,17 @@ internal static bool AddAdmin(ulong id)
         return count == 1;
     }
 
+    internal static CocMemberAlias? TryGetAlias(string alias)
+    {
+        alias = alias.ToLower();
+        return s_db.Table<CocMemberAlias>().FirstOrDefault(a => a.Alias == alias);
+    }
+
     internal static IEnumerable<CocMemberAlias> GetAliases() => s_db.Table<CocMemberAlias>();
 
     internal static bool AddAlias(string alias, Member member)
     {
-        CocMemberAlias cocMemberAlias = new(alias, member.CocId);
+        CocMemberAlias cocMemberAlias = new(alias.ToLower(), member.CocId);
         int count = s_db.Insert(cocMemberAlias);
         return count == 1;
     }