feat(bot): use avater service for image verify

This commit is contained in:
alyssa 2025-02-23 09:33:32 +00:00
parent 5f6c8c0d14
commit d537f05b23
9 changed files with 94 additions and 63 deletions

View file

@ -291,7 +291,7 @@ public class Groups
ctx.CheckOwnGroup(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await _avatarHosting.VerifyAvatarOrThrow(img.Url);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { Icon = img.CleanUrl ?? img.Url });
@ -366,7 +366,7 @@ public class Groups
ctx.CheckOwnGroup(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
await _avatarHosting.VerifyAvatarOrThrow(img.Url, true);
await ctx.Repository.UpdateGroup(target.Id, new GroupPatch { BannerImage = img.CleanUrl ?? img.Url });

View file

@ -83,7 +83,7 @@ public class Member
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await _avatarHosting.VerifyAvatarOrThrow(img.Url);
await ctx.Repository.UpdateMember(member.Id, new MemberPatch { AvatarUrl = img.CleanUrl ?? img.Url }, conn);
dispatchData.Add("avatar_url", img.CleanUrl);

View file

@ -159,7 +159,7 @@ public class MemberAvatar
ctx.CheckSystem().CheckOwnMember(target);
avatarArg = await _avatarHosting.TryRehostImage(avatarArg.Value, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, avatarArg.Value.Url);
await _avatarHosting.VerifyAvatarOrThrow(avatarArg.Value.Url);
await UpdateAvatar(location, ctx, target, avatarArg.Value.CleanUrl ?? avatarArg.Value.Url);
await PrintResponse(location, ctx, target, avatarArg.Value, guildData);
}

View file

@ -231,7 +231,7 @@ public class MemberEdit
{
ctx.CheckOwnMember(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
await _avatarHosting.VerifyAvatarOrThrow(img.Url, true);
await ctx.Repository.UpdateMember(target.Id, new MemberPatch { BannerImage = img.CleanUrl ?? img.Url });

View file

@ -572,7 +572,7 @@ public class SystemEdit
ctx.CheckOwnSystem(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await _avatarHosting.VerifyAvatarOrThrow(img.Url);
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { AvatarUrl = img.CleanUrl ?? img.Url });
@ -659,7 +659,7 @@ public class SystemEdit
ctx.CheckOwnSystem(target);
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Avatar, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url);
await _avatarHosting.VerifyAvatarOrThrow(img.Url);
await ctx.Repository.UpdateSystemGuild(target.Id, ctx.Guild.Id, new SystemGuildPatch { AvatarUrl = img.CleanUrl ?? img.Url });
@ -781,7 +781,7 @@ public class SystemEdit
else if (await ctx.MatchImage() is { } img)
{
img = await _avatarHosting.TryRehostImage(img, AvatarHostingService.RehostedImageType.Banner, ctx.Author.Id, ctx.System);
await AvatarUtils.VerifyAvatarOrThrow(_client, img.Url, true);
await _avatarHosting.VerifyAvatarOrThrow(img.Url, true);
await ctx.Repository.UpdateSystem(target.Id, new SystemPatch { BannerImage = img.CleanUrl ?? img.Url });

View file

@ -18,6 +18,44 @@ public class AvatarHostingService
};
}
public async Task VerifyAvatarOrThrow(string url, bool isBanner = false)
{
if (url.Length > Limits.MaxUriLength)
throw Errors.UrlTooLong(url);
if (!PluralKit.Core.MiscUtils.TryMatchUri(url, out var uri))
throw Errors.InvalidUrl;
if (uri.Host.Contains("toyhou.se"))
throw new PKError("Due to server issues, PluralKit is unable to read images hosted on toyhou.se.");
if (uri.Host == "cdn.pluralkit.me") return;
if (_config.AvatarServiceUrl == null)
return;
var kind = isBanner ? "banner" : "avatar";
try
{
var response = await _client.PostAsJsonAsync(_config.AvatarServiceUrl + "/verify",
new { url, kind });
if (response.StatusCode != HttpStatusCode.OK)
{
var error = await response.Content.ReadFromJsonAsync<ErrorResponse>();
throw new PKError($"{error.Error}");
}
}
catch (TaskCanceledException e)
{
// don't show an internal error to users
if (e.Message.Contains("HttpClient.Timeout"))
throw new PKError("Temporary error setting image, please try again later");
throw;
}
}
public async Task<ParsedImage> TryRehostImage(ParsedImage input, RehostedImageType type, ulong userId, PKSystem? system)
{
try

View file

@ -8,57 +8,6 @@ namespace PluralKit.Bot;
public static class AvatarUtils
{
public static async Task VerifyAvatarOrThrow(HttpClient client, string url, bool isFullSizeImage = false)
{
if (url.Length > Limits.MaxUriLength)
throw Errors.UrlTooLong(url);
// List of MIME types we consider acceptable
var acceptableMimeTypes = new[]
{
"image/jpeg", "image/gif", "image/png", "image/webp"
};
if (!PluralKit.Core.MiscUtils.TryMatchUri(url, out var uri))
throw Errors.InvalidUrl;
if (uri.Host.Contains("toyhou.se"))
throw new PKError("Due to server issues, PluralKit is unable to read images hosted on toyhou.se.");
url = TryRewriteCdnUrl(url);
var response = await client.GetAsync(url);
if (!response.IsSuccessStatusCode) // Check status code
throw Errors.AvatarServerError(response.StatusCode);
if (response.Content.Headers.ContentLength == null) // Check presence of content length
throw Errors.AvatarNotAnImage(null);
try
{
if (!acceptableMimeTypes.Contains(response.Content.Headers.ContentType.MediaType)) // Check MIME type
throw Errors.AvatarNotAnImage(response.Content.Headers.ContentType.MediaType);
}
catch (NullReferenceException)
{
throw new PKError("Could not verify avatar is an image. This can happen when the server sends a malformed response."
+ "\nPlease join the support server for help: <https://discord.gg/PczBt78>");
}
if (isFullSizeImage)
// no need to do size checking on banners
return;
if (response.Content.Headers.ContentLength > Limits.AvatarFileSizeLimit) // Check content length
throw Errors.AvatarFileSizeLimit(response.Content.Headers.ContentLength.Value);
// Parse the image header in a worker
var stream = await response.Content.ReadAsStreamAsync();
var image = await Task.Run(() => Image.Identify(stream));
if (image == null) throw Errors.AvatarInvalid;
if (image.Width > Limits.AvatarDimensionLimit ||
image.Height > Limits.AvatarDimensionLimit) // Check image size
throw Errors.AvatarDimensionsTooLarge(image.Width, image.Height);
}
// Rewrite cdn.discordapp.com URLs to media.discordapp.net for jpg/png files
// This lets us add resizing parameters to "borrow" their media proxy server to downsize the image
// which in turn makes it more likely to be underneath the size limit!