Merge branch 'feat/webhooks' into main

This commit is contained in:
spiral
2021-11-25 17:15:42 -05:00
34 changed files with 920 additions and 39 deletions

View File

@@ -18,6 +18,12 @@ namespace PluralKit.Bot
throw new PKError("This command can not be run in a DM.");
}
public static Context CheckDMContext(this Context ctx)
{
if (ctx.Channel.GuildId == null) return ctx;
throw new PKError("This command must be run in a DM.");
}
public static Context CheckSystemPrivacy(this Context ctx, PKSystem target, PrivacyLevel level)
{
if (level.CanAccess(ctx.LookupContextFor(target))) return ctx;

View File

@@ -1,3 +1,5 @@
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Myriad.Extensions;
@@ -9,14 +11,15 @@ using PluralKit.Core;
namespace PluralKit.Bot
{
public class Token
public class Api
{
private readonly IDatabase _db;
private readonly ModelRepository _repo;
public Token(IDatabase db, ModelRepository repo)
private readonly DispatchService _dispatch;
private static readonly Regex _webhookRegex = new("https://(?:\\w+.)?discord(?:app)?.com/api(?:/v.*)?/webhooks/(.*)");
public Api(ModelRepository repo, DispatchService dispatch)
{
_db = db;
_repo = repo;
_dispatch = dispatch;
}
public async Task GetToken(Context ctx)
@@ -91,5 +94,63 @@ namespace PluralKit.Bot
await ctx.Reply($"{Emojis.Error} Could not send token in DMs. Are your DMs closed?");
}
}
public async Task SystemWebhook(Context ctx)
{
ctx.CheckSystem().CheckDMContext();
if (!ctx.HasNext(false))
{
if (ctx.System.WebhookUrl == null)
await ctx.Reply("Your system does not have a webhook URL set. Set one with `pk;system webhook <url>`!");
else
await ctx.Reply($"Your system's webhook URL is <{ctx.System.WebhookUrl}>.");
return;
}
if (await ctx.MatchClear("your system's webhook URL"))
{
await _repo.UpdateSystem(ctx.System.Id, new()
{
WebhookUrl = null,
WebhookToken = null,
});
await ctx.Reply($"{Emojis.Success} System webhook URL removed.");
return;
}
var newUrl = ctx.RemainderOrNull();
if (!await DispatchExt.ValidateUri(newUrl))
throw new PKError($"The URL {newUrl.AsCode()} is invalid or I cannot access it. Are you sure this is a valid, publicly accessible URL?");
if (_webhookRegex.IsMatch(newUrl))
throw new PKError("PluralKit does not currently support setting a Discord webhook URL as your system's webhook URL.");
try
{
await _dispatch.DoPostRequest(ctx.System.Id, newUrl, null, true);
}
catch (Exception e)
{
throw new PKError($"Could not verify that the new URL is working: {e.Message}");
}
var newToken = StringUtils.GenerateToken();
await _repo.UpdateSystem(ctx.System.Id, new()
{
WebhookUrl = newUrl,
WebhookToken = newToken,
});
await ctx.Reply($"{Emojis.Success} Successfully the new webhook URL for your system."
+ $"\n\n{Emojis.Warn} The following token is used to authenticate requests from PluralKit to you."
+ " If it leaks, you should clear and re-set the webhook URL to get a new token."
+ "\ntodo: add link to docs or something"
);
await ctx.Reply(newToken);
}
}
}

View File

@@ -152,9 +152,9 @@ namespace PluralKit.Bot
return ctx.Execute<SystemLink>(Unlink, m => m.UnlinkAccount(ctx));
if (ctx.Match("token"))
if (ctx.Match("refresh", "renew", "invalidate", "reroll", "regen"))
return ctx.Execute<Token>(TokenRefresh, m => m.RefreshToken(ctx));
return ctx.Execute<Api>(TokenRefresh, m => m.RefreshToken(ctx));
else
return ctx.Execute<Token>(TokenGet, m => m.GetToken(ctx));
return ctx.Execute<Api>(TokenGet, m => m.GetToken(ctx));
if (ctx.Match("import"))
return ctx.Execute<ImportExport>(Import, m => m.Import(ctx));
if (ctx.Match("export"))
@@ -286,6 +286,8 @@ namespace PluralKit.Bot
await ctx.Execute<SystemEdit>(SystemAvatar, m => m.Avatar(ctx));
else if (ctx.Match("delete", "remove", "destroy", "erase", "yeet"))
await ctx.Execute<SystemEdit>(SystemDelete, m => m.Delete(ctx));
else if (ctx.Match("webhook", "hook"))
await ctx.Execute<Api>(null, m => m.SystemWebhook(ctx));
else if (ctx.Match("timezone", "tz"))
await ctx.Execute<SystemEdit>(SystemTimezone, m => m.SystemTimezone(ctx));
else if (ctx.Match("proxy"))

View File

@@ -10,6 +10,8 @@ using Dapper;
using Humanizer;
using Newtonsoft.Json.Linq;
using NodaTime;
using Myriad.Builders;
@@ -24,13 +26,15 @@ namespace PluralKit.Bot
private readonly ModelRepository _repo;
private readonly EmbedService _embeds;
private readonly HttpClient _client;
private readonly DispatchService _dispatch;
public Groups(IDatabase db, ModelRepository repo, EmbedService embeds, HttpClient client)
public Groups(IDatabase db, ModelRepository repo, EmbedService embeds, HttpClient client, DispatchService dispatch)
{
_db = db;
_repo = repo;
_embeds = embeds;
_client = client;
_dispatch = dispatch;
}
public async Task CreateGroup(Context ctx)
@@ -59,6 +63,12 @@ namespace PluralKit.Bot
var newGroup = await _repo.CreateGroup(ctx.System.Id, groupName);
_ = _dispatch.Dispatch(newGroup.Id, new UpdateDispatchData()
{
Event = DispatchEvent.CREATE_GROUP,
EventData = JObject.FromObject(new { name = groupName }),
});
var eb = new EmbedBuilder()
.Description($"Your new group, **{groupName}**, has been created, with the group ID **`{newGroup.Hid}`**.\nBelow are a couple of useful commands:")
.Field(new("View the group card", $"> pk;group **{newGroup.Reference()}**"))

View File

@@ -21,13 +21,15 @@ namespace PluralKit.Bot
private readonly ModelRepository _repo;
private readonly EmbedService _embeds;
private readonly HttpClient _client;
private readonly DispatchService _dispatch;
public Member(EmbedService embeds, IDatabase db, ModelRepository repo, HttpClient client)
public Member(EmbedService embeds, IDatabase db, ModelRepository repo, HttpClient client, DispatchService dispatch)
{
_embeds = embeds;
_db = db;
_repo = repo;
_client = client;
_dispatch = dispatch;
}
public async Task NewMember(Context ctx)
@@ -62,12 +64,20 @@ namespace PluralKit.Bot
// Try to match an image attached to the message
var avatarArg = ctx.Message.Attachments.FirstOrDefault();
Exception imageMatchError = null;
bool sentDispatch = false;
if (avatarArg != null)
{
try
{
await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Url);
await _repo.UpdateMember(member.Id, new MemberPatch { AvatarUrl = avatarArg.Url });
await _db.Execute(conn => _repo.UpdateMember(member.Id, new MemberPatch { AvatarUrl = avatarArg.Url }, conn));
_ = _dispatch.Dispatch(member.Id, new()
{
Event = DispatchEvent.CREATE_MEMBER,
EventData = JObject.FromObject(new { name = memberName, avatar_url = avatarArg.Url }),
});
sentDispatch = true;
}
catch (Exception e)
{
@@ -75,6 +85,13 @@ namespace PluralKit.Bot
}
}
if (!sentDispatch)
_ = _dispatch.Dispatch(member.Id, new()
{
Event = DispatchEvent.CREATE_MEMBER,
EventData = JObject.FromObject(new { name = memberName }),
});
// Send confirmation and space hint
await ctx.Reply($"{Emojis.Success} Member \"{memberName}\" (`{member.Hid}`) registered! Check out the getting started page for how to get a member up and running: https://pluralkit.me/start#create-a-member");
// todo: move this to ModelRepository

View File

@@ -73,6 +73,7 @@ namespace PluralKit.Bot
// Commands
builder.RegisterType<CommandTree>().AsSelf();
builder.RegisterType<Admin>().AsSelf();
builder.RegisterType<Api>().AsSelf();
builder.RegisterType<Autoproxy>().AsSelf();
builder.RegisterType<Checks>().AsSelf();
builder.RegisterType<Fun>().AsSelf();
@@ -94,7 +95,6 @@ namespace PluralKit.Bot
builder.RegisterType<SystemFront>().AsSelf();
builder.RegisterType<SystemLink>().AsSelf();
builder.RegisterType<SystemList>().AsSelf();
builder.RegisterType<Token>().AsSelf();
// Bot core
builder.RegisterType<Bot>().AsSelf().SingleInstance();

View File

@@ -30,17 +30,19 @@ namespace PluralKit.Bot
private readonly ModelRepository _repo;
private readonly ILogger _logger;
private readonly WebhookExecutorService _webhookExecutor;
private readonly DispatchService _dispatch;
private readonly ProxyMatcher _matcher;
private readonly IMetrics _metrics;
private readonly IDiscordCache _cache;
private readonly LastMessageCacheService _lastMessage;
private readonly DiscordApiClient _rest;
public ProxyService(LogChannelService logChannel, ILogger logger, WebhookExecutorService webhookExecutor, IDatabase db,
public ProxyService(LogChannelService logChannel, ILogger logger, WebhookExecutorService webhookExecutor, DispatchService dispatch, IDatabase db,
ProxyMatcher matcher, IMetrics metrics, ModelRepository repo, IDiscordCache cache, DiscordApiClient rest, LastMessageCacheService lastMessage)
{
_logChannel = logChannel;
_webhookExecutor = webhookExecutor;
_dispatch = dispatch;
_db = db;
_matcher = matcher;
_metrics = metrics;
@@ -297,6 +299,8 @@ namespace PluralKit.Bot
Task LogMessageToChannel() => _logChannel.LogMessage(ctx, sentMessage, triggerMessage, proxyMessage).AsTask();
Task DispatchWebhook() => _dispatch.Dispatch(ctx.SystemId.Value, sentMessage);
async Task DeleteProxyTriggerMessage()
{
// Wait a second or so before deleting the original message
@@ -315,11 +319,11 @@ namespace PluralKit.Bot
}
// Run post-proxy actions (simultaneously; order doesn't matter)
// Note that only AddMessage is using our passed-in connection, careful not to pass it elsewhere and run into conflicts
await Task.WhenAll(
DeleteProxyTriggerMessage(),
SaveMessageInDatabase(),
LogMessageToChannel()
LogMessageToChannel(),
DispatchWebhook()
);
}