From e4f38c76a98d6f12438fe3dbad98f7744c39d096 Mon Sep 17 00:00:00 2001 From: dusk Date: Fri, 3 Oct 2025 02:21:12 +0000 Subject: [PATCH] implement proxied message and permcheck commands --- Cargo.lock | 1 + PluralKit.Bot/CommandMeta/CommandTree.cs | 48 +++---------- .../Context/ContextArgumentsExt.cs | 15 ++-- .../Context/ContextEntityArgumentsExt.cs | 20 ------ .../Context/ContextParametersExt.cs | 16 +++++ PluralKit.Bot/CommandSystem/Parameters.cs | 10 ++- PluralKit.Bot/Commands/Autoproxy.cs | 8 +-- PluralKit.Bot/Commands/Checks.cs | 50 +++----------- PluralKit.Bot/Commands/Message.cs | 31 ++------- crates/command_definitions/src/checks.rs | 1 - crates/command_definitions/src/debug.rs | 15 ++++ crates/command_definitions/src/help.rs | 1 + crates/command_definitions/src/lib.rs | 3 +- crates/command_definitions/src/message.rs | 31 +++++++++ crates/command_parser/Cargo.toml | 1 + crates/command_parser/src/parameter.rs | 61 ++++++++++++++++- crates/commands/src/bin/write_cs_glue.rs | 4 ++ crates/commands/src/commands.udl | 4 +- crates/commands/src/lib.rs | 68 +++++++++++++++---- 19 files changed, 233 insertions(+), 155 deletions(-) delete mode 100644 crates/command_definitions/src/checks.rs diff --git a/Cargo.lock b/Cargo.lock index 42154562..f0469c2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -664,6 +664,7 @@ version = "0.1.0" dependencies = [ "lazy_static", "ordermap", + "regex", "smol_str", ] diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 1749aaec..41b6af9c 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.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( "For the list of commands, see the website: "), @@ -236,6 +237,14 @@ public partial class CommandTree Commands.AutoproxyLatch => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Latch())), Commands.AutoproxyFront => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Front())), Commands.AutoproxyMember(var param, _) => ctx.Execute(AutoproxySet, m => m.SetAutoproxyMode(ctx, new Autoproxy.Mode.Member(param.target))), + Commands.PermcheckChannel(var param, _) => ctx.Execute(PermCheck, m => m.PermCheckChannel(ctx, param.target)), + Commands.PermcheckGuild(var param, _) => ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx, param.target)), + Commands.MessageProxyCheck(var param, _) => ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx, param.target)), + Commands.MessageInfo(var param, var flags) => ctx.Execute(Message, m => m.GetMessage(ctx, param.target.MessageId, flags.GetReplyFormat(), flags.delete, flags.author)), + 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)), _ => // this should only ever occur when deving if commands are not implemented... ctx.Reply( @@ -251,16 +260,6 @@ public partial class CommandTree return ctx.Execute(Import, m => m.Import(ctx)); if (ctx.Match("export")) return ctx.Execute(Export, m => m.Export(ctx)); - if (ctx.Match("explain")) - return ctx.Execute(Explain, m => m.Explain(ctx)); - if (ctx.Match("message", "msg", "messageinfo")) - return ctx.Execute(Message, m => m.GetMessage(ctx)); - if (ctx.Match("edit", "e")) - return ctx.Execute(MessageEdit, m => m.EditMessage(ctx, false)); - if (ctx.Match("x")) - return ctx.Execute(MessageEdit, m => m.EditMessage(ctx, true)); - if (ctx.Match("reproxy", "rp", "crimes", "crime")) - return ctx.Execute(MessageReproxy, m => m.ReproxyMessage(ctx)); if (ctx.Match("log")) if (ctx.Match("channel")) return ctx.Execute(LogChannel, m => m.SetLogChannel(ctx), true); @@ -283,17 +282,8 @@ public partial class CommandTree return ctx.Execute(BlacklistShow, m => m.ShowProxyBlacklisted(ctx), true); else return ctx.Reply($"{Emojis.Warn} Blacklist commands have moved to `{ctx.DefaultPrefix}serverconfig`."); - if (ctx.Match("proxy")) - if (ctx.Match("debug")) - return ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); 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("permcheck")) - return ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx)); - if (ctx.Match("proxycheck")) - return ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); - if (ctx.Match("debug")) - return HandleDebugCommand(ctx); if (ctx.Match("admin")) return HandleAdminCommand(ctx); if (ctx.Match("dashboard", "dash")) @@ -372,26 +362,6 @@ public partial class CommandTree await ctx.Reply($"{Emojis.Error} Unknown command."); } - private async Task HandleDebugCommand(Context ctx) - { - var availableCommandsStr = "Available debug targets: `permissions`, `proxying`"; - - if (ctx.Match("permissions", "perms", "permcheck")) - if (ctx.Match("channel", "ch")) - await ctx.Execute(PermCheck, m => m.PermCheckChannel(ctx)); - else - await ctx.Execute(PermCheck, m => m.PermCheckGuild(ctx)); - else if (ctx.Match("channel")) - await ctx.Execute(PermCheck, m => m.PermCheckChannel(ctx)); - else if (ctx.Match("proxy", "proxying", "proxycheck")) - await ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); - else if (!ctx.HasNext()) - await ctx.Reply($"{Emojis.Error} You need to pass a command. {availableCommandsStr}"); - else - await ctx.Reply( - $"{Emojis.Error} Unknown debug command {ctx.PeekArgument().AsCode()}. {availableCommandsStr}"); - } - private async Task CommandHelpRoot(Context ctx) { if (!ctx.HasNext()) diff --git a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs index b967d62e..48006cbb 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextArgumentsExt.cs @@ -106,25 +106,24 @@ public static class ContextArgumentsExt else return null; } - public static (ulong? messageId, ulong? channelId) MatchMessage(this Context ctx, bool parseRawMessageId) + public static (ulong? messageId, ulong? channelId) GetRepliedTo(this Context ctx) { if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null) return (ctx.Message.MessageReference.MessageId, ctx.Message.MessageReference.ChannelId); + return (null, null); + } - var word = ctx.PeekArgument(); - if (word == null) - return (null, null); - - if (parseRawMessageId && ulong.TryParse(word, out var mid)) + public static (ulong? messageId, ulong? channelId) ParseMessage(this Context ctx, string maybeMessageRef, bool parseRawMessageId) + { + if (parseRawMessageId && ulong.TryParse(maybeMessageRef, out var mid)) return (mid, null); - var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)"); + var match = Regex.Match(maybeMessageRef, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)"); if (!match.Success) return (null, null); var channelId = ulong.Parse(match.Groups[1].Value); var messageId = ulong.Parse(match.Groups[2].Value); - ctx.PopArgument(); return (messageId, channelId); } } diff --git a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs index bf0fe27b..43d87375 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextEntityArgumentsExt.cs @@ -213,24 +213,4 @@ public static class ContextEntityArgumentsExt ctx.PopArgument(); return channel; } - - public static async Task ParseGuild(this Context ctx, string input) - { - if (!ulong.TryParse(input, out var id)) - return null; - - return await ctx.Rest.GetGuildOrNull(id); - } - - public static async Task MatchGuild(this Context ctx) - { - if (!ulong.TryParse(ctx.PeekArgument(), out var id)) - return null; - - var guild = await ctx.Rest.GetGuildOrNull(id); - if (guild != null) - ctx.PopArgument(); - - return guild; - } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs index 63e701d5..fe60eb5f 100644 --- a/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs +++ b/PluralKit.Bot/CommandSystem/Context/ContextParametersExt.cs @@ -100,6 +100,22 @@ public static class ContextParametersExt ); } + public static async Task ParamResolveMessage(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.MessageRef)?.message + ); + } + + public static async Task ParamResolveChannel(this Context ctx, string param_name) + { + return await ctx.Parameters.ResolveParameter( + ctx, param_name, + param => (param as Parameter.ChannelRef)?.channel + ); + } + public static async Task ParamResolveGuild(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 0c440b61..3d63efdd 100644 --- a/PluralKit.Bot/CommandSystem/Parameters.cs +++ b/PluralKit.Bot/CommandSystem/Parameters.cs @@ -13,6 +13,8 @@ public abstract record Parameter() public record GroupRef(PKGroup group): Parameter; public record GroupRefs(List groups): Parameter; public record SystemRef(PKSystem system): Parameter; + public record MessageRef(Message.Reference message): Parameter; + public record ChannelRef(Channel channel): Parameter; public record GuildRef(Guild guild): Parameter; public record MemberPrivacyTarget(MemberPrivacySubject target): Parameter; public record GroupPrivacyTarget(GroupPrivacySubject target): Parameter; @@ -118,8 +120,12 @@ public class Parameters return new Parameter.Opaque(opaque.raw); case uniffi.commands.Parameter.Avatar avatar: return new Parameter.Avatar(await ctx.GetUserPfp(avatar.avatar) ?? ctx.ParseImage(avatar.avatar)); - case uniffi.commands.Parameter.GuildRef guildRef: - return new Parameter.GuildRef(await ctx.ParseGuild(guildRef.guild) ?? throw new PKError($"Guild {guildRef.guild} not found")); + case uniffi.commands.Parameter.MessageRef(var guildId, var channelId, var messageId): + return new Parameter.MessageRef(new Message.Reference(guildId, channelId, messageId)); + case uniffi.commands.Parameter.ChannelRef(var channelId): + return new Parameter.ChannelRef(await ctx.Rest.GetChannelOrNull(channelId) ?? throw new PKError($"Channel {channelId} not found")); + case uniffi.commands.Parameter.GuildRef(var guildId): + return new Parameter.GuildRef(await ctx.Rest.GetGuildOrNull(guildId) ?? throw new PKError($"Guild {guildId} not found")); } return null; } diff --git a/PluralKit.Bot/Commands/Autoproxy.cs b/PluralKit.Bot/Commands/Autoproxy.cs index 97d6ddd2..39e57e42 100644 --- a/PluralKit.Bot/Commands/Autoproxy.cs +++ b/PluralKit.Bot/Commands/Autoproxy.cs @@ -13,10 +13,10 @@ public class Autoproxy public abstract record Mode() { - public record Off() : Mode; - public record Latch() : Mode; - public record Front() : Mode; - public record Member(PKMember member) : Mode; + public record Off(): Mode; + public record Latch(): Mode; + public record Front(): Mode; + public record Member(PKMember member): Mode; } public Autoproxy(IClock clock) diff --git a/PluralKit.Bot/Commands/Checks.cs b/PluralKit.Bot/Commands/Checks.cs index fa2f6e37..26a53354 100644 --- a/PluralKit.Bot/Commands/Checks.cs +++ b/PluralKit.Bot/Commands/Checks.cs @@ -36,37 +36,11 @@ public class Checks _cache = cache; } - public async Task PermCheckGuild(Context ctx) + public async Task PermCheckGuild(Context ctx, Guild guild) { - Guild guild; - GuildMemberPartial senderGuildUser = null; - - if (ctx.Guild != null && !ctx.HasNext()) - { - guild = ctx.Guild; - senderGuildUser = ctx.Member; - } - else - { - var guildIdStr = ctx.RemainderOrNull() ?? - throw new PKSyntaxError("You must pass a server ID or run this command in a server."); - if (!ulong.TryParse(guildIdStr, out var guildId)) - throw new PKSyntaxError($"Could not parse {guildIdStr.AsCode()} as an ID."); - - try - { - guild = await _rest.GetGuild(guildId); - } - catch (ForbiddenException) - { - throw Errors.GuildNotFound(guildId); - } - - if (guild != null) - senderGuildUser = await _rest.GetGuildMember(guildId, ctx.Author.Id); - if (guild == null || senderGuildUser == null) - throw Errors.GuildNotFound(guildId); - } + var senderGuildUser = await _rest.GetGuildMember(guild.Id, ctx.Author.Id); + if (senderGuildUser == null) + throw Errors.GuildNotFound(guild.Id); var guildMember = await _rest.GetGuildMember(guild.Id, _botConfig.ClientId); @@ -135,17 +109,13 @@ public class Checks await ctx.Reply(embed: eb.Build()); } - public async Task PermCheckChannel(Context ctx) + public async Task PermCheckChannel(Context ctx, Channel channel) { - if (!ctx.HasNext()) - throw new PKSyntaxError("You need to specify a channel."); - var error = "Channel not found or you do not have permissions to access it."; // todo: this breaks if channel is not in cache and bot does not have View Channel permissions // with new cache it breaks if channel is not in current guild - var channel = await ctx.MatchChannel(); - if (channel == null || channel.GuildId == null) + if (channel.GuildId == null) throw new PKError(error); var guild = await _rest.GetGuildOrNull(channel.GuildId.Value); @@ -189,15 +159,17 @@ public class Checks await ctx.Reply(embed: eb.Build()); } - public async Task MessageProxyCheck(Context ctx) + public async Task MessageProxyCheck(Context ctx, Message.Reference? messageReference) { - if (!ctx.HasNext() && ctx.Message.MessageReference == null) + if (messageReference == null && ctx.Message.MessageReference == null) throw new PKSyntaxError("You need to specify a message."); var failedToGetMessage = "Could not find a valid message to check, was not able to fetch the message, or the message was not sent by you."; - var (messageId, channelId) = ctx.MatchMessage(false); + var (messageId, channelId) = ctx.GetRepliedTo(); + if (messageReference != null) + (messageId, channelId) = (messageReference.MessageId, messageReference.ChannelId); if (messageId == null || channelId == null) throw new PKError(failedToGetMessage); diff --git a/PluralKit.Bot/Commands/Message.cs b/PluralKit.Bot/Commands/Message.cs index ec34cea9..0395cf87 100644 --- a/PluralKit.Bot/Commands/Message.cs +++ b/PluralKit.Bot/Commands/Message.cs @@ -58,9 +58,9 @@ public class ProxiedMessage _redisService = redisService; } - public async Task ReproxyMessage(Context ctx) + public async Task ReproxyMessage(Context ctx, ulong? messageId) { - var (msg, systemId) = await GetMessageToEdit(ctx, ReproxyTimeout, true); + 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."); @@ -93,9 +93,9 @@ public class ProxiedMessage } } - public async Task EditMessage(Context ctx, bool useRegex) + public async Task EditMessage(Context ctx, ulong? messageId, string newContent, bool useRegex, bool mutateSpace, bool append, bool prepend, bool clearEmbeds, bool clearAttachments) { - var (msg, systemId) = await GetMessageToEdit(ctx, EditTimeout, false); + var (msg, systemId) = await GetMessageToEdit(ctx, messageId, EditTimeout, false); if (ctx.System.Id != systemId) throw new PKError("Can't edit a message sent by a different system."); @@ -104,21 +104,10 @@ public class ProxiedMessage if (originalMsg == null) throw new PKError("Could not edit message."); - // Regex flag - useRegex = useRegex || ctx.MatchFlag("regex", "x"); - - // Check if we should append or prepend - var mutateSpace = ctx.MatchFlag("nospace", "ns") ? "" : " "; - var append = ctx.MatchFlag("append", "a"); - var prepend = ctx.MatchFlag("prepend", "p"); - // Grab the original message content and new message content var originalContent = originalMsg.Content; - var newContent = ctx.RemainderOrNull()?.NormalizeLineEndSpacing(); // Should we clear embeds? - var clearEmbeds = ctx.MatchFlag("clear-embed", "ce"); - var clearAttachments = ctx.MatchFlag("clear-attachments", "ca"); if ((clearEmbeds || clearAttachments) && newContent == null) newContent = originalMsg.Content!; @@ -249,14 +238,13 @@ public class ProxiedMessage } } - private async Task<(PKMessage, SystemId)> GetMessageToEdit(Context ctx, Duration timeout, bool isReproxy) + private async Task<(PKMessage, SystemId)> GetMessageToEdit(Context ctx, ulong? referencedMessage, Duration timeout, bool isReproxy) { var editType = isReproxy ? "reproxy" : "edit"; var editTypeAction = isReproxy ? "reproxied" : "edited"; PKMessage? msg = null; - var (referencedMessage, _) = ctx.MatchMessage(false); if (referencedMessage != null) { await using var conn = await ctx.Database.Obtain(); @@ -332,9 +320,8 @@ public class ProxiedMessage return lastMessage; } - public async Task GetMessage(Context ctx) + public async Task GetMessage(Context ctx, ulong? messageId, ReplyFormat format, bool isDelete, bool author) { - var (messageId, _) = ctx.MatchMessage(true); if (messageId == null) { if (!ctx.HasNext()) @@ -342,8 +329,6 @@ public class ProxiedMessage throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link."); } - var isDelete = ctx.Match("delete") || ctx.MatchFlag("delete"); - var message = await ctx.Repository.GetFullMessage(messageId.Value); if (message == null) { @@ -360,8 +345,6 @@ public class ProxiedMessage else if (!await ctx.CheckPermissionsInGuildChannel(channel, PermissionSet.ViewChannel)) showContent = false; - var format = ctx.MatchFormat(); - if (format != ReplyFormat.Standard) { var discordMessage = await _rest.GetMessageOrNull(message.Message.Channel, message.Message.Mid); @@ -423,7 +406,7 @@ public class ProxiedMessage return; } - if (ctx.Match("author") || ctx.MatchFlag("author")) + if (author) { var user = await _rest.GetUser(message.Message.Sender); var eb = new EmbedBuilder() diff --git a/crates/command_definitions/src/checks.rs b/crates/command_definitions/src/checks.rs deleted file mode 100644 index 8b137891..00000000 --- a/crates/command_definitions/src/checks.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/command_definitions/src/debug.rs b/crates/command_definitions/src/debug.rs index 8b137891..36f0e224 100644 --- a/crates/command_definitions/src/debug.rs +++ b/crates/command_definitions/src/debug.rs @@ -1 +1,16 @@ +use super::*; +pub fn debug() -> (&'static str, [&'static str; 1]) { + ("debug", ["dbg"]) +} + +pub fn cmds() -> impl Iterator { + let debug = debug(); + let perms = ("permissions", ["perms", "permcheck"]); + [ + command!(debug, perms, ("channel", ["ch"]), ChannelRef => "permcheck_channel"), + command!(debug, perms, ("guild", ["g"]), GuildRef => "permcheck_guild"), + command!(debug, ("proxy", ["proxying", "proxycheck"]), MessageRef => "message_proxy_check"), + ] + .into_iter() +} diff --git a/crates/command_definitions/src/help.rs b/crates/command_definitions/src/help.rs index dd5942cd..da83e879 100644 --- a/crates/command_definitions/src/help.rs +++ b/crates/command_definitions/src/help.rs @@ -3,6 +3,7 @@ use super::*; pub fn cmds() -> impl Iterator { let help = ("help", ["h"]); [ + command!("explain" => "explain"), command!(help => "help") .flag(("foo", OpaqueString)) // todo: just for testing .help("Shows the help command"), diff --git a/crates/command_definitions/src/lib.rs b/crates/command_definitions/src/lib.rs index e79558e4..68dace2f 100644 --- a/crates/command_definitions/src/lib.rs +++ b/crates/command_definitions/src/lib.rs @@ -1,7 +1,6 @@ pub mod admin; pub mod api; pub mod autoproxy; -pub mod checks; pub mod commands; pub mod config; pub mod dashboard; @@ -33,6 +32,8 @@ pub fn all() -> impl Iterator { .chain(random::cmds()) .chain(api::cmds()) .chain(autoproxy::cmds()) + .chain(debug::cmds()) + .chain(message::cmds()) .map(|cmd| { cmd.hidden_flag(("plaintext", ["pt"])) .hidden_flag(("raw", ["r"])) diff --git a/crates/command_definitions/src/message.rs b/crates/command_definitions/src/message.rs index 8b137891..eeb2e53e 100644 --- a/crates/command_definitions/src/message.rs +++ b/crates/command_definitions/src/message.rs @@ -1 +1,32 @@ +use super::*; +pub fn cmds() -> impl Iterator { + let message = tokens!(("message", ["msg", "messageinfo"]), MessageRef); + + let edit = tokens!(("edit", ["e"]), ("new_content", OpaqueStringRemainder)); + let apply_edit = |cmd: Command| { + cmd.flag(("append", ["a"])) + .flag(("prepend", ["p"])) + .flag(("regex", ["r"])) + .flag(("mutate-space", ["ms"])) + .flag(("clear-embeds", ["ce"])) + .flag(("clear-attachments", ["ca"])) + .help("Edits a proxied message") + }; + + [ + command!(message => "message_info") + .flag(("delete", ["d"])) + .flag(("author", ["a"])) + .help("Shows information about a proxied message"), + command!(message, ("author", ["sender"]) => "message_author") + .help("Shows the author of a proxied message"), + command!(message, ("delete", ["del"]) => "message_delete") + .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") + .help("Reproxies a message with a different member"), + ] + .into_iter() +} diff --git a/crates/command_parser/Cargo.toml b/crates/command_parser/Cargo.toml index 169bef16..639d4a44 100644 --- a/crates/command_parser/Cargo.toml +++ b/crates/command_parser/Cargo.toml @@ -7,3 +7,4 @@ edition = "2024" lazy_static = { workspace = true } smol_str = "0.3.2" ordermap = "0.5" +regex = "1" \ No newline at end of file diff --git a/crates/command_parser/src/parameter.rs b/crates/command_parser/src/parameter.rs index 9236ed86..de187aef 100644 --- a/crates/command_parser/src/parameter.rs +++ b/crates/command_parser/src/parameter.rs @@ -15,7 +15,9 @@ pub enum ParameterValue { GroupRef(String), GroupRefs(Vec), SystemRef(String), - GuildRef(String), + MessageRef(Option, Option, u64), + ChannelRef(u64), + GuildRef(u64), MemberPrivacyTarget(String), GroupPrivacyTarget(String), SystemPrivacyTarget(String), @@ -54,6 +56,8 @@ impl Display for Parameter { ParameterKind::GroupRef => write!(f, ""), ParameterKind::GroupRefs => write!(f, " ..."), ParameterKind::SystemRef => write!(f, ""), + ParameterKind::MessageRef => write!(f, ""), + ParameterKind::ChannelRef => write!(f, ""), ParameterKind::GuildRef => write!(f, ""), ParameterKind::MemberPrivacyTarget => write!(f, ""), ParameterKind::GroupPrivacyTarget => write!(f, ""), @@ -92,6 +96,8 @@ pub enum ParameterKind { GroupRef, GroupRefs, SystemRef, + MessageRef, + ChannelRef, GuildRef, MemberPrivacyTarget, GroupPrivacyTarget, @@ -111,6 +117,8 @@ impl ParameterKind { ParameterKind::GroupRef => "target", ParameterKind::GroupRefs => "targets", ParameterKind::SystemRef => "target", + ParameterKind::MessageRef => "target", + ParameterKind::ChannelRef => "target", ParameterKind::GuildRef => "target", ParameterKind::MemberPrivacyTarget => "member_privacy_target", ParameterKind::GroupPrivacyTarget => "group_privacy_target", @@ -157,7 +165,56 @@ impl ParameterKind { Toggle::from_str(input).map(|t| ParameterValue::Toggle(t.into())) } ParameterKind::Avatar => Ok(ParameterValue::Avatar(input.into())), - ParameterKind::GuildRef => Ok(ParameterValue::GuildRef(input.into())), + ParameterKind::MessageRef => { + if let Ok(message_id) = input.parse::() { + return Ok(ParameterValue::MessageRef(None, None, message_id)); + } + + static RE: std::sync::LazyLock = std::sync::LazyLock::new(|| { + regex::Regex::new( + r"https://(?:\w+\.)?discord(?:app)?\.com/channels/(\d+)/(\d+)/(\d+)", + ) + .unwrap() + }); + + if let Some(captures) = RE.captures(input) { + let guild_id = captures + .get(1) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| SmolStr::new("invalid guild ID in message link"))?; + let channel_id = captures + .get(2) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| SmolStr::new("invalid channel ID in message link"))?; + let message_id = captures + .get(3) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| SmolStr::new("invalid message ID in message link"))?; + + Ok(ParameterValue::MessageRef( + Some(guild_id), + Some(channel_id), + message_id, + )) + } else { + Err(SmolStr::new("invalid message reference")) + } + } + ParameterKind::ChannelRef => { + let mut text = input; + + if text.len() > 3 && text.starts_with("<#") && text.ends_with('>') { + text = &text[2..text.len() - 1]; + } + + text.parse::() + .map(ParameterValue::ChannelRef) + .map_err(|_| SmolStr::new("invalid channel ID")) + } + ParameterKind::GuildRef => input + .parse::() + .map(ParameterValue::GuildRef) + .map_err(|_| SmolStr::new("invalid guild ID")), } } diff --git a/crates/commands/src/bin/write_cs_glue.rs b/crates/commands/src/bin/write_cs_glue.rs index 8108772b..d2aeb5ef 100644 --- a/crates/commands/src/bin/write_cs_glue.rs +++ b/crates/commands/src/bin/write_cs_glue.rs @@ -266,6 +266,8 @@ fn get_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "bool", ParameterKind::Avatar => "ParsedImage", + ParameterKind::MessageRef => "Message.Reference", + ParameterKind::ChannelRef => "Channel", ParameterKind::GuildRef => "Guild", } } @@ -284,6 +286,8 @@ fn get_param_param_ty(kind: ParameterKind) -> &'static str { ParameterKind::PrivacyLevel => "PrivacyLevel", ParameterKind::Toggle => "Toggle", ParameterKind::Avatar => "Avatar", + ParameterKind::MessageRef => "Message", + ParameterKind::ChannelRef => "Channel", ParameterKind::GuildRef => "Guild", } } diff --git a/crates/commands/src/commands.udl b/crates/commands/src/commands.udl index 7011c463..899e1cc4 100644 --- a/crates/commands/src/commands.udl +++ b/crates/commands/src/commands.udl @@ -13,7 +13,9 @@ interface Parameter { GroupRef(string group); GroupRefs(sequence groups); SystemRef(string system); - GuildRef(string guild); + MessageRef(u64? guild_id, u64? channel_id, u64 message_id); + ChannelRef(u64 channel_id); + GuildRef(u64 guild_id); MemberPrivacyTarget(string target); GroupPrivacyTarget(string target); SystemPrivacyTarget(string target); diff --git a/crates/commands/src/lib.rs b/crates/commands/src/lib.rs index 0e363781..dd4f6025 100644 --- a/crates/commands/src/lib.rs +++ b/crates/commands/src/lib.rs @@ -22,19 +22,53 @@ pub enum CommandResult { #[derive(Debug, Clone)] pub enum Parameter { - MemberRef { member: String }, - MemberRefs { members: Vec }, - GroupRef { group: String }, - GroupRefs { groups: Vec }, - SystemRef { system: String }, - GuildRef { guild: String }, - MemberPrivacyTarget { target: String }, - GroupPrivacyTarget { target: String }, - SystemPrivacyTarget { target: String }, - PrivacyLevel { level: String }, - OpaqueString { raw: String }, - Toggle { toggle: bool }, - Avatar { avatar: String }, + MemberRef { + member: String, + }, + MemberRefs { + members: Vec, + }, + GroupRef { + group: String, + }, + GroupRefs { + groups: Vec, + }, + SystemRef { + system: String, + }, + MessageRef { + guild_id: Option, + channel_id: Option, + message_id: u64, + }, + ChannelRef { + channel_id: u64, + }, + GuildRef { + guild_id: u64, + }, + MemberPrivacyTarget { + target: String, + }, + GroupPrivacyTarget { + target: String, + }, + SystemPrivacyTarget { + target: String, + }, + PrivacyLevel { + level: String, + }, + OpaqueString { + raw: String, + }, + Toggle { + toggle: bool, + }, + Avatar { + avatar: String, + }, } impl From for Parameter { @@ -52,7 +86,13 @@ impl From for Parameter { ParameterValue::OpaqueString(raw) => Self::OpaqueString { raw }, ParameterValue::Toggle(toggle) => Self::Toggle { toggle }, ParameterValue::Avatar(avatar) => Self::Avatar { avatar }, - ParameterValue::GuildRef(guild) => Self::GuildRef { guild }, + ParameterValue::MessageRef(guild_id, channel_id, message_id) => Self::MessageRef { + guild_id, + channel_id, + message_id, + }, + ParameterValue::ChannelRef(channel_id) => Self::ChannelRef { channel_id }, + ParameterValue::GuildRef(guild_id) => Self::GuildRef { guild_id }, } } }