feat: proxy debug command

Co-authored-by: Spectralitree <72747870+Spectralitree@users.noreply.github.com>
This commit is contained in:
spiral 2021-08-03 21:06:14 -04:00
parent d8458c0846
commit b9f73cadb7
No known key found for this signature in database
GPG Key ID: A6059F0CA0E1BD31
8 changed files with 154 additions and 25 deletions

View File

@ -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<List<PKMember>> ParseMemberList(this Context ctx, SystemId? restrictToSystem)

View File

@ -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 <id|link> [delete|author]", "Looks up a proxied message");
public static Command MessageEdit = new Command("edit", "edit [link] <text>", "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 <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> [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<SystemEdit>(SystemProxy, m => m.SystemProxy(ctx));
if (ctx.Match("debug"))
return ctx.Execute<Misc>(ProxyCheck, m => m.MessageProxyCheck(ctx));
else
return ctx.Execute<SystemEdit>(SystemProxy, m => m.SystemProxy(ctx));
if (ctx.Match("invite")) return ctx.Execute<Misc>(Invite, m => m.Invite(ctx));
if (ctx.Match("mn")) return ctx.Execute<Fun>(null, m => m.Mn(ctx));
if (ctx.Match("fire")) return ctx.Execute<Fun>(null, m => m.Fire(ctx));
@ -202,6 +206,10 @@ namespace PluralKit.Bot
if (ctx.Match("stats")) return ctx.Execute<Misc>(null, m => m.Stats(ctx));
if (ctx.Match("permcheck"))
return ctx.Execute<Misc>(PermCheck, m => m.PermCheckGuild(ctx));
if (ctx.Match("proxycheck"))
return ctx.Execute<Misc>(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<Misc>(PermCheck, m => m.PermCheckGuild(ctx));
else if (ctx.Match("proxy", "proxying", "proxycheck"))
await ctx.Execute<Misc>(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

View File

@ -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);

View File

@ -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}");
}
}
}
}

View File

@ -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

View File

@ -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 <member>` 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 <member> 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,

View File

@ -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) {}
}
}
}

View File

@ -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 <message link|reply>` - Checks why your message has not been proxied.
- `pk;edit [message link|reply] <new content>` - 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