diff --git a/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs b/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs index 06dfa5fe..bc5f5960 100644 --- a/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs +++ b/PluralKit.Bot/CommandSystem/ContextArgumentsExt.cs @@ -71,25 +71,26 @@ namespace PluralKit.Bot return matched; } - public static ulong? MatchMessage(this Context ctx, bool parseRawMessageId) + public static (ulong? messageId, ulong? channelId) MatchMessage(this Context ctx, bool parseRawMessageId) { - if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference != null) - return ctx.Message.MessageReference.MessageId; + if (ctx.Message.Type == Message.MessageType.Reply && ctx.Message.MessageReference?.MessageId != null) + return (ctx.Message.MessageReference.MessageId, ctx.Message.MessageReference.ChannelId); var word = ctx.PeekArgument(); if (word == null) - return null; + return (null, null); if (parseRawMessageId && ulong.TryParse(word, out var mid)) - return mid; + return (mid, null); - var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/\\d+/(\\d+)"); + var match = Regex.Match(word, "https://(?:\\w+.)?discord(?:app)?.com/channels/\\d+/(\\d+)/(\\d+)"); if (!match.Success) - return null; + return (null, null); - var messageId = ulong.Parse(match.Groups[1].Value); + var channelId = ulong.Parse(match.Groups[1].Value); + var messageId = ulong.Parse(match.Groups[2].Value); ctx.PopArgument(); - return messageId; + return (messageId, channelId); } public static async Task> ParseMemberList(this Context ctx, SystemId? restrictToSystem) diff --git a/PluralKit.Bot/Commands/CommandTree.cs b/PluralKit.Bot/Commands/CommandTree.cs index 645529f6..77aef124 100644 --- a/PluralKit.Bot/Commands/CommandTree.cs +++ b/PluralKit.Bot/Commands/CommandTree.cs @@ -84,6 +84,7 @@ namespace PluralKit.Bot public static Command Explain = new Command("explain", "explain", "Explains the basics of systems and proxying"); public static Command Message = new Command("message", "message [delete|author]", "Looks up a proxied message"); public static Command MessageEdit = new Command("edit", "edit [link] ", "Edit a previously proxied message"); + public static Command ProxyCheck = new Command("proxycheck", "proxycheck [link]", "Checks why your message has not been proxied"); public static Command LogChannel = new Command("log channel", "log channel ", "Designates a channel to post proxied messages to"); public static Command LogChannelClear = new Command("log channel", "log channel -clear", "Clears the currently set log channel"); public static Command LogEnable = new Command("log enable", "log enable all| [channel 2] [channel 3...]", "Enables message logging in certain channels"); @@ -191,7 +192,10 @@ namespace PluralKit.Bot return PrintCommandList(ctx, "channel blacklisting", BlacklistCommands); else return PrintCommandExpectedError(ctx, BlacklistCommands); if (ctx.Match("proxy")) - return ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); + if (ctx.Match("debug")) + return ctx.Execute(ProxyCheck, m => m.MessageProxyCheck(ctx)); + else + return ctx.Execute(SystemProxy, m => m.SystemProxy(ctx)); if (ctx.Match("invite")) return ctx.Execute(Invite, m => m.Invite(ctx)); if (ctx.Match("mn")) return ctx.Execute(null, m => m.Mn(ctx)); if (ctx.Match("fire")) return ctx.Execute(null, m => m.Fire(ctx)); @@ -202,6 +206,10 @@ namespace PluralKit.Bot if (ctx.Match("stats")) 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("random", "r")) @@ -231,6 +239,20 @@ namespace PluralKit.Bot 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")) + await ctx.Execute(PermCheck, m => m.PermCheckGuild(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 HandleSystemCommand(Context ctx) { // If we have no parameters, default to self-target diff --git a/PluralKit.Bot/Commands/MessageEdit.cs b/PluralKit.Bot/Commands/MessageEdit.cs index 7143afe9..146aaee3 100644 --- a/PluralKit.Bot/Commands/MessageEdit.cs +++ b/PluralKit.Bot/Commands/MessageEdit.cs @@ -75,7 +75,7 @@ namespace PluralKit.Bot await using var conn = await _db.Obtain(); FullMessage? msg = null; - var referencedMessage = ctx.MatchMessage(false); + var (referencedMessage, _) = ctx.MatchMessage(false); if (referencedMessage != null) { msg = await _repo.GetMessage(conn, referencedMessage.Value); diff --git a/PluralKit.Bot/Commands/Misc.cs b/PluralKit.Bot/Commands/Misc.cs index e9fac7d2..f6d768cd 100644 --- a/PluralKit.Bot/Commands/Misc.cs +++ b/PluralKit.Bot/Commands/Misc.cs @@ -17,6 +17,7 @@ using Myriad.Cache; using Myriad.Extensions; using Myriad.Gateway; using Myriad.Rest; +using Myriad.Rest.Exceptions; using Myriad.Rest.Types.Requests; using Myriad.Types; @@ -34,8 +35,11 @@ namespace PluralKit.Bot { private readonly DiscordApiClient _rest; private readonly Cluster _cluster; private readonly Bot _bot; + private readonly ProxyService _proxy; + private readonly ProxyMatcher _matcher; - public Misc(BotConfig botConfig, IMetrics metrics, CpuStatService cpu, ShardInfoService shards, EmbedService embeds, ModelRepository repo, IDatabase db, IDiscordCache cache, DiscordApiClient rest, Bot bot, Cluster cluster) + public Misc(BotConfig botConfig, IMetrics metrics, CpuStatService cpu, ShardInfoService shards, EmbedService embeds, ModelRepository repo, + IDatabase db, IDiscordCache cache, DiscordApiClient rest, Bot bot, Cluster cluster, ProxyService proxy, ProxyMatcher matcher) { _botConfig = botConfig; _metrics = metrics; @@ -48,6 +52,8 @@ namespace PluralKit.Bot { _rest = rest; _bot = bot; _cluster = cluster; + _proxy = proxy; + _matcher = matcher; } public async Task Invite(Context ctx) @@ -218,7 +224,7 @@ namespace PluralKit.Bot { public async Task GetMessage(Context ctx) { - var messageId = ctx.MatchMessage(true); + var (messageId, _) = ctx.MatchMessage(true); if (messageId == null) { if (!ctx.HasNext()) @@ -250,5 +256,69 @@ namespace PluralKit.Bot { await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message)); } + + public async Task MessageProxyCheck(Context ctx) + { + if (!ctx.HasNext() && ctx.Message.MessageReference == null) + throw new PKError("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); + if (messageId == null || channelId == null) + throw new PKError(failedToGetMessage); + + await using var conn = await _db.Obtain(); + + var proxiedMsg = await _repo.GetMessage(conn, messageId.Value); + if (proxiedMsg != null) + { + await ctx.Reply($"{Emojis.Success} This message was proxied successfully."); + return; + } + + // get the message info + var msg = ctx.Message; + try + { + msg = await _rest.GetMessage(channelId.Value, messageId.Value); + } + catch (ForbiddenException) + { + throw new PKError(failedToGetMessage); + } + + // if user is fetching a message in a different channel sent by someone else, throw a generic error message + if (msg == null || (msg.Author.Id != ctx.Author.Id && msg.ChannelId != ctx.Channel.Id)) + throw new PKError(failedToGetMessage); + + if ((_botConfig.Prefixes ?? BotConfig.DefaultPrefixes).Any(p => msg.Content.StartsWith(p))) + throw new PKError("This message starts with the bot's prefix, and was parsed as a command."); + if (msg.WebhookId != null) + throw new PKError("You cannot check messages sent by a webhook."); + if (msg.Author.Id != ctx.Author.Id) + throw new PKError("You can only check your own messages."); + + // get the channel info + var channel = _cache.GetChannel(channelId.Value); + if (channel == null) + throw new PKError("Unable to get the channel associated with this message."); + + // using channel.GuildId here since _rest.GetMessage() doesn't return the GuildId + var context = await _repo.GetMessageContext(conn, msg.Author.Id, channel.GuildId.Value, msg.ChannelId); + var members = (await _repo.GetProxyMembers(conn, msg.Author.Id, channel.GuildId.Value)).ToList(); + + // Run everything through the checks, catch the ProxyCheckFailedException, and reply with the error message. + try + { + _proxy.ShouldProxy(channel, msg, context); + _matcher.TryMatch(context, members, out var match, msg.Content, msg.Attachments.Length > 0, context.AllowAutoproxy); + + await ctx.Reply("I'm not sure why this message was not proxied, sorry."); + } catch (ProxyService.ProxyChecksFailedException e) + { + await ctx.Reply($"{e.Message}"); + } + } } } diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 43a93d63..d13bbd78 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -154,6 +154,10 @@ namespace PluralKit.Bot { return await _proxy.HandleIncomingMessage(shard, evt, ctx, guild, channel, allowAutoproxy: ctx.AllowAutoproxy, botPermissions); } + + // Catch any failed proxy checks so they get ignored in the global error handler + catch (ProxyService.ProxyChecksFailedException) {} + catch (PKError e) { // User-facing errors, print to the channel properly formatted diff --git a/PluralKit.Bot/Proxy/ProxyMatcher.cs b/PluralKit.Bot/Proxy/ProxyMatcher.cs index e2a3c5e7..653aa4b8 100644 --- a/PluralKit.Bot/Proxy/ProxyMatcher.cs +++ b/PluralKit.Bot/Proxy/ProxyMatcher.cs @@ -45,24 +45,41 @@ namespace PluralKit.Bot // Skip autoproxy match if we hit the escape character if (messageContent.StartsWith(AutoproxyEscapeCharacter)) - return false; + throw new ProxyService.ProxyChecksFailedException("This message matches none of your proxy tags, and it was not autoproxied because it starts with a backslash (`\\`)."); // Find the member we should autoproxy (null if none) var member = ctx.AutoproxyMode switch { AutoproxyMode.Member when ctx.AutoproxyMember != null => members.FirstOrDefault(m => m.Id == ctx.AutoproxyMember), - + AutoproxyMode.Front when ctx.LastSwitchMembers.Length > 0 => members.FirstOrDefault(m => m.Id == ctx.LastSwitchMembers[0]), - AutoproxyMode.Latch when ctx.LastMessageMember != null && !IsLatchExpired(ctx) => + AutoproxyMode.Latch when ctx.LastMessageMember != null => members.FirstOrDefault(m => m.Id == ctx.LastMessageMember.Value), _ => null }; + // Throw an error if the member is null, message varies depending on autoproxy mode + if (member == null) + { + if (ctx.AutoproxyMode == AutoproxyMode.Front) + throw new ProxyService.ProxyChecksFailedException("You are using autoproxy front, but no members are currently registered as fronting. Please use `pk;switch ` to log a new switch."); + else if (ctx.AutoproxyMode == AutoproxyMode.Member) + throw new ProxyService.ProxyChecksFailedException("You are using member-specific autoproxy with an invalid member. Was this member deleted?"); + else if (ctx.AutoproxyMode == AutoproxyMode.Latch) + throw new ProxyService.ProxyChecksFailedException("You are using autoproxy latch, but have not sent any messages yet in this server. Please send a message using proxy tags first."); + throw new ProxyService.ProxyChecksFailedException("This message matches none of your proxy tags and autoproxy is not enabled."); + } + + if (ctx.AutoproxyMode != AutoproxyMode.Member && !member.AllowAutoproxy) + throw new ProxyService.ProxyChecksFailedException("This member has autoproxy disabled. To enable it, use `pk;m autoproxy on`."); + + // Moved the IsLatchExpired() check to here, so that an expired latch and a latch without any previous messages throw different errors + if (ctx.AutoproxyMode == AutoproxyMode.Latch && IsLatchExpired(ctx)) + throw new ProxyService.ProxyChecksFailedException("Latch-mode autoproxy has timed out. Please send a new message using proxy tags."); - if (member == null || (ctx.AutoproxyMode != AutoproxyMode.Member && !member.AllowAutoproxy)) return false; match = new ProxyMatch { Content = messageContent, diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index feda07a8..9ab3c995 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -89,24 +89,34 @@ namespace PluralKit.Bot return true; } - private bool ShouldProxy(Channel channel, Message msg, MessageContext ctx) + public bool ShouldProxy(Channel channel, Message msg, MessageContext ctx) { // Make sure author has a system - if (ctx.SystemId == null) return false; + if (ctx.SystemId == null) + throw new ProxyChecksFailedException(Errors.NoSystemError.Message); // Make sure channel is a guild text channel and this is a normal message - if (!DiscordUtils.IsValidGuildChannel(channel)) return false; - if (msg.Type != Message.MessageType.Default && msg.Type != Message.MessageType.Reply) return false; + if (!DiscordUtils.IsValidGuildChannel(channel)) + throw new ProxyChecksFailedException("This channel is not a text channel."); + if (msg.Type != Message.MessageType.Default && msg.Type != Message.MessageType.Reply) + throw new ProxyChecksFailedException("This message is not a normal message."); // Make sure author is a normal user - if (msg.Author.System == true || msg.Author.Bot || msg.WebhookId != null) return false; + if (msg.Author.System == true || msg.Author.Bot || msg.WebhookId != null) + throw new ProxyChecksFailedException("This message was not sent by a normal user."); // Make sure proxying is enabled here - if (!ctx.ProxyEnabled || ctx.InBlacklist) return false; + if (ctx.InBlacklist) + throw new ProxyChecksFailedException($"Proxying was disabled in this channel by a server administrator (via the proxy blacklist)."); + + // Make sure the system has proxying enabled in the server + if (!ctx.ProxyEnabled) + throw new ProxyChecksFailedException("Your system has proxying disabled in this server. Type `pk;proxy on` to enable it."); // Make sure we have either an attachment or message content var isMessageBlank = msg.Content == null || msg.Content.Trim().Length == 0; - if (isMessageBlank && msg.Attachments.Length == 0) return false; + if (isMessageBlank && msg.Attachments.Length == 0) + throw new ProxyChecksFailedException("Message cannot be blank."); // All good! return true; @@ -364,5 +374,9 @@ namespace PluralKit.Bot { if (proxyName.Length > Limits.MaxProxyNameLength) throw Errors.ProxyNameTooLong(proxyName); } + public class ProxyChecksFailedException : Exception + { + public ProxyChecksFailedException(string message) : base(message) {} + } } } diff --git a/docs/content/command-list.md b/docs/content/command-list.md index efa76e98..1ab8dcb3 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -125,7 +125,8 @@ Some arguments indicate the use of specific Discord features. These include: - `pk;invite` - Sends the bot invite link for PluralKit. - `pk;import` - Imports a data file from PluralKit or Tupperbox. - `pk;export` - Exports a data file containing your system information. -- `pk;permcheck [server id]` - [Checks the given server's permission setup](./staff/permissions.md#permission-checker-command) to check if it's compatible with PluralKit. +- `pk;debug permissions [server id]` - [Checks the given server's permission setup](./staff/permissions.md#permission-checker-command) to check if it's compatible with PluralKit. +- `pk;debug proxying ` - Checks why your message has not been proxied. - `pk;edit [message link|reply] ` - Edits a proxied message. Without an explicit message target, will target the last message proxied by your system in the current channel. **Does not support message IDs!** ## API