refactor: move some commands out of Misc

This commit is contained in:
spiral 2021-08-25 14:36:13 -04:00
parent 603123777d
commit b46561cb0a
No known key found for this signature in database
GPG Key ID: A6059F0CA0E1BD31
5 changed files with 259 additions and 211 deletions

View File

@ -0,0 +1,209 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Humanizer;
using Myriad.Builders;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Rest;
using Myriad.Rest.Exceptions;
using Myriad.Types;
using PluralKit.Core;
namespace PluralKit.Bot
{
public class Checks
{
private readonly DiscordApiClient _rest;
private readonly Bot _bot;
private readonly IDiscordCache _cache;
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly BotConfig _botConfig;
private readonly ProxyService _proxy;
private readonly ProxyMatcher _matcher;
public Checks(DiscordApiClient rest, Bot bot, IDiscordCache cache, IDatabase db, ModelRepository repo,
BotConfig botConfig, ProxyService proxy, ProxyMatcher matcher)
{
_rest = rest;
_bot = bot;
_cache = cache;
_db = db;
_repo = repo;
_botConfig = botConfig;
_proxy = proxy;
_matcher = matcher;
}
public async Task PermCheckGuild(Context ctx)
{
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 (Myriad.Rest.Exceptions.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 requiredPermissions = new []
{
PermissionSet.ViewChannel,
PermissionSet.SendMessages,
PermissionSet.AddReactions,
PermissionSet.AttachFiles,
PermissionSet.EmbedLinks,
PermissionSet.ManageMessages,
PermissionSet.ManageWebhooks
};
// Loop through every channel and group them by sets of permissions missing
var permissionsMissing = new Dictionary<ulong, List<Channel>>();
var hiddenChannels = 0;
foreach (var channel in await _rest.GetGuildChannels(guild.Id))
{
var botPermissions = _bot.PermissionsIn(channel.Id);
var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser);
if ((userPermissions & PermissionSet.ViewChannel) == 0)
{
// If the user can't see this channel, don't calculate permissions for it
// (to prevent info-leaking, mostly)
// Instead, count how many hidden channels and show the user (so they don't get confused)
hiddenChannels++;
continue;
}
// We use a bitfield so we can set individual permission bits in the loop
// TODO: Rewrite with proper bitfield math
ulong missingPermissionField = 0;
foreach (var requiredPermission in requiredPermissions)
if ((botPermissions & requiredPermission) == 0)
missingPermissionField |= (ulong) requiredPermission;
// If we're not missing any permissions, don't bother adding it to the dict
// This means we can check if the dict is empty to see if all channels are proxyable
if (missingPermissionField != 0)
{
permissionsMissing.TryAdd(missingPermissionField, new List<Channel>());
permissionsMissing[missingPermissionField].Add(channel);
}
}
// Generate the output embed
var eb = new EmbedBuilder()
.Title($"Permission check for **{guild.Name}**");
if (permissionsMissing.Count == 0)
{
eb.Description($"No errors found, all channels proxyable :)").Color(DiscordUtils.Green);
}
else
{
foreach (var (missingPermissionField, channels) in permissionsMissing)
{
// Each missing permission field can have multiple missing channels
// so we extract them all and generate a comma-separated list
var missingPermissionNames = ((PermissionSet) missingPermissionField).ToPermissionString();
var channelsList = string.Join("\n", channels
.OrderBy(c => c.Position)
.Select(c => $"#{c.Name}"));
eb.Field(new($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000)));
eb.Color(DiscordUtils.Red);
}
}
if (hiddenChannels > 0)
eb.Footer(new($"{"channel".ToQuantity(hiddenChannels)} were ignored as you do not have view access to them."));
// Send! :)
await ctx.Reply(embed: eb.Build());
}
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

@ -166,9 +166,9 @@ namespace PluralKit.Bot
if (ctx.Match("explain"))
return ctx.Execute<Help>(Explain, m => m.Explain(ctx));
if (ctx.Match("message", "msg"))
return ctx.Execute<Misc>(Message, m => m.GetMessage(ctx));
return ctx.Execute<ProxiedMessage>(Message, m => m.GetMessage(ctx));
if (ctx.Match("edit", "e"))
return ctx.Execute<MessageEdit>(MessageEdit, m => m.EditMessage(ctx));
return ctx.Execute<ProxiedMessage>(MessageEdit, m => m.EditMessage(ctx));
if (ctx.Match("log"))
if (ctx.Match("channel"))
return ctx.Execute<ServerConfig>(LogChannel, m => m.SetLogChannel(ctx));
@ -193,7 +193,7 @@ namespace PluralKit.Bot
else return PrintCommandExpectedError(ctx, BlacklistCommands);
if (ctx.Match("proxy"))
if (ctx.Match("debug"))
return ctx.Execute<Misc>(ProxyCheck, m => m.MessageProxyCheck(ctx));
return ctx.Execute<Checks>(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));
@ -205,9 +205,9 @@ namespace PluralKit.Bot
if (ctx.Match("flash")) return ctx.Execute<Fun>(null, m => m.Flash(ctx));
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));
return ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx));
if (ctx.Match("proxycheck"))
return ctx.Execute<Misc>(ProxyCheck, m => m.MessageProxyCheck(ctx));
return ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
if (ctx.Match("debug"))
return HandleDebugCommand(ctx);
if (ctx.Match("admin"))
@ -244,9 +244,9 @@ namespace PluralKit.Bot
var availableCommandsStr = "Available debug targets: `permissions`, `proxying`";
if (ctx.Match("permissions", "perms", "permcheck"))
await ctx.Execute<Misc>(PermCheck, m => m.PermCheckGuild(ctx));
await ctx.Execute<Checks>(PermCheck, m => m.PermCheckGuild(ctx));
else if (ctx.Match("proxy", "proxying", "proxycheck"))
await ctx.Execute<Misc>(ProxyCheck, m => m.MessageProxyCheck(ctx));
await ctx.Execute<Checks>(ProxyCheck, m => m.MessageProxyCheck(ctx));
else if (!ctx.HasNext())
await ctx.Reply($"{Emojis.Error} You need to pass a command. {availableCommandsStr}");
else

View File

@ -1,6 +1,7 @@
#nullable enable
using System.Threading.Tasks;
using Myriad.Builders;
using Myriad.Cache;
using Myriad.Extensions;
using Myriad.Rest;
@ -13,22 +14,25 @@ using PluralKit.Core;
namespace PluralKit.Bot
{
public class MessageEdit
public class ProxiedMessage
{
private static readonly Duration EditTimeout = Duration.FromMinutes(10);
private readonly IDatabase _db;
private readonly ModelRepository _repo;
private readonly EmbedService _embeds;
private readonly IClock _clock;
private readonly DiscordApiClient _rest;
private readonly WebhookExecutorService _webhookExecutor;
private readonly LogChannelService _logChannel;
private readonly IDiscordCache _cache;
public MessageEdit(IDatabase db, ModelRepository repo, IClock clock, DiscordApiClient rest, WebhookExecutorService webhookExecutor, LogChannelService logChannel, IDiscordCache cache)
public ProxiedMessage(IDatabase db, ModelRepository repo, EmbedService embeds, IClock clock, DiscordApiClient rest,
WebhookExecutorService webhookExecutor, LogChannelService logChannel, IDiscordCache cache)
{
_db = db;
_repo = repo;
_embeds = embeds;
_clock = clock;
_rest = rest;
_webhookExecutor = webhookExecutor;
@ -115,5 +119,40 @@ namespace PluralKit.Bot
return lastMessage;
}
public async Task GetMessage(Context ctx)
{
var (messageId, _) = ctx.MatchMessage(true);
if (messageId == null)
{
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a message ID or link.");
throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link.");
}
var message = await _db.Execute(c => _repo.GetMessage(c, messageId.Value));
if (message == null) throw Errors.MessageNotFound(messageId.Value);
if (ctx.Match("delete") || ctx.MatchFlag("delete"))
{
if (message.System.Id != ctx.System.Id)
throw new PKError("You can only delete your own messages.");
await ctx.Rest.DeleteMessage(message.Message.Channel, message.Message.Mid);
await ctx.Rest.DeleteMessage(ctx.Message);
return;
}
if (ctx.Match("author") || ctx.MatchFlag("author"))
{
var user = await _cache.GetOrFetchUser(_rest, message.Message.Sender);
var eb = new EmbedBuilder()
.Author(new(user != null ? $"{user.Username}#{user.Discriminator}" : $"Deleted user ${message.Message.Sender}", IconUrl: user != null ? user.AvatarUrl() : null))
.Description(message.Message.Sender.ToString());
await ctx.Reply(user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*", embed: eb.Build());
return;
}
await ctx.Reply(embed: await _embeds.CreateMessageInfoEmbed(message));
}
}
}

View File

@ -119,206 +119,5 @@ namespace PluralKit.Bot {
new MessageEditRequest {Content = "", Embed = embed.Build()});
}
public async Task PermCheckGuild(Context ctx)
{
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 (Myriad.Rest.Exceptions.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 requiredPermissions = new []
{
PermissionSet.ViewChannel,
PermissionSet.SendMessages,
PermissionSet.AddReactions,
PermissionSet.AttachFiles,
PermissionSet.EmbedLinks,
PermissionSet.ManageMessages,
PermissionSet.ManageWebhooks
};
// Loop through every channel and group them by sets of permissions missing
var permissionsMissing = new Dictionary<ulong, List<Channel>>();
var hiddenChannels = 0;
foreach (var channel in await _rest.GetGuildChannels(guild.Id))
{
var botPermissions = _bot.PermissionsIn(channel.Id);
var userPermissions = PermissionExtensions.PermissionsFor(guild, channel, ctx.Author.Id, senderGuildUser);
if ((userPermissions & PermissionSet.ViewChannel) == 0)
{
// If the user can't see this channel, don't calculate permissions for it
// (to prevent info-leaking, mostly)
// Instead, count how many hidden channels and show the user (so they don't get confused)
hiddenChannels++;
continue;
}
// We use a bitfield so we can set individual permission bits in the loop
// TODO: Rewrite with proper bitfield math
ulong missingPermissionField = 0;
foreach (var requiredPermission in requiredPermissions)
if ((botPermissions & requiredPermission) == 0)
missingPermissionField |= (ulong) requiredPermission;
// If we're not missing any permissions, don't bother adding it to the dict
// This means we can check if the dict is empty to see if all channels are proxyable
if (missingPermissionField != 0)
{
permissionsMissing.TryAdd(missingPermissionField, new List<Channel>());
permissionsMissing[missingPermissionField].Add(channel);
}
}
// Generate the output embed
var eb = new EmbedBuilder()
.Title($"Permission check for **{guild.Name}**");
if (permissionsMissing.Count == 0)
{
eb.Description($"No errors found, all channels proxyable :)").Color(DiscordUtils.Green);
}
else
{
foreach (var (missingPermissionField, channels) in permissionsMissing)
{
// Each missing permission field can have multiple missing channels
// so we extract them all and generate a comma-separated list
var missingPermissionNames = ((PermissionSet) missingPermissionField).ToPermissionString();
var channelsList = string.Join("\n", channels
.OrderBy(c => c.Position)
.Select(c => $"#{c.Name}"));
eb.Field(new($"Missing *{missingPermissionNames}*", channelsList.Truncate(1000)));
eb.Color(DiscordUtils.Red);
}
}
if (hiddenChannels > 0)
eb.Footer(new($"{"channel".ToQuantity(hiddenChannels)} were ignored as you do not have view access to them."));
// Send! :)
await ctx.Reply(embed: eb.Build());
}
public async Task GetMessage(Context ctx)
{
var (messageId, _) = ctx.MatchMessage(true);
if (messageId == null)
{
if (!ctx.HasNext())
throw new PKSyntaxError("You must pass a message ID or link.");
throw new PKSyntaxError($"Could not parse {ctx.PeekArgument().AsCode()} as a message ID or link.");
}
var message = await _db.Execute(c => _repo.GetMessage(c, messageId.Value));
if (message == null) throw Errors.MessageNotFound(messageId.Value);
if (ctx.Match("delete") || ctx.MatchFlag("delete"))
{
if (message.System.Id != ctx.System.Id)
throw new PKError("You can only delete your own messages.");
await ctx.Rest.DeleteMessage(message.Message.Channel, message.Message.Mid);
await ctx.Rest.DeleteMessage(ctx.Message);
return;
}
if (ctx.Match("author") || ctx.MatchFlag("author"))
{
var user = await _cache.GetOrFetchUser(_rest, message.Message.Sender);
var eb = new EmbedBuilder()
.Author(new(user != null ? $"{user.Username}#{user.Discriminator}" : $"Deleted user ${message.Message.Sender}", IconUrl: user != null ? user.AvatarUrl() : null))
.Description(message.Message.Sender.ToString());
await ctx.Reply(user != null ? $"{user.Mention()} ({user.Id})" : $"*(deleted user {message.Message.Sender})*", embed: eb.Build());
return;
}
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

@ -48,6 +48,7 @@ namespace PluralKit.Bot
builder.RegisterType<CommandTree>().AsSelf();
builder.RegisterType<Admin>().AsSelf();
builder.RegisterType<Autoproxy>().AsSelf();
builder.RegisterType<Checks>().AsSelf();
builder.RegisterType<Fun>().AsSelf();
builder.RegisterType<Groups>().AsSelf();
builder.RegisterType<Help>().AsSelf();
@ -57,8 +58,8 @@ namespace PluralKit.Bot
builder.RegisterType<MemberEdit>().AsSelf();
builder.RegisterType<MemberGroup>().AsSelf();
builder.RegisterType<MemberProxy>().AsSelf();
builder.RegisterType<MessageEdit>().AsSelf();
builder.RegisterType<Misc>().AsSelf();
builder.RegisterType<ProxiedMessage>().AsSelf();
builder.RegisterType<Random>().AsSelf();
builder.RegisterType<ServerConfig>().AsSelf();
builder.RegisterType<Switch>().AsSelf();