Fix various issues with proxying and webhook caching

This commit is contained in:
Ske 2019-07-10 23:16:17 +02:00
parent 8940226385
commit ca56fd419b
4 changed files with 61 additions and 7 deletions

View File

@ -51,7 +51,7 @@ namespace PluralKit.Bot
.AddTransient(_ => _config.GetSection("PluralKit").Get<CoreConfig>() ?? new CoreConfig())
.AddTransient(_ => _config.GetSection("PluralKit").GetSection("Bot").Get<BotConfig>() ?? new BotConfig())
.AddScoped<IDbConnection>(svc =>
.AddTransient<IDbConnection>(svc =>
{
var conn = new NpgsqlConnection(svc.GetRequiredService<CoreConfig>().Database);

View File

@ -46,7 +46,7 @@ namespace PluralKit.Bot {
return new EmbedBuilder()
.WithAuthor($"#{message.Channel.Name}: {member.Name}", member.AvatarUrl)
.WithDescription(message.Content)
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: ${message.Id}")
.WithFooter($"System ID: {system.Hid} | Member ID: {member.Hid} | Sender: {sender.Username}#{sender.Discriminator} ({sender.Id}) | Message ID: {message.Id}")
.WithTimestamp(message.Timestamp)
.Build();
}

View File

@ -77,12 +77,19 @@ namespace PluralKit.Bot
}
public async Task HandleMessageAsync(IMessage message) {
var results = await _connection.QueryAsync<PKMember, PKSystem, ProxyDatabaseResult>("select members.*, systems.* from members, systems, accounts where members.system = systems.id and accounts.system = systems.id and accounts.uid = @Uid", (member, system) => new ProxyDatabaseResult { Member = member, System = system }, new { Uid = message.Author.Id });
var results = await _connection.QueryAsync<PKMember, PKSystem, ProxyDatabaseResult>(
"select members.*, systems.* from members, systems, accounts where members.system = systems.id and accounts.system = systems.id and accounts.uid = @Uid",
(member, system) =>
new ProxyDatabaseResult { Member = member, System = system }, new { Uid = message.Author.Id });
// Find a member with proxy tags matching the message
var match = GetProxyTagMatch(message.Content, results);
if (match == null) return;
// We know message.Channel can only be ITextChannel as PK doesn't work in DMs/groups
// Afterwards we ensure the bot has the right permissions, otherwise bail early
if (!await EnsureBotPermissions(message.Channel as ITextChannel)) return;
// Fetch a webhook for this channel, and send the proxied message
var webhook = await _webhookCache.GetWebhook(message.Channel as ITextChannel);
var hookMessage = await ExecuteWebhook(webhook, match.InnerText, match.ProxyName, match.Member.AvatarUrl, message.Attachments.FirstOrDefault());
@ -96,8 +103,42 @@ namespace PluralKit.Bot
await message.DeleteAsync();
}
private async Task<bool> EnsureBotPermissions(ITextChannel channel)
{
var guildUser = await channel.Guild.GetCurrentUserAsync();
var permissions = guildUser.GetPermissions(channel);
if (!permissions.ManageWebhooks)
{
await channel.SendMessageAsync(
$"{Emojis.Error} PluralKit does not have the *Manage Webhooks* permission in this channel, and thus cannot proxy messages. Please contact a server administrator to remedy this.");
return false;
}
if (!permissions.ManageMessages)
{
await channel.SendMessageAsync(
$"{Emojis.Error} PluralKit does not have the *Manage Messages* permission in this channel, and thus cannot delete the original trigger message. Please contact a server administrator to remedy this.");
return false;
}
return true;
}
private async Task<IMessage> ExecuteWebhook(IWebhook webhook, string text, string username, string avatarUrl, IAttachment attachment) {
var client = new DiscordWebhookClient(webhook);
// TODO: DiscordWebhookClient's ctor does a call to GetWebhook that may be unnecessary, see if there's a way to do this The Hard Way :tm:
// TODO: this will probably crash if there are multiple consecutive failures, perhaps have a loop instead?
DiscordWebhookClient client;
try
{
client = new DiscordWebhookClient(webhook);
}
catch (InvalidOperationException)
{
// webhook was deleted or invalid
webhook = await _webhookCache.InvalidateAndRefreshWebhook(webhook);
client = new DiscordWebhookClient(webhook);
}
ulong messageId;
if (attachment != null) {
@ -108,6 +149,8 @@ namespace PluralKit.Bot
} else {
messageId = await client.SendMessageAsync(text, username: username, avatarUrl: avatarUrl);
}
// TODO: SendMessageAsync should return a full object(??), see if there's a way to avoid the extra server call here
return await webhook.Channel.GetMessageAsync(messageId);
}

View File

@ -32,13 +32,24 @@ namespace PluralKit.Bot
// We cache the webhook through a Lazy<Task<T>>, this way we make sure to only create one webhook per channel
// If the webhook is requested twice before it's actually been found, the Lazy<T> wrapper will stop the
// webhook from being created twice.
var lazyWebhookValue =
var lazyWebhookValue =
_webhooks.GetOrAdd(channel.Id, new Lazy<Task<IWebhook>>(() => GetOrCreateWebhook(channel)));
return await lazyWebhookValue.Value;
// It's possible to "move" a webhook to a different channel after creation
// Here, we ensure it's actually still pointing towards the proper channel, and if not, wipe and refetch one.
var webhook = await lazyWebhookValue.Value;
if (webhook.Channel.Id != channel.Id) return await InvalidateAndRefreshWebhook(webhook);
return webhook;
}
public async Task<IWebhook> InvalidateAndRefreshWebhook(IWebhook webhook)
{
_webhooks.TryRemove(webhook.Channel.Id, out _);
return await GetWebhook(webhook.Channel.Id);
}
private async Task<IWebhook> GetOrCreateWebhook(ITextChannel channel) =>
await FindExistingWebhook(channel) ?? await GetOrCreateWebhook(channel);
await FindExistingWebhook(channel) ?? await DoCreateWebhook(channel);
private async Task<IWebhook> FindExistingWebhook(ITextChannel channel) => (await channel.GetWebhooksAsync()).FirstOrDefault(IsWebhookMine);