diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index bdb9f831..6559a6e9 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -8,6 +8,7 @@ public partial class CommandTree { return command switch { + Commands.Dashboard => ctx.Execute(Dashboard, m => m.Dashboard(ctx)), Commands.Explain => ctx.Execute(Explain, m => m.Explain(ctx)), Commands.Help(_, var flags) => ctx.Execute(Help, m => m.HelpRoot(ctx, flags.show_embed)), Commands.HelpCommands => ctx.Reply( @@ -244,9 +245,33 @@ public partial class CommandTree Commands.MessageAuthor(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), false, true)), Commands.MessageDelete(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), true, false)), Commands.MessageEdit(var param, var flags) => ctx.Execute(MessageEdit, m => m.EditMessage(ctx, param.target.MessageId, param.new_content, flags.regex, flags.mutate_space, flags.append, flags.prepend, flags.clear_embeds, flags.clear_attachments)), - Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.target.MessageId)), + Commands.MessageReproxy(var param, _) => ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx, param.msg.MessageId, param.member)), Commands.Import(var param, _) => ctx.Execute(Import, m => m.Import(ctx, param.url)), Commands.Export(_, _) => ctx.Execute(Export, m => m.Export(ctx)), + Commands.AdminUpdateSystemId(var param, _) => ctx.Execute(null, m => m.UpdateSystemId(ctx, param.target, param.new_hid)), + Commands.AdminUpdateMemberId(var param, _) => ctx.Execute(null, m => m.UpdateMemberId(ctx, param.target, param.new_hid)), + Commands.AdminUpdateGroupId(var param, _) => ctx.Execute(null, m => m.UpdateGroupId(ctx, param.target, param.new_hid)), + Commands.AdminRerollSystemId(var param, _) => ctx.Execute(null, m => m.RerollSystemId(ctx, param.target)), + Commands.AdminRerollMemberId(var param, _) => ctx.Execute(null, m => m.RerollMemberId(ctx, param.target)), + Commands.AdminRerollGroupId(var param, _) => ctx.Execute(null, m => m.RerollGroupId(ctx, param.target)), + Commands.AdminSystemMemberLimit(var param, _) => ctx.Execute(null, m => m.SystemMemberLimit(ctx, param.target, param.limit)), + Commands.AdminSystemGroupLimit(var param, _) => ctx.Execute(null, m => m.SystemGroupLimit(ctx, param.target, param.limit)), + Commands.AdminSystemRecover(var param, var flags) => ctx.Execute(null, m => m.SystemRecover(ctx, param.token, param.account, flags.reroll_token)), + Commands.AdminSystemDelete(var param, _) => ctx.Execute(null, m => m.SystemDelete(ctx, param.target)), + Commands.AdminSendMessage(var param, _) => ctx.Execute(null, m => m.SendAdminMessage(ctx, param.account, param.content)), + Commands.AdminAbuselogCreate(var param, var flags) => ctx.Execute(null, m => m.AbuseLogCreate(ctx, param.account, flags.deny_boy_usage, param.description)), + Commands.AdminAbuselogShowAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogShow(ctx, param.account, null)), + Commands.AdminAbuselogFlagDenyAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogFlagDeny(ctx, param.account, null, param.value)), + Commands.AdminAbuselogDescriptionAccount(var param, var flags) => ctx.Execute(null, m => m.AbuseLogDescription(ctx, param.account, null, param.desc, flags.clear)), + Commands.AdminAbuselogAddUserAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogAddUser(ctx, param.account, null, ctx.Author)), + Commands.AdminAbuselogRemoveUserAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogRemoveUser(ctx, param.account, null, ctx.Author)), + Commands.AdminAbuselogDeleteAccount(var param, _) => ctx.Execute(null, m => m.AbuseLogDelete(ctx, param.account, null)), + Commands.AdminAbuselogShowLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogShow(ctx, null, param.log_id)), + Commands.AdminAbuselogFlagDenyLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogFlagDeny(ctx, null, param.log_id, param.value)), + Commands.AdminAbuselogDescriptionLogId(var param, var flags) => ctx.Execute(null, m => m.AbuseLogDescription(ctx, null, param.log_id, param.desc, flags.clear)), + Commands.AdminAbuselogAddUserLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogAddUser(ctx, null, param.log_id, ctx.Author)), + Commands.AdminAbuselogRemoveUserLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogRemoveUser(ctx, null, param.log_id, ctx.Author)), + Commands.AdminAbuselogDeleteLogId(var param, _) => ctx.Execute(null, m => m.AbuseLogDelete(ctx, null, param.log_id)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -282,82 +307,6 @@ public partial class CommandTree return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `{ctx.DefaultPrefix}serverconfig`."); if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); if (ctx.Match("stats", "status")) return ctx.Execute(null, m => m.Stats(ctx)); - if (ctx.Match("admin")) - return HandleAdminCommand(ctx); - if (ctx.Match("dashboard", "dash")) - return ctx.Execute(Dashboard, m => m.Dashboard(ctx)); - } - - private async Task HandleAdminAbuseLogCommand(Context ctx) - { - ctx.AssertBotAdmin(); - - if (ctx.Match("n", "new", "create")) - await ctx.Execute(Admin, a => a.AbuseLogCreate(ctx)); - else - { - AbuseLog? abuseLog = null!; - var account = await ctx.MatchUser(); - if (account != null) - { - abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id); - } - else - { - abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(ctx.PopArgument())); - } - - if (abuseLog == null) - { - await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query."); - return; - } - - if (!ctx.HasNext()) - await ctx.Execute(Admin, a => a.AbuseLogShow(ctx, abuseLog)); - else if (ctx.Match("au", "adduser")) - await ctx.Execute(Admin, a => a.AbuseLogAddUser(ctx, abuseLog)); - else if (ctx.Match("ru", "removeuser")) - await ctx.Execute(Admin, a => a.AbuseLogRemoveUser(ctx, abuseLog)); - else if (ctx.Match("desc", "description")) - await ctx.Execute(Admin, a => a.AbuseLogDescription(ctx, abuseLog)); - else if (ctx.Match("deny", "deny-bot-usage")) - await ctx.Execute(Admin, a => a.AbuseLogFlagDeny(ctx, abuseLog)); - else if (ctx.Match("yeet", "remove", "delete")) - await ctx.Execute(Admin, a => a.AbuseLogDelete(ctx, abuseLog)); - else - await ctx.Reply($"{Emojis.Error} Unknown subcommand {ctx.PeekArgument().AsCode()}."); - } - } - - private async Task HandleAdminCommand(Context ctx) - { - if (ctx.Match("usid", "updatesystemid")) - await ctx.Execute(Admin, a => a.UpdateSystemId(ctx)); - else if (ctx.Match("umid", "updatememberid")) - 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")) - await ctx.Execute(Admin, a => a.SystemGroupLimit(ctx)); - else if (ctx.Match("sr", "systemrecover")) - await ctx.Execute(Admin, a => a.SystemRecover(ctx)); - else if (ctx.Match("sd", "systemdelete")) - await ctx.Execute(Admin, a => a.SystemDelete(ctx)); - else if (ctx.Match("sendmsg", "sendmessage")) - await ctx.Execute(Admin, a => a.SendAdminMessage(ctx)); - else if (ctx.Match("al", "abuselog")) - await HandleAdminAbuseLogCommand(ctx); - else - await ctx.Reply($"{Emojis.Error} Unknown command."); } private async Task CommandHelpRoot(Context ctx) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index 43d87375..627c1185 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -18,37 +18,6 @@ public static class ContextEntityArgumentsExt return null; } - public static async Task MatchUser(this Context ctx) - { - var text = ctx.PeekArgument(); - if (text.TryParseMention(out var id)) - { - var user = await ctx.Cache.GetOrFetchUser(ctx.Rest, id); - if (user != null) ctx.PopArgument(); - return user; - } - - return null; - } - - public static bool MatchUserRaw(this Context ctx, out ulong id) - { - id = 0; - - var text = ctx.PeekArgument(); - if (text.TryParseMention(out var mentionId)) - id = mentionId; - - return id != 0; - } - - public static Task PeekSystem(this Context ctx) => throw new NotImplementedException(); - - public static async Task MatchSystem(this Context ctx) - { - throw new NotImplementedException(); - } - public static async Task ParseSystem(this Context ctx, string input) { // System references can take three forms: @@ -67,7 +36,7 @@ public static class ContextEntityArgumentsExt return null; } - public static async Task ParseMember(this Context ctx, string input, bool byId, SystemId? restrictToSystem = null) + public static async Task ParseMember(this Context ctx, string input, bool byId) { // Member references can have one of three forms, depending on // whether you're in a system or not: @@ -100,53 +69,22 @@ public static class ContextEntityArgumentsExt // If we are supposed to restrict it to a system anyway we can just do that PKMember memberByHid = null; - if (restrictToSystem != null) - { - memberByHid = await ctx.Repository.GetMemberByHid(hid, restrictToSystem); - if (memberByHid != null) - return memberByHid; - } - // otherwise we try the querier's system and if that doesn't work we do global - else - { - memberByHid = await ctx.Repository.GetMemberByHid(hid, ctx.System?.Id); - if (memberByHid != null) - return memberByHid; + memberByHid = await ctx.Repository.GetMemberByHid(hid, ctx.System?.Id); + if (memberByHid != null) + return memberByHid; - // ff ctx.System was null then this would be a duplicate of above and we don't want to run it again - if (ctx.System != null) - { - memberByHid = await ctx.Repository.GetMemberByHid(hid); - if (memberByHid != null) - return memberByHid; - } + // ff ctx.System was null then this would be a duplicate of above and we don't want to run it again + if (ctx.System != null) + { + memberByHid = await ctx.Repository.GetMemberByHid(hid); + if (memberByHid != null) + return memberByHid; } // We didn't find anything, so we return null. return null; } - public static async Task PeekMember(this Context ctx, SystemId? restrictToSystem = null) - { - throw new NotImplementedException(); - } - - /// - /// Attempts to pop a member descriptor from the stack, returning it if present. If a member could not be - /// resolved by the next word in the argument stack, does *not* touch the stack, and returns null. - /// - public static async Task MatchMember(this Context ctx, SystemId? restrictToSystem = null) - { - // First, peek a member - var member = await ctx.PeekMember(restrictToSystem); - - // If the peek was successful, we've used up the next argument, so we pop that just to get rid of it. - if (member != null) ctx.PopArgument(); - - // Finally, we return the member value. - return member; - } - public static async Task ParseGroup(this Context ctx, string input, bool byId, SystemId? restrictToSystem = null) { if (ctx.System != null && !byId) @@ -166,18 +104,6 @@ public static class ContextEntityArgumentsExt return null; } - public static async Task PeekGroup(this Context ctx, SystemId? restrictToSystem = null) - { - throw new NotImplementedException(); - } - - public static async Task MatchGroup(this Context ctx, SystemId? restrictToSystem = null) - { - var group = await ctx.PeekGroup(restrictToSystem); - if (group != null) ctx.PopArgument(); - return group; - } - public static string CreateNotFoundError(this Context ctx, string entity, string input, bool byId = false) { var isIDOnlyQuery = ctx.System == null || byId; diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index cc101e46..53e6de76 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -1,4 +1,5 @@ using PluralKit.Core; +using Myriad.Types; namespace PluralKit.Bot; @@ -12,6 +13,14 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveNumber(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.Number)?.value + ); + } + public static async Task ParamResolveMember(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( @@ -52,6 +61,14 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveUser(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.UserRef)?.user + ); + } + public static async Task ParamResolveMemberPrivacyTarget(this Context ctx, string param_name) { return await ctx.Parameters.ResolveParameter( diff --git a/PluralKit.Bot/CommandSystem/Parameters.cs b/PluralKit.Bot/CommandSystem/Parameters.cs index 22bbebf4..d6ae35e4 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -1,5 +1,6 @@ using Humanizer; using Myriad.Types; +using Myriad.Extensions; using PluralKit.Core; using uniffi.commands; @@ -13,6 +14,7 @@ public abstract record Parameter() public record GroupRef(PKGroup group): Parameter; public record GroupRefs(List groups): Parameter; public record SystemRef(PKSystem system): Parameter; + public record UserRef(User user): Parameter; public record MessageRef(Message.Reference message): Parameter; public record ChannelRef(Channel channel): Parameter; public record GuildRef(Guild guild): Parameter; @@ -22,6 +24,7 @@ public abstract record Parameter() public record PrivacyLevel(Core.PrivacyLevel level): Parameter; public record Toggle(bool value): Parameter; public record Opaque(string value): Parameter; + public record Number(int value): Parameter; public record Avatar(ParsedImage avatar): Parameter; } @@ -96,6 +99,11 @@ public class Parameters await ctx.ParseSystem(systemRef.system) ?? throw new PKError(ctx.CreateNotFoundError("System", systemRef.system)) ); + case uniffi.commands.Parameter.UserRef(var userId): + return new Parameter.UserRef( + await ctx.Cache.GetOrFetchUser(ctx.Rest, userId) + ?? throw new PKError(ctx.CreateNotFoundError("User", userId.ToString())) + ); // todo(dusk): ideally generate enums for these from rust code in the cs glue case uniffi.commands.Parameter.MemberPrivacyTarget memberPrivacyTarget: // this should never really fail... @@ -118,6 +126,8 @@ public class Parameters return new Parameter.Toggle(toggle.toggle); case uniffi.commands.Parameter.OpaqueString opaque: return new Parameter.Opaque(opaque.raw); + case uniffi.commands.Parameter.OpaqueInt number: + return new Parameter.Number(number.raw); case uniffi.commands.Parameter.Avatar avatar: return new Parameter.Avatar(await ctx.GetUserPfp(avatar.avatar) ?? ctx.ParseImage(avatar.avatar)); case uniffi.commands.Parameter.MessageRef(var guildId, var channelId, var messageId): diff --git a/PluralKit.Bot/Commands/Admin.cs b/PluralKit.Bot/Commands/Admin.cs index 44171345..e65e6bc1 100644 --- a/PluralKit.Bot/Commands/Admin.cs +++ b/PluralKit.Bot/Commands/Admin.cs @@ -1,5 +1,3 @@ -using System.Text.RegularExpressions; - using Humanizer; using Dapper; using SqlKata; @@ -113,18 +111,10 @@ public class Admin return eb.Build(); } - public async Task UpdateSystemId(Context ctx) + public async Task UpdateSystemId(Context ctx, PKSystem target, string newHid) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - - var input = ctx.PopArgument(); - if (!input.TryParseHid(out var newHid)) - throw new PKError($"Invalid new system ID `{input}`."); - var existingSystem = await ctx.Repository.GetSystemByHid(newHid); if (existingSystem != null) throw new PKError($"Another system already exists with ID `{newHid}`."); @@ -138,18 +128,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task UpdateMemberId(Context ctx) + public async Task UpdateMemberId(Context ctx, PKMember target, string newHid) { ctx.AssertBotAdmin(); - var target = await ctx.MatchMember(); - if (target == null) - throw new PKError("Unknown member."); - - var input = ctx.PopArgument(); - if (!input.TryParseHid(out var newHid)) - throw new PKError($"Invalid new member ID `{input}`."); - var existingMember = await ctx.Repository.GetMemberByHid(newHid); if (existingMember != null) throw new PKError($"Another member already exists with ID `{newHid}`."); @@ -167,18 +149,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task UpdateGroupId(Context ctx) + public async Task UpdateGroupId(Context ctx, PKGroup target, string newHid) { ctx.AssertBotAdmin(); - var target = await ctx.MatchGroup(); - if (target == null) - throw new PKError("Unknown group."); - - var input = ctx.PopArgument(); - if (!input.TryParseHid(out var newHid)) - throw new PKError($"Invalid new group ID `{input}`."); - var existingGroup = await ctx.Repository.GetGroupByHid(newHid); if (existingGroup != null) throw new PKError($"Another group already exists with ID `{newHid}`."); @@ -195,14 +169,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollSystemId(Context ctx) + public async Task RerollSystemId(Context ctx, PKSystem target) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - await ctx.Reply(null, await CreateEmbed(ctx, target)); if (!await ctx.PromptYesNo($"Reroll system ID `{target.Hid}`?", "Reroll")) @@ -218,14 +188,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} System ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollMemberId(Context ctx) + public async Task RerollMemberId(Context ctx, PKMember target) { ctx.AssertBotAdmin(); - var target = await ctx.MatchMember(); - if (target == null) - throw new PKError("Unknown member."); - var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply(null, await CreateEmbed(ctx, system)); @@ -245,14 +211,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Member ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task RerollGroupId(Context ctx) + public async Task RerollGroupId(Context ctx, PKGroup target) { ctx.AssertBotAdmin(); - var target = await ctx.MatchGroup(); - if (target == null) - throw new PKError("Unknown group."); - var system = await ctx.Repository.GetSystem(target.System); await ctx.Reply(null, await CreateEmbed(ctx, system)); @@ -271,27 +233,19 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group ID updated (`{target.Hid}` -> `{newHid}`)."); } - public async Task SystemMemberLimit(Context ctx) + public async Task SystemMemberLimit(Context ctx, PKSystem target, int? newLimit) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - var config = await ctx.Repository.GetSystemConfig(target.Id); var currentLimit = config.MemberLimitOverride ?? Limits.MaxMemberCount; - if (!ctx.HasNext()) + if (newLimit == null) { await ctx.Reply(null, await CreateEmbed(ctx, target)); return; } - var newLimitStr = ctx.PopArgument().ToLower().Replace(",", null).Replace("k", "000"); - if (!int.TryParse(newLimitStr, out var newLimit)) - throw new PKError($"Couldn't parse `{newLimitStr}` as number."); - await ctx.Reply(null, await CreateEmbed(ctx, target)); if (!await ctx.PromptYesNo($"Update member limit from **{currentLimit}** to **{newLimit}**?", "Update")) throw new PKError("Member limit change cancelled."); @@ -300,27 +254,19 @@ public class Admin await ctx.Reply($"{Emojis.Success} Member limit updated."); } - public async Task SystemGroupLimit(Context ctx) + public async Task SystemGroupLimit(Context ctx, PKSystem target, int? newLimit) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - var config = await ctx.Repository.GetSystemConfig(target.Id); var currentLimit = config.GroupLimitOverride ?? Limits.MaxGroupCount; - if (!ctx.HasNext()) + if (newLimit == null) { await ctx.Reply(null, await CreateEmbed(ctx, target)); return; } - var newLimitStr = ctx.PopArgument().ToLower().Replace(",", null).Replace("k", "000"); - if (!int.TryParse(newLimitStr, out var newLimit)) - throw new PKError($"Couldn't parse `{newLimitStr}` as number."); - await ctx.Reply(null, await CreateEmbed(ctx, target)); if (!await ctx.PromptYesNo($"Update group limit from **{currentLimit}** to **{newLimit}**?", "Update")) throw new PKError("Group limit change cancelled."); @@ -329,13 +275,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Group limit updated."); } - public async Task SystemRecover(Context ctx) + public async Task SystemRecover(Context ctx, string systemToken, User account, bool rerollToken) { 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 } @@ -344,10 +287,6 @@ public class Admin 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, ctx.Config, ctx.DefaultPrefix); @@ -378,14 +317,10 @@ public class Admin }); } - public async Task SystemDelete(Context ctx) + public async Task SystemDelete(Context ctx, PKSystem target) { ctx.AssertBotAdmin(); - var target = await ctx.MatchSystem(); - if (target == null) - throw new PKError("Unknown system."); - await ctx.Reply($"To delete the following system, reply with the system's UUID: `{target.Uuid.ToString()}`", await CreateEmbed(ctx, target)); if (!await ctx.ConfirmWithReply(target.Uuid.ToString())) @@ -396,18 +331,11 @@ public class Admin await ctx.Reply($"{Emojis.Success} System deletion succesful."); } - public async Task AbuseLogCreate(Context ctx) + public async Task AbuseLogCreate(Context ctx, User account, bool denyBotUsage, string? description) { - var denyBotUsage = ctx.MatchFlag("deny", "deny-bot-usage"); - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention)."); + ctx.AssertBotAdmin(); - string? desc = null!; - if (ctx.HasNext(false)) - desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - - var abuseLog = await ctx.Repository.CreateAbuseLog(desc, denyBotUsage); + var abuseLog = await ctx.Repository.CreateAbuseLog(description, denyBotUsage); await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id); await ctx.Reply( @@ -415,14 +343,49 @@ public class Admin await CreateAbuseLogEmbed(ctx, abuseLog)); } - public async Task AbuseLogShow(Context ctx, AbuseLog abuseLog) + public async Task GetAbuseLog(Context ctx, User? account, string? id) { + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = null!; + if (account != null) + { + abuseLog = await ctx.Repository.GetAbuseLogByAccount(account.Id); + } + else + { + abuseLog = await ctx.Repository.GetAbuseLogByGuid(new Guid(id)); + } + + if (abuseLog == null) + { + await ctx.Reply($"{Emojis.Error} Could not find an existing abuse log entry for that query."); + return null; + } + + return abuseLog; + } + + public async Task AbuseLogShow(Context ctx, User? account, string? id) + { + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id); + if (abuseLog == null) + return; + await ctx.Reply(null, await CreateAbuseLogEmbed(ctx, abuseLog)); } - public async Task AbuseLogFlagDeny(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogFlagDeny(Context ctx, User? account, string? id, bool? value) { - if (!ctx.HasNext()) + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id); + if (abuseLog == null) + return; + + if (value == null) { await ctx.Reply( $"Bot usage is currently {(abuseLog.DenyBotUsage ? "denied" : "allowed")} " @@ -430,27 +393,31 @@ public class Admin } else { - var value = ctx.MatchToggle(true); if (abuseLog.DenyBotUsage != value) - await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value }); + await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { DenyBotUsage = value.Value }); await ctx.Reply( - $"Bot usage is now **{(value ? "denied" : "allowed")}** " + $"Bot usage is now **{(value.Value ? "denied" : "allowed")}** " + $"for accounts associated with abuse log `{abuseLog.Uuid}`."); } } - public async Task AbuseLogDescription(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogDescription(Context ctx, User? account, string? id, string? description, bool clear) { - if (ctx.MatchClear() && await ctx.ConfirmClear("this abuse log description")) + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id); + if (abuseLog == null) + return; + + if (clear && await ctx.ConfirmClear("this abuse log description")) { await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = null }); await ctx.Reply($"{Emojis.Success} Abuse log description cleared."); } - else if (ctx.HasNext()) + else if (description != null) { - var desc = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); - await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = desc }); + await ctx.Repository.UpdateAbuseLog(abuseLog.Id, new AbuseLogPatch { Description = description }); await ctx.Reply($"{Emojis.Success} Abuse log description updated."); } else @@ -461,11 +428,13 @@ public class Admin } } - public async Task AbuseLogAddUser(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogAddUser(Context ctx, User? accountToFind, string? id, User account) { - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to associate the abuse log with (either ID or @mention)."); + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, accountToFind, id); + if (abuseLog == null) + return; await ctx.Repository.AddAbuseLogAccount(abuseLog.Id, account.Id); await ctx.Reply( @@ -473,11 +442,13 @@ public class Admin await CreateAbuseLogEmbed(ctx, abuseLog)); } - public async Task AbuseLogRemoveUser(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogRemoveUser(Context ctx, User? accountToFind, string? id, User account) { - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to remove from the abuse log (either ID or @mention)."); + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, accountToFind, id); + if (abuseLog == null) + return; await ctx.Repository.UpdateAccount(account.Id, new() { @@ -489,8 +460,14 @@ public class Admin await CreateAbuseLogEmbed(ctx, abuseLog)); } - public async Task AbuseLogDelete(Context ctx, AbuseLog abuseLog) + public async Task AbuseLogDelete(Context ctx, User? account, string? id) { + ctx.AssertBotAdmin(); + + AbuseLog? abuseLog = await GetAbuseLog(ctx, account, id); + if (abuseLog == null) + return; + if (!await ctx.PromptYesNo($"Really delete abuse log entry `{abuseLog.Uuid}`?", "Delete", matchFlag: false)) { await ctx.Reply($"{Emojis.Error} Deletion cancelled."); @@ -501,17 +478,10 @@ public class Admin await ctx.Reply($"{Emojis.Success} Successfully deleted abuse log entry."); } - public async Task SendAdminMessage(Context ctx) + public async Task SendAdminMessage(Context ctx, User account, string content) { ctx.AssertBotAdmin(); - var account = await ctx.MatchUser(); - if (account == null) - throw new PKError("You must pass an account to send an admin message to (either ID or @mention)."); - if (!ctx.HasNext()) - throw new PKError("You must provide a message to send."); - - var content = ctx.RemainderOrNull(false).NormalizeLineEndSpacing(); var messageContent = $"## [Admin Message]\n\n{content}\n\nWe cannot read replies sent to this DM. If you wish to contact the staff team, please join the support server () or send us an email at ."; try diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index 0395cf87..7bb1f681 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -58,16 +58,14 @@ public class ProxiedMessage _redisService = redisService; } - public async Task ReproxyMessage(Context ctx, ulong? messageId) + public async Task ReproxyMessage(Context ctx, ulong? messageId, PKMember target) { var (msg, systemId) = await GetMessageToEdit(ctx, messageId, ReproxyTimeout, true); if (ctx.System.Id != systemId) throw new PKError("Can't reproxy a message sent by a different system."); - // Get target member ID - var target = await ctx.MatchMember(restrictToSystem: ctx.System.Id); - if (target == null) + if (target == null || target.System != ctx.System.Id) throw new PKError("Could not find a member to reproxy the message with."); // Fetch members and get the ProxyMember for `target` diff --git a/crates/command_definitions/src/admin.rs b/crates/command_definitions/src/admin.rs index 8b137891..56f4dd71 100644 --- a/crates/command_definitions/src/admin.rs +++ b/crates/command_definitions/src/admin.rs @@ -1 +1,64 @@ +use super::*; +pub fn admin() -> &'static str { + "admin" +} + +pub fn cmds() -> impl Iterator { + let admin = admin(); + + let abuselog = tokens!(admin, ("abuselog", ["al"])); + let make_abuselog_cmds = |log_param: Parameter| { + [ + command!(abuselog, ("show", ["s"]), log_param => format!("admin_abuselog_show_{}", log_param.name())) + .help("Shows an abuse log entry"), + command!(abuselog, ("flagdeny", ["fd"]), log_param, Optional(("value", Toggle)) => format!("admin_abuselog_flag_deny_{}", log_param.name())) + .help("Sets the deny flag on an abuse log entry"), + command!(abuselog, ("description", ["desc"]), log_param, Optional(("desc", OpaqueStringRemainder)) => format!("admin_abuselog_description_{}", log_param.name())) + .flag(("clear", ["c"])) + .help("Sets the description of an abuse log entry"), + command!(abuselog, ("adduser", ["au"]), log_param => format!("admin_abuselog_add_user_{}", log_param.name())) + .help("Adds a user to an abuse log entry"), + command!(abuselog, ("removeuser", ["ru"]), log_param => format!("admin_abuselog_remove_user_{}", log_param.name())) + .help("Removes a user from an abuse log entry"), + command!(abuselog, ("delete", ["d"]), log_param => format!("admin_abuselog_delete_{}", log_param.name())) + .help("Deletes an abuse log entry"), + ].into_iter() + }; + let abuselog_cmds = [ + command!(abuselog, ("create", ["c", "new"]), ("account", UserRef), Optional(("description", OpaqueStringRemainder)) => "admin_abuselog_create") + .flag(("deny-boy-usage", ["deny"])) + .help("Creates an abuse log entry") + ] + .into_iter() + .chain(make_abuselog_cmds(Skip(("account", UserRef)).into())) // falls through to log_id + .chain(make_abuselog_cmds(("log_id", OpaqueString).into())); + + [ + command!(admin, ("updatesystemid", ["usid"]), SystemRef, ("new_hid", OpaqueString) => "admin_update_system_id") + .help("Updates a system's ID"), + command!(admin, ("updatememberid", ["umid"]), MemberRef, ("new_hid", OpaqueString) => "admin_update_member_id") + .help("Updates a member's ID"), + command!(admin, ("updategroupid", ["ugid"]), GroupRef, ("new_hid", OpaqueString) => "admin_update_group_id") + .help("Updates a group's ID"), + command!(admin, ("rerollsystemid", ["rsid"]), SystemRef => "admin_reroll_system_id") + .help("Rerolls a system's ID"), + command!(admin, ("rerollmemberid", ["rmid"]), MemberRef => "admin_reroll_member_id") + .help("Rerolls a member's ID"), + command!(admin, ("rerollgroupid", ["rgid"]), GroupRef => "admin_reroll_group_id") + .help("Rerolls a group's ID"), + command!(admin, ("updatememberlimit", ["uml"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_member_limit") + .help("Updates a system's member limit"), + command!(admin, ("updategrouplimit", ["ugl"]), SystemRef, Optional(("limit", OpaqueInt)) => "admin_system_group_limit") + .help("Updates a system's group limit"), + command!(admin, ("systemrecover", ["sr"]), ("token", OpaqueString), ("account", UserRef) => "admin_system_recover") + .flag(("reroll-token", ["rt"])) + .help("Recovers a system"), + command!(admin, ("systemdelete", ["sd"]), SystemRef => "admin_system_delete") + .help("Deletes a system"), + command!(admin, ("sendmessage", ["sendmsg"]), ("account", UserRef), ("content", OpaqueStringRemainder) => "admin_send_message") + .help("Sends a message to a user"), + ] + .into_iter() + .chain(abuselog_cmds) +} diff --git a/crates/command_definitions/src/dashboard.rs b/crates/command_definitions/src/dashboard.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/command_definitions/src/dashboard.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index 9a94dea8..8991cf72 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -5,9 +5,7 @@ pub fn cmds() -> impl Iterator { [ command!(("dashboard", ["dash"]) => "dashboard"), command!("explain" => "explain"), - command!(help => "help") - .flag(("foo", OpaqueString)) // todo: just for testing - .help("Shows the help command"), + command!(help => "help").help("Shows the help command"), command!(help, "commands" => "help_commands").help("help commands"), command!(help, "proxy" => "help_proxy").help("help proxy"), ] diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index 4204dad2..2fbc00fb 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -3,7 +3,6 @@ pub mod api; pub mod autoproxy; pub mod commands; pub mod config; -pub mod dashboard; pub mod debug; pub mod fun; pub mod group; @@ -40,10 +39,12 @@ pub fn all() -> impl Iterator { .chain(debug::cmds()) .chain(message::cmds()) .chain(import_export::cmds()) + .chain(admin::cmds()) .map(|cmd| { cmd.hidden_flag(("plaintext", ["pt"])) .hidden_flag(("raw", ["r"])) .hidden_flag(("show-embed", ["se"])) + .hidden_flag(("by-id", ["id"])) }) } diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index eeb2e53e..e36969c2 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -25,7 +25,7 @@ pub fn cmds() -> impl Iterator { .help("Deletes a proxied message"), apply_edit(command!(message, edit => "message_edit")), apply_edit(command!(edit => "message_edit")), - command!(("reproxy", ["rp", "crimes", "crime"]), MessageRef => "message_reproxy") + command!(("reproxy", ["rp", "crimes", "crime"]), ("msg", MessageRef), ("member", MemberRef) => "message_reproxy") .help("Reproxies a message with a different member"), ] .into_iter() diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 28d7a693..04d035ea 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -3,18 +3,21 @@ use std::{ str::FromStr, }; -use smol_str::SmolStr; +use regex::Regex; +use smol_str::{SmolStr, format_smolstr}; use crate::token::{Token, TokenMatchResult}; #[derive(Debug, Clone)] pub enum ParameterValue { OpaqueString(String), + OpaqueInt(i32), MemberRef(String), MemberRefs(Vec), GroupRef(String), GroupRefs(Vec), SystemRef(String), + UserRef(u64), MessageRef(Option, Option, u64), ChannelRef(u64), GuildRef(u64), @@ -85,6 +88,10 @@ impl Parameter { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => { Ok(ParameterValue::OpaqueString(input.into())) } + ParameterKind::OpaqueInt => input + .parse::() + .map(|num| ParameterValue::OpaqueInt(num)) + .map_err(|err| format_smolstr!("invalid integer: {err}")), ParameterKind::GroupRef => Ok(ParameterValue::GroupRef(input.into())), ParameterKind::GroupRefs => Ok(ParameterValue::GroupRefs( input.split(' ').map(|s| s.trim().to_string()).collect(), @@ -94,6 +101,22 @@ impl Parameter { input.split(' ').map(|s| s.trim().to_string()).collect(), )), ParameterKind::SystemRef => Ok(ParameterValue::SystemRef(input.into())), + ParameterKind::UserRef => { + if let Ok(user_id) = input.parse::() { + return Ok(ParameterValue::UserRef(user_id)); + } + + static RE: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r"<@!?(\\d{17,19})>").unwrap()); + if let Some(captures) = RE.captures(&input) { + return captures[1] + .parse::() + .map(|id| ParameterValue::UserRef(id)) + .map_err(|_| SmolStr::new("invalid user ID")); + } + + Err(SmolStr::new("invalid user ID")) + } ParameterKind::MemberPrivacyTarget => MemberPrivacyTargetKind::from_str(input) .map(|target| ParameterValue::MemberPrivacyTarget(target.as_ref().into())), ParameterKind::GroupPrivacyTarget => GroupPrivacyTargetKind::from_str(input) @@ -166,6 +189,9 @@ impl Display for Parameter { ParameterKind::OpaqueString => { write!(f, "[{}]", self.name) } + ParameterKind::OpaqueInt => { + write!(f, "[{}]", self.name) + } ParameterKind::OpaqueStringRemainder => { write!(f, "[{}]...", self.name) } @@ -174,6 +200,7 @@ impl Display for Parameter { ParameterKind::GroupRef => write!(f, ""), ParameterKind::GroupRefs => write!(f, " ..."), ParameterKind::SystemRef => write!(f, ""), + ParameterKind::UserRef => write!(f, ""), ParameterKind::MessageRef => write!(f, ""), ParameterKind::ChannelRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), @@ -246,12 +273,14 @@ impl> From> for Parameter { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ParameterKind { OpaqueString, + OpaqueInt, OpaqueStringRemainder, MemberRef, MemberRefs, GroupRef, GroupRefs, SystemRef, + UserRef, MessageRef, ChannelRef, GuildRef, @@ -267,12 +296,14 @@ impl ParameterKind { pub(crate) fn default_name(&self) -> &str { match self { ParameterKind::OpaqueString => "string", + ParameterKind::OpaqueInt => "number", ParameterKind::OpaqueStringRemainder => "string", ParameterKind::MemberRef => "target", ParameterKind::MemberRefs => "targets", ParameterKind::GroupRef => "target", ParameterKind::GroupRefs => "targets", ParameterKind::SystemRef => "target", + ParameterKind::UserRef => "target", ParameterKind::MessageRef => "target", ParameterKind::ChannelRef => "target", ParameterKind::GuildRef => "target", diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index ff1a0886..f94aed77 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -260,11 +260,13 @@ fn command_callback_to_name(cb: &str) -> String { fn get_param_ty(kind: ParameterKind) -> &'static str { match kind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "string", + ParameterKind::OpaqueInt => "int", ParameterKind::MemberRef => "PKMember", ParameterKind::MemberRefs => "List", ParameterKind::GroupRef => "PKGroup", ParameterKind::GroupRefs => "List", ParameterKind::SystemRef => "PKSystem", + ParameterKind::UserRef => "User", ParameterKind::MemberPrivacyTarget => "MemberPrivacySubject", ParameterKind::GroupPrivacyTarget => "GroupPrivacySubject", ParameterKind::SystemPrivacyTarget => "SystemPrivacySubject", @@ -280,11 +282,13 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { fn get_param_param_ty(kind: ParameterKind) -> &'static str { match kind { ParameterKind::OpaqueString | ParameterKind::OpaqueStringRemainder => "Opaque", + ParameterKind::OpaqueInt => "Number", ParameterKind::MemberRef => "Member", ParameterKind::MemberRefs => "Members", ParameterKind::GroupRef => "Group", ParameterKind::GroupRefs => "Groups", ParameterKind::SystemRef => "System", + ParameterKind::UserRef => "User", ParameterKind::MemberPrivacyTarget => "MemberPrivacyTarget", ParameterKind::GroupPrivacyTarget => "GroupPrivacyTarget", ParameterKind::SystemPrivacyTarget => "SystemPrivacyTarget", diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 757e6615..7fd3b312 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -13,6 +13,7 @@ interface Parameter { GroupRef(string group); GroupRefs(sequence groups); SystemRef(string system); + UserRef(u64 user_id); MessageRef(u64? guild_id, u64? channel_id, u64 message_id); ChannelRef(u64 channel_id); GuildRef(u64 guild_id); @@ -21,6 +22,7 @@ interface Parameter { SystemPrivacyTarget(string target); PrivacyLevel(string level); OpaqueString(string raw); + OpaqueInt(i32 raw); Toggle(boolean toggle); Avatar(string avatar); Null(); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 65199a7d..b91cf137 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -37,6 +37,9 @@ pub enum Parameter { SystemRef { system: String, }, + UserRef { + user_id: u64, + }, MessageRef { guild_id: Option, channel_id: Option, @@ -63,6 +66,9 @@ pub enum Parameter { OpaqueString { raw: String, }, + OpaqueInt { + raw: i32, + }, Toggle { toggle: bool, }, @@ -80,11 +86,13 @@ impl From for Parameter { ParameterValue::GroupRef(group) => Self::GroupRef { group }, ParameterValue::GroupRefs(groups) => Self::GroupRefs { groups }, ParameterValue::SystemRef(system) => Self::SystemRef { system }, + ParameterValue::UserRef(user_id) => Self::UserRef { user_id }, ParameterValue::MemberPrivacyTarget(target) => Self::MemberPrivacyTarget { target }, ParameterValue::GroupPrivacyTarget(target) => Self::GroupPrivacyTarget { target }, ParameterValue::SystemPrivacyTarget(target) => Self::SystemPrivacyTarget { target }, ParameterValue::PrivacyLevel(level) => Self::PrivacyLevel { level }, ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, + ParameterValue::OpaqueInt(raw) => Self::OpaqueInt { raw }, ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, ParameterValue::Avatar(avatar) => Self::Avatar { avatar }, ParameterValue::MessageRef(guild_id, channel_id, message_id) => Self::MessageRef {