feat: add support for external avatar hosting service (#614)

This commit is contained in:
Astrid 2024-02-11 03:53:46 +01:00 committed by GitHub
parent 8befb1c857
commit 8157c6932e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 94 additions and 5 deletions

View file

@ -25,6 +25,7 @@ public class BotConfig
public string? RedisGatewayUrl { get; set; }
public string? DiscordBaseUrl { get; set; }
public string? AvatarServiceUrl { get; set; }
public bool DisableErrorReporting { get; set; } = false;

View file

@ -21,13 +21,15 @@ public class Groups
private readonly HttpClient _client;
private readonly DispatchService _dispatch;
private readonly EmbedService _embeds;
private readonly AvatarHostingService _avatarHosting;
public Groups(EmbedService embeds, HttpClient client,
DispatchService dispatch)
DispatchService dispatch, AvatarHostingService avatarHosting)
{
_embeds = embeds;
_client = client;
_dispatch = dispatch;
_avatarHosting = avatarHosting;
}
public async Task CreateGroup(Context ctx)
@ -261,6 +263,7 @@ public class Groups
{
ctx.CheckOwnGroup(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.CleanUrl ?? img.Url });
@ -326,6 +329,7 @@ public class Groups
{
ctx.CheckOwnGroup(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.CleanUrl ?? img.Url });

View file

@ -16,13 +16,15 @@ public class Member
private readonly HttpClient _client;
private readonly DispatchService _dispatch;
private readonly EmbedService _embeds;
private readonly AvatarHostingService _avatarHosting;
public Member(EmbedService embeds, HttpClient client,
DispatchService dispatch)
DispatchService dispatch, AvatarHostingService avatarHosting)
{
_embeds = embeds;
_client = client;
_dispatch = dispatch;
_avatarHosting = avatarHosting;
}
public async Task NewMember(Context ctx)
@ -78,6 +80,8 @@ public class Member
uriBuilder.Query = "";
img.CleanUrl = uriBuilder.Uri.AbsoluteUri;
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await ctx.Repository.UpdateMember(member.Id, new MemberPatch { AvatarUrl = img.CleanUrl ?? img.Url }, conn);

View file

@ -9,10 +9,12 @@ namespace PluralKit.Bot;
public class MemberAvatar
{
private readonly HttpClient _client;
private readonly AvatarHostingService _avatarHosting;
public MemberAvatar(HttpClient client)
public MemberAvatar(HttpClient client, AvatarHostingService avatarHosting)
{
_client = client;
_avatarHosting = avatarHosting;
}
private async Task AvatarClear(MemberAvatarLocation location, Context ctx, PKMember target, MemberGuildSettings? mgs)
@ -138,6 +140,8 @@ public class MemberAvatar
}
ctx.CheckSystem().CheckOwnMember(target);
avatarArg = await _avatarHosting.TryRehostImage(avatarArg.Value, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id);
await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Value.Url);
await UpdateAvatar(location, ctx, target, avatarArg.Value.CleanUrl ?? avatarArg.Value.Url);
await PrintResponse(location, ctx, target, avatarArg.Value, guildData);

View file

@ -13,10 +13,12 @@ namespace PluralKit.Bot;
public class MemberEdit
{
private readonly HttpClient _client;
private readonly AvatarHostingService _avatarHosting;
public MemberEdit(HttpClient client)
public MemberEdit(HttpClient client, AvatarHostingService avatarHosting)
{
_client = client;
_avatarHosting = avatarHosting;
}
public async Task Name(Context ctx, PKMember target)
@ -180,6 +182,7 @@ public class MemberEdit
async Task SetBannerImage(ParsedImage img)
{
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = img.CleanUrl ?? img.Url });

View file

@ -18,12 +18,14 @@ public class SystemEdit
private readonly HttpClient _client;
private readonly DataFileService _dataFiles;
private readonly PrivateChannelService _dmCache;
private readonly AvatarHostingService _avatarHosting;
public SystemEdit(DataFileService dataFiles, HttpClient client, PrivateChannelService dmCache)
public SystemEdit(DataFileService dataFiles, HttpClient client, PrivateChannelService dmCache, AvatarHostingService avatarHosting)
{
_dataFiles = dataFiles;
_client = client;
_dmCache = dmCache;
_avatarHosting = avatarHosting;
}
public async Task Name(Context ctx, PKSystem target)
@ -473,6 +475,7 @@ public class SystemEdit
{
ctx.CheckOwnSystem(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.CleanUrl ?? img.Url });
@ -541,6 +544,7 @@ public class SystemEdit
{
ctx.CheckOwnSystem(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.CleanUrl ?? img.Url });
@ -638,6 +642,7 @@ public class SystemEdit
else if (await ctx.MatchImage() is { } img)
{
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = img.CleanUrl ?? img.Url });

View file

@ -136,6 +136,7 @@ public class BotModule: Module
builder.RegisterType<ErrorMessageService>().AsSelf().SingleInstance();
builder.RegisterType<CommandMessageService>().AsSelf().SingleInstance();
builder.RegisterType<InteractionDispatchService>().AsSelf().SingleInstance();
builder.RegisterType<AvatarHostingService>().AsSelf().SingleInstance();
// Sentry stuff
builder.Register(_ => new Scope(null)).AsSelf().InstancePerLifetimeScope();

View file

@ -0,0 +1,65 @@
using System.Net;
using System.Net.Http.Json;
namespace PluralKit.Bot;
public class AvatarHostingService
{
private readonly BotConfig _config;
private readonly HttpClient _client;
public AvatarHostingService(BotConfig config, HttpClient client)
{
_config = config;
_client = client;
}
public async Task<ParsedImage> TryRehostImage(ParsedImage input, RehostedImageType type, ulong userId)
{
var uploaded = await TryUploadAvatar(input.Url, type, userId);
if (uploaded != null)
{
// todo: make new image type called Cdn?
return new ParsedImage { Url = uploaded, Source = AvatarSource.Url };
}
return input;
}
public async Task<string?> TryUploadAvatar(string? avatarUrl, RehostedImageType type, ulong userId)
{
if (!AvatarUtils.IsDiscordCdnUrl(avatarUrl))
return null;
if (_config.AvatarServiceUrl == null)
return null;
var kind = type switch
{
RehostedImageType.Avatar => "avatar",
RehostedImageType.Banner => "banner",
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
};
var response = await _client.PostAsJsonAsync(_config.AvatarServiceUrl + "/pull",
new { url = avatarUrl, kind, uploaded_by = userId });
if (response.StatusCode != HttpStatusCode.OK)
{
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
throw new PKError($"Error uploading image to CDN: {error.Error}");
}
var success = await response.Content.ReadFromJsonAsync<SuccessResponse>();
return success.Url;
}
public record ErrorResponse(string Error);
public record SuccessResponse(string Url, bool New);
public enum RehostedImageType
{
Avatar,
Banner,
}
}

View file

@ -80,4 +80,6 @@ public static class AvatarUtils
return newUrl;
}
public static bool IsDiscordCdnUrl(string? url) => url != null && DiscordCdnUrl.Match(url).Success;
}