From 00d2a8fe89c7e1934647a04b421a812704676cbf Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sat, 9 Sep 2023 20:32:28 +0200 Subject: [PATCH 01/16] Split emot resize module from image transform module --- Kattbot/CommandHandlers/Images/GetBigEmote.cs | 35 ++------- .../CommandHandlers/Images/TransformImage.cs | 77 +++++++++++++++++++ Kattbot/CommandModules/CommandModule.cs | 21 +++-- Kattbot/CommandModules/EmoteModule.cs | 56 ++++++++++++++ Kattbot/CommandModules/ImageModule.cs | 44 +---------- Kattbot/Services/Images/ImageService.cs | 12 +-- 6 files changed, 161 insertions(+), 84 deletions(-) create mode 100644 Kattbot/CommandHandlers/Images/TransformImage.cs create mode 100644 Kattbot/CommandModules/EmoteModule.cs diff --git a/Kattbot/CommandHandlers/Images/GetBigEmote.cs b/Kattbot/CommandHandlers/Images/GetBigEmote.cs index 86e41be..c647db1 100644 --- a/Kattbot/CommandHandlers/Images/GetBigEmote.cs +++ b/Kattbot/CommandHandlers/Images/GetBigEmote.cs @@ -1,6 +1,4 @@ -using System; -using System.IO; -using System.Net.Http; +using System.IO; using System.Threading; using System.Threading.Tasks; using DSharpPlus.CommandsNext; @@ -13,19 +11,16 @@ namespace Kattbot.CommandHandlers.Images; public class GetBigEmoteRequest : CommandRequest { - public static readonly string EffectDeepFry = "deepfry"; - public static readonly string EffectOilPaint = "oilpaint"; - - public GetBigEmoteRequest(CommandContext ctx) + public GetBigEmoteRequest(CommandContext ctx, DiscordEmoji emoji, uint? scaleFactor = null) : base(ctx) { + Emoji = emoji; + ScaleFactor = scaleFactor; } - public DiscordEmoji Emoji { get; set; } = null!; + public DiscordEmoji Emoji { get; set; } public uint? ScaleFactor { get; set; } - - public string? Effect { get; set; } } #pragma warning disable SA1402 // File may only contain a single type @@ -43,37 +38,21 @@ public async Task Handle(GetBigEmoteRequest request, CancellationToken cancellat CommandContext ctx = request.Ctx; DiscordEmoji emoji = request.Emoji; bool hasScaleFactor = request.ScaleFactor.HasValue; - string? effect = request.Effect; string url = emoji.GetEmojiImageUrl(); using var image = await _imageService.DownloadImage(url); - ImageStreamResult imageStreamResult; - - if (effect == GetBigEmoteRequest.EffectDeepFry) - { - uint scaleFactor = request.ScaleFactor.HasValue ? request.ScaleFactor.Value : 2; - imageStreamResult = await _imageService.DeepFryImage(image, scaleFactor); - } - else if (effect == GetBigEmoteRequest.EffectOilPaint) - { - uint scaleFactor = request.ScaleFactor.HasValue ? request.ScaleFactor.Value : 2; - imageStreamResult = await _imageService.OilPaintImage(image, scaleFactor); - } - else - { - imageStreamResult = hasScaleFactor + var imageStreamResult = hasScaleFactor ? await _imageService.ScaleImage(image, request.ScaleFactor!.Value) : await _imageService.GetImageStream(image); - } MemoryStream imageStream = imageStreamResult.MemoryStream; string fileExtension = imageStreamResult.FileExtension; var responseBuilder = new DiscordMessageBuilder(); - string fileName = hasScaleFactor ? "bigger" : "big"; + string fileName = hasScaleFactor ? $"big_x{request.ScaleFactor}" : "big"; responseBuilder.AddFile($"{fileName}.{fileExtension}", imageStream); diff --git a/Kattbot/CommandHandlers/Images/TransformImage.cs b/Kattbot/CommandHandlers/Images/TransformImage.cs new file mode 100644 index 0000000..199c0b4 --- /dev/null +++ b/Kattbot/CommandHandlers/Images/TransformImage.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; +using DSharpPlus.Entities; +using Kattbot.Helpers; +using Kattbot.Services.Images; +using MediatR; + +namespace Kattbot.CommandHandlers.Images; + +public class TransformImageRequest : CommandRequest +{ + public static readonly string EffectDeepFry = "deepfry"; + public static readonly string EffectOilPaint = "oilpaint"; + + public TransformImageRequest(CommandContext ctx, DiscordEmoji emoji, string effect) + : base(ctx) + { + Emoji = emoji; + Effect = effect; + } + + public DiscordEmoji Emoji { get; set; } + + public string Effect { get; set; } +} + +#pragma warning disable SA1402 // File may only contain a single type +public class TransformImageHandler : IRequestHandler +{ + private readonly ImageService _imageService; + + public TransformImageHandler(ImageService imageService) + { + _imageService = imageService; + } + + public async Task Handle(TransformImageRequest request, CancellationToken cancellationToken) + { + CommandContext ctx = request.Ctx; + DiscordEmoji emoji = request.Emoji; + string effect = request.Effect; + + string url = emoji.GetEmojiImageUrl(); + + using var image = await _imageService.DownloadImage(url); + + ImageStreamResult imageStreamResult; + + if (effect == TransformImageRequest.EffectDeepFry) + { + imageStreamResult = await _imageService.DeepFryImage(image); + } + else if (effect == TransformImageRequest.EffectOilPaint) + { + imageStreamResult = await _imageService.OilPaintImage(image); + } + else + { + throw new InvalidOperationException($"Unknown effect: {effect}"); + } + + MemoryStream imageStream = imageStreamResult.MemoryStream; + string fileExtension = imageStreamResult.FileExtension; + + var responseBuilder = new DiscordMessageBuilder(); + + string fileName = effect; + + responseBuilder.AddFile($"{fileName}.{fileExtension}", imageStream); + + await ctx.RespondAsync(responseBuilder); + } +} diff --git a/Kattbot/CommandModules/CommandModule.cs b/Kattbot/CommandModules/CommandModule.cs index 8eba47b..66cd2c1 100644 --- a/Kattbot/CommandModules/CommandModule.cs +++ b/Kattbot/CommandModules/CommandModule.cs @@ -33,14 +33,23 @@ public Task GetHelp(CommandContext ctx) sb.AppendLine($"`{commandPrefix}stats help`"); sb.AppendLine(); - sb.AppendLine("Other commands"); - sb.AppendLine($"`{commandPrefix}prep me`"); - sb.AppendLine($"`{commandPrefix}prep [username]`"); - sb.AppendLine($"`{commandPrefix}meow`"); + sb.AppendLine("Emote commands"); sb.AppendLine($"`{commandPrefix}big [emote]`"); sb.AppendLine($"`{commandPrefix}bigger [emote]`"); - sb.AppendLine($"`{commandPrefix}deepfry [emote]`"); - sb.AppendLine($"`{commandPrefix}oilpaint [emote]`"); + sb.AppendLine($"`{commandPrefix}gigantic [emote]`"); + sb.AppendLine($"`{commandPrefix}humongous [emote]`"); + + sb.AppendLine(); + sb.AppendLine("Image commands"); + sb.AppendLine($"`{commandPrefix}deepfry [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}oilpaint [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}pet [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}dallify [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}dalle [text]`"); + + sb.AppendLine(); + sb.AppendLine("Other commands"); + sb.AppendLine($"`{commandPrefix}meow`"); sb.AppendLine($"*(\"?\" denotes an optional parameter)*"); diff --git a/Kattbot/CommandModules/EmoteModule.cs b/Kattbot/CommandModules/EmoteModule.cs new file mode 100644 index 0000000..61aead3 --- /dev/null +++ b/Kattbot/CommandModules/EmoteModule.cs @@ -0,0 +1,56 @@ +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.Entities; +using Kattbot.Attributes; +using Kattbot.CommandHandlers.Images; +using Kattbot.Workers; + +namespace Kattbot.CommandModules; + +[BaseCommandCheck] +public class EmoteModule : BaseCommandModule +{ + private readonly CommandQueueChannel _commandParallelQueue; + + public EmoteModule(CommandQueueChannel commandParallelQueue) + { + _commandParallelQueue = commandParallelQueue; + } + + [Command("big")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task BigEmote(CommandContext ctx, DiscordEmoji emoji) + { + var request = new GetBigEmoteRequest(ctx, emoji); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + + [Command("bigger")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task BiggerEmote(CommandContext ctx, DiscordEmoji emoji) + { + var request = new GetBigEmoteRequest(ctx, emoji, 2); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + + [Command("gigantic")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task GiganticEmote(CommandContext ctx, DiscordEmoji emoji) + { + var request = new GetBigEmoteRequest(ctx, emoji, 3); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + + [Command("humongous")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task HumongousEmote(CommandContext ctx, DiscordEmoji emoji) + { + var request = new GetBigEmoteRequest(ctx, emoji, 4); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } +} diff --git a/Kattbot/CommandModules/ImageModule.cs b/Kattbot/CommandModules/ImageModule.cs index 10c145e..9efd779 100644 --- a/Kattbot/CommandModules/ImageModule.cs +++ b/Kattbot/CommandModules/ImageModule.cs @@ -18,42 +18,11 @@ public ImageModule(CommandQueueChannel commandParallelQueue) _commandParallelQueue = commandParallelQueue; } - [Command("big")] - [Cooldown(5, 10, CooldownBucketType.Global)] - public Task BigEmote(CommandContext ctx, DiscordEmoji emoji) - { - var request = new GetBigEmoteRequest(ctx) - { - Emoji = emoji, - }; - - return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); - } - - [Command("bigger")] - [Cooldown(5, 10, CooldownBucketType.Global)] - public Task BiggerEmote(CommandContext ctx, DiscordEmoji emoji) - { - var request = new GetBigEmoteRequest(ctx) - { - Emoji = emoji, - ScaleFactor = 2, - }; - - return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); - } - [Command("deepfry")] [Cooldown(5, 10, CooldownBucketType.Global)] public Task DeepFryEmote(CommandContext ctx, DiscordEmoji emoji) { - var request = new GetBigEmoteRequest(ctx) - { - Emoji = emoji, - ScaleFactor = 2, - Effect = GetBigEmoteRequest.EffectDeepFry, - }; - + var request = new TransformImageRequest(ctx, emoji, TransformImageRequest.EffectDeepFry); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } @@ -61,19 +30,14 @@ public Task DeepFryEmote(CommandContext ctx, DiscordEmoji emoji) [Cooldown(5, 10, CooldownBucketType.Global)] public Task OilPaintEmote(CommandContext ctx, DiscordEmoji emoji) { - var request = new GetBigEmoteRequest(ctx) - { - Emoji = emoji, - ScaleFactor = 2, - Effect = GetBigEmoteRequest.EffectOilPaint, - }; + var request = new TransformImageRequest(ctx, emoji, TransformImageRequest.EffectOilPaint); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } [Command("pet")] [Cooldown(5, 10, CooldownBucketType.Global)] - public Task PetEmote(CommandContext ctx, DiscordEmoji emoji, string speed = null) + public Task PetEmote(CommandContext ctx, DiscordEmoji emoji, string? speed = null) { var request = new GetAnimatedEmoji(ctx) { @@ -86,7 +50,7 @@ public Task PetEmote(CommandContext ctx, DiscordEmoji emoji, string speed = null [Command("pet")] [Cooldown(5, 10, CooldownBucketType.Global)] - public Task PetUser(CommandContext ctx, DiscordUser user, string speed = null) + public Task PetUser(CommandContext ctx, DiscordUser user, string? speed = null) { var request = new GetAnimatedUserAvatar(ctx) { diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index e243d90..aade37f 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -67,14 +67,10 @@ public Task ScaleImage(Image image, uint scaleFactor) return GetImageStream(image); } - public Task DeepFryImage(Image image, uint scaleFactor) + public Task DeepFryImage(Image image) { - int newWidth = image.Width * (int)scaleFactor; - int newHeight = image.Height * (int)scaleFactor; - image.Mutate(i => { - i.Resize(newWidth, newHeight, KnownResamplers.Welch); i.Contrast(5f); i.Brightness(1.5f); i.GaussianSharpen(5f); @@ -84,16 +80,12 @@ public Task DeepFryImage(Image image, uint scaleFactor) return GetImageStream(image); } - public Task OilPaintImage(Image image, uint scaleFactor) + public Task OilPaintImage(Image image) { - int newWidth = image.Width * (int)scaleFactor; - int newHeight = image.Height * (int)scaleFactor; - int paintLevel = 25; image.Mutate(i => { - i.Resize(newWidth, newHeight, KnownResamplers.Welch); i.OilPaint(paintLevel, paintLevel); }); From e88fc1fdd0251587b65694fed409c5b97a381c21 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sat, 9 Sep 2023 23:14:04 +0200 Subject: [PATCH 02/16] Dallify (emojis) --- .../{DallePromptCommand.cs => DallePrompt.cs} | 10 +-- .../CommandHandlers/Images/DallifyImage.cs | 88 +++++++++++++++++++ Kattbot/CommandModules/ImageModule.cs | 9 ++ Kattbot/Services/Dalle/DalleHttpClient.cs | 48 +++++++++- Kattbot/Services/Dalle/DalleModels.cs | 41 +++++++++ Kattbot/Services/Images/ImageService.cs | 26 ++++++ 6 files changed, 216 insertions(+), 6 deletions(-) rename Kattbot/CommandHandlers/Images/{DallePromptCommand.cs => DallePrompt.cs} (91%) create mode 100644 Kattbot/CommandHandlers/Images/DallifyImage.cs diff --git a/Kattbot/CommandHandlers/Images/DallePromptCommand.cs b/Kattbot/CommandHandlers/Images/DallePrompt.cs similarity index 91% rename from Kattbot/CommandHandlers/Images/DallePromptCommand.cs rename to Kattbot/CommandHandlers/Images/DallePrompt.cs index 5a40b82..f1652ce 100644 --- a/Kattbot/CommandHandlers/Images/DallePromptCommand.cs +++ b/Kattbot/CommandHandlers/Images/DallePrompt.cs @@ -14,23 +14,23 @@ namespace Kattbot.CommandHandlers.Images; #pragma warning disable SA1402 // File may only contain a single type public class DallePromptCommand : CommandRequest { - public string Prompt { get; set; } - public DallePromptCommand(CommandContext ctx, string prompt) : base(ctx) { Prompt = prompt; } + + public string Prompt { get; set; } } -public class DallePromptCommandHandler : IRequestHandler +public class DallePromptHandler : IRequestHandler { private const int MaxEmbedTitleLength = 256; private readonly DalleHttpClient _dalleHttpClient; private readonly ImageService _imageService; - public DallePromptCommandHandler(DalleHttpClient dalleHttpClient, ImageService imageService) + public DallePromptHandler(DalleHttpClient dalleHttpClient, ImageService imageService) { _dalleHttpClient = dalleHttpClient; _imageService = imageService; @@ -42,7 +42,7 @@ public async Task Handle(DallePromptCommand request, CancellationToken cancellat try { - var response = await _dalleHttpClient.CreateImage(new CreateImageRequest { Prompt = request.Prompt }); + var response = await _dalleHttpClient.CreateImage(new CreateImageRequest { Prompt = request.Prompt, User = request.Ctx.User.Id.ToString() }); if (response.Data == null || !response.Data.Any()) throw new Exception("Empty result"); diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs new file mode 100644 index 0000000..df9ba22 --- /dev/null +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; +using DSharpPlus.Entities; +using Kattbot.Helpers; +using Kattbot.Services.Dalle; +using Kattbot.Services.Images; +using MediatR; + +namespace Kattbot.CommandHandlers.Images; + +#pragma warning disable SA1402 // File may only contain a single type +public class DallifyImageCommand : CommandRequest +{ + public DallifyImageCommand(CommandContext ctx, DiscordEmoji emoji) + : base(ctx) + { + Emoji = emoji; + } + + public DiscordEmoji Emoji { get; set; } +} + +public class DallifyImageHandler : IRequestHandler +{ + private readonly DalleHttpClient _dalleHttpClient; + private readonly ImageService _imageService; + + public DallifyImageHandler(DalleHttpClient dalleHttpClient, ImageService imageService) + { + _dalleHttpClient = dalleHttpClient; + _imageService = imageService; + } + + public async Task Handle(DallifyImageCommand request, CancellationToken cancellationToken) + { + DiscordEmoji emoji = request.Emoji; + + DiscordMessage message = await request.Ctx.RespondAsync("Working on it"); + + try + { + string url = emoji.GetEmojiImageUrl(); + + using var emojiImage = await _imageService.DownloadImage(url); + + var emojiImageAsPng = await ImageService.ConvertImageToPng(emojiImage); + + var squaredEmojiImage = await _imageService.SquareImage(emojiImageAsPng); + + string fileName = $"{Guid.NewGuid()}.png"; + + const string size = "256x256"; + + var imageVariationRequest = new CreateImageVariationRequest + { + Image = squaredEmojiImage.MemoryStream.ToArray(), + Size = size, + User = request.Ctx.User.Id.ToString(), + }; + + var response = await _dalleHttpClient.CreateImageVariation(imageVariationRequest, fileName); + + if (response.Data == null || !response.Data.Any()) throw new Exception("Empty result"); + + var imageUrl = response.Data.First(); + + var image = await _imageService.DownloadImage(imageUrl.Url); + + var imageStream = await _imageService.GetImageStream(image); + + DiscordMessageBuilder mb = new DiscordMessageBuilder() + .AddFile(fileName, imageStream.MemoryStream) + .WithContent($"There you go {request.Ctx.Member?.Mention ?? "Unknown user"}"); + + await message.DeleteAsync(); + + await request.Ctx.RespondAsync(mb); + } + catch (Exception) + { + await message.DeleteAsync(); + throw; + } + } +} diff --git a/Kattbot/CommandModules/ImageModule.cs b/Kattbot/CommandModules/ImageModule.cs index 9efd779..09c2117 100644 --- a/Kattbot/CommandModules/ImageModule.cs +++ b/Kattbot/CommandModules/ImageModule.cs @@ -69,4 +69,13 @@ public Task Dalle(CommandContext ctx, [RemainingText] string prompt) return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } + + [Command("dallify")] + [Cooldown(5, 60, CooldownBucketType.Global)] + public Task Dallify(CommandContext ctx, DiscordEmoji emoji) + { + var request = new DallifyImageCommand(ctx, emoji); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } } diff --git a/Kattbot/Services/Dalle/DalleHttpClient.cs b/Kattbot/Services/Dalle/DalleHttpClient.cs index f074849..56b4795 100644 --- a/Kattbot/Services/Dalle/DalleHttpClient.cs +++ b/Kattbot/Services/Dalle/DalleHttpClient.cs @@ -19,7 +19,6 @@ public DalleHttpClient(HttpClient client, IOptions options) _client = client; _client.BaseAddress = new Uri("https://api.openai.com/v1/images/"); - _client.DefaultRequestHeaders.Add("Accept", "application/json"); _client.DefaultRequestHeaders.Add("Authorization", $"Bearer {options.Value.OpenAiApiKey}"); } @@ -55,4 +54,51 @@ public async Task CreateImage(CreateImageRequest request) throw new Exception($"HTTP {ex.StatusCode}: {ex.Message}"); } } + + public async Task CreateImageVariation(CreateImageVariationRequest request, string fileName) + { + var postBody = new MultipartFormDataContent(); + + if (request.N.HasValue) + postBody.Add(new StringContent(request.N.ToString()!), "n"); + + if (request.Size != null) + postBody.Add(new StringContent(request.Size), "size"); + + if (request.ResponseFormat != null) + postBody.Add(new StringContent(request.ResponseFormat), "response_format"); + + if (request.User != null) + postBody.Add(new StringContent(request.User), "user"); + + postBody.Add(new ByteArrayContent(request.Image), "image", fileName); + + HttpResponseMessage? response; + Stream? responseContentStream = null; + + try + { + response = await _client.PostAsync("variations", postBody); + + responseContentStream = await response.Content.ReadAsStreamAsync(); + + response.EnsureSuccessStatusCode(); + + var parsedResponse = await JsonSerializer.DeserializeAsync(responseContentStream) + ?? throw new Exception("Failed to parse response"); + + return parsedResponse; + } + catch (HttpRequestException) when (responseContentStream != null) + { + var parsedResponse = await JsonSerializer.DeserializeAsync(responseContentStream) + ?? throw new Exception("Failed to parse error response"); + + throw new Exception(parsedResponse.Error.Message); + } + catch (HttpRequestException ex) + { + throw new Exception($"HTTP {ex.StatusCode}: {ex.Message}"); + } + } } diff --git a/Kattbot/Services/Dalle/DalleModels.cs b/Kattbot/Services/Dalle/DalleModels.cs index 3f16cce..d262697 100644 --- a/Kattbot/Services/Dalle/DalleModels.cs +++ b/Kattbot/Services/Dalle/DalleModels.cs @@ -3,6 +3,7 @@ namespace Kattbot.Services.Dalle; +#pragma warning disable SA1402 // File may only contain a single type public record CreateImageRequest { /// @@ -43,6 +44,46 @@ public record CreateImageRequest public string? User { get; set; } } +public record CreateImageVariationRequest +{ + /// + /// Gets or sets the image to use as the basis for the variation(s). Must be a valid PNG file, less than 4MB, and square. + /// https://platform.openai.com/docs/api-reference/images/createVariation#image. + /// + [JsonPropertyName("image")] + public byte[] Image { get; set; } = null!; + + /// + /// Gets or sets the number of images to generate. Must be between 1 and 10. + /// Defaults to 1. + /// https://platform.openai.com/docs/api-reference/images/createVariation#n. + /// + [JsonPropertyName("n")] + public int? N { get; set; } + + /// + /// Gets or sets the size of the generated images. Must be one of 256x256, 512x512, or 1024x1024. + /// Defaults to 1024x1024 + /// https://platform.openai.com/docs/api-reference/images/createVariation#size. + /// + [JsonPropertyName("size")] + public string? Size { get; set; } + + /// + /// Gets or sets the format in which the generated images are returned. Must be one of url or b64_json. + /// Defaults to url + /// https://platform.openai.com/docs/api-reference/images/createVariation#response_format. + /// + [JsonPropertyName("response_format")] + public string? ResponseFormat { get; set; } + + /// + /// Gets or sets a unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. + /// https://platform.openai.com/docs/api-reference/images/createVariation#user. + /// + public string? User { get; set; } +} + public record CreateImageResponse { [JsonPropertyName("created")] diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index aade37f..031fdb6 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -33,6 +33,20 @@ public static Image LoadImage(byte[] imageBytes) return Image.Load(imageBytes); } + public static async Task ConvertImageToPng(Image image) + { + if (image.Metadata.DecodedImageFormat is PngFormat) + return image; + + using var pngMemoryStream = new MemoryStream(); + + await image.SaveAsPngAsync(pngMemoryStream); + + var convertedImage = await Image.LoadAsync(pngMemoryStream); + + return convertedImage; + } + public async Task DownloadImage(string url) { byte[] imageBytes; @@ -110,6 +124,18 @@ public Image CropImageToCircle(Image image) return cloned; } + public Task SquareImage(Image image) + { + int newSize = Math.Min(image.Width, image.Height); + + image.Mutate(i => + { + i.Resize(newSize, newSize); + }); + + return GetImageStream(image); + } + public async Task CombineImages(string[] base64Images) { IEnumerable bytesImages = base64Images.Select(Convert.FromBase64String); From 73f81aa2e15a95dd30b8b486e80949e9ca8a5cbe Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 10 Sep 2023 17:58:44 +0200 Subject: [PATCH 03/16] Display help command text in embeds --- Kattbot/CommandModules/CommandModule.cs | 63 ------------- Kattbot/CommandModules/HelpModule.cs | 93 ++++++++++++++++++ .../ResultFormatters/EmbedBuilderHelper.cs | 94 ------------------- Kattbot/CommandModules/StatsCommandModule.cs | 29 ------ Kattbot/Helpers/DiscordConstants.cs | 1 + Kattbot/Helpers/EmbedBuilderHelper.cs | 24 +++++ 6 files changed, 118 insertions(+), 186 deletions(-) delete mode 100644 Kattbot/CommandModules/CommandModule.cs create mode 100644 Kattbot/CommandModules/HelpModule.cs delete mode 100644 Kattbot/CommandModules/ResultFormatters/EmbedBuilderHelper.cs create mode 100644 Kattbot/Helpers/EmbedBuilderHelper.cs diff --git a/Kattbot/CommandModules/CommandModule.cs b/Kattbot/CommandModules/CommandModule.cs deleted file mode 100644 index 66cd2c1..0000000 --- a/Kattbot/CommandModules/CommandModule.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Text; -using System.Threading.Tasks; -using DSharpPlus.CommandsNext; -using DSharpPlus.CommandsNext.Attributes; -using Kattbot.Attributes; -using Kattbot.CommandModules.ResultFormatters; -using Microsoft.Extensions.Options; - -namespace Kattbot.CommandModules; - -[BaseCommandCheck] -public class CommandModule : BaseCommandModule -{ - private readonly BotOptions _options; - - public CommandModule( - IOptions options) - { - _options = options.Value; - } - - [Command("help")] - [Description("Return some help")] - public Task GetHelp(CommandContext ctx) - { - var sb = new StringBuilder(); - - string commandPrefix = _options.CommandPrefix; - - sb.AppendLine($"`{commandPrefix}stats me [?interval] [?page]`"); - sb.AppendLine($"`{commandPrefix}stats best [?interval] [?page]`"); - sb.AppendLine($"`{commandPrefix}stats [emote] [?interval]`"); - sb.AppendLine($"`{commandPrefix}stats help`"); - - sb.AppendLine(); - sb.AppendLine("Emote commands"); - sb.AppendLine($"`{commandPrefix}big [emote]`"); - sb.AppendLine($"`{commandPrefix}bigger [emote]`"); - sb.AppendLine($"`{commandPrefix}gigantic [emote]`"); - sb.AppendLine($"`{commandPrefix}humongous [emote]`"); - - sb.AppendLine(); - sb.AppendLine("Image commands"); - sb.AppendLine($"`{commandPrefix}deepfry [emote|user|image]`"); - sb.AppendLine($"`{commandPrefix}oilpaint [emote|user|image]`"); - sb.AppendLine($"`{commandPrefix}pet [emote|user|image]`"); - sb.AppendLine($"`{commandPrefix}dallify [emote|user|image]`"); - sb.AppendLine($"`{commandPrefix}dalle [text]`"); - - sb.AppendLine(); - sb.AppendLine("Other commands"); - sb.AppendLine($"`{commandPrefix}meow`"); - - sb.AppendLine($"*(\"?\" denotes an optional parameter)*"); - - sb.AppendLine(); - sb.AppendLine("Kattbot source code: github.com/selfdocumentingcode/Kattbot"); - - string result = FormattedResultHelper.BuildMessage($"Commands", sb.ToString()); - - return ctx.RespondAsync(result); - } -} diff --git a/Kattbot/CommandModules/HelpModule.cs b/Kattbot/CommandModules/HelpModule.cs new file mode 100644 index 0000000..11a50bc --- /dev/null +++ b/Kattbot/CommandModules/HelpModule.cs @@ -0,0 +1,93 @@ +using System.Text; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; +using DSharpPlus.CommandsNext.Attributes; +using Kattbot.Attributes; +using Kattbot.CommandModules.ResultFormatters; +using Kattbot.Helpers; +using Microsoft.Extensions.Options; + +namespace Kattbot.CommandModules; + +[BaseCommandCheck] +[Group("help")] +public class HelpModule : BaseCommandModule +{ + private readonly BotOptions _options; + + public HelpModule( + IOptions options) + { + _options = options.Value; + } + + [GroupCommand] + [Description("Return some help")] + public Task GetHelp(CommandContext ctx) + { + var sb = new StringBuilder(); + + string commandPrefix = _options.CommandPrefix; + + sb.AppendLine($"`{commandPrefix}stats me [?interval] [?page]`"); + sb.AppendLine($"`{commandPrefix}stats best [?interval] [?page]`"); + sb.AppendLine($"`{commandPrefix}stats [emote] [?interval]`"); + sb.AppendLine($"`\"?\" denotes an optional parameter`"); + sb.AppendLine($"`{commandPrefix}help stats .. More information about stats command`"); + + sb.AppendLine(); + sb.AppendLine("Emote commands"); + sb.AppendLine($"`{commandPrefix}big [emote]`"); + sb.AppendLine($"`{commandPrefix}bigger [emote]`"); + sb.AppendLine($"`{commandPrefix}gigantic [emote]`"); + sb.AppendLine($"`{commandPrefix}humongous [emote]`"); + + sb.AppendLine(); + sb.AppendLine("Image commands"); + sb.AppendLine($"`{commandPrefix}deepfry [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}oilpaint [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}pet [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}dallify [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}dalle [text]`"); + + sb.AppendLine(); + sb.AppendLine("Other commands"); + sb.AppendLine($"`{commandPrefix}meow`"); + + sb.AppendLine(); + sb.AppendLine("Kattbot source code: [github.com/selfdocumentingcode/Kattbot](https://github.com/selfdocumentingcode/Kattbot)"); + + var eb = EmbedBuilderHelper.BuildSimpleEmbed("Help", sb.ToString()); + + return ctx.RespondAsync(eb); + } + + [Command("stats")] + [Description("Help about stats")] + public Task GetHelpStats(CommandContext ctx) + { + var sb = new StringBuilder(); + + string commandPrefix = _options.CommandPrefix; + + sb.AppendLine(); + sb.AppendLine($"Command arguments:"); + sb.AppendLine($"`username .. Discord username with # identifier or @mention`"); + sb.AppendLine($"`emote .. Discord emote (server emotes only)`"); + sb.AppendLine($"`-p, --page .. Displays a different page of the result set (default 1st page)`"); + sb.AppendLine($"`-i, --interval .. Limits result set to given interval (default 2 months)`"); + sb.AppendLine($"` Valid interval units: \"m\", \"w\", \"d\"`"); + sb.AppendLine($"` Optionally use interval value \"lifetime\"`"); + sb.AppendLine(); + sb.AppendLine($"Usage examples:"); + sb.AppendLine($"`{commandPrefix}stats best`"); + sb.AppendLine($"`{commandPrefix}stats worst --page 2`"); + sb.AppendLine($"`{commandPrefix}stats User#1234 --interval 3m`"); + sb.AppendLine($"`{commandPrefix}stats me -p 2 -i 2w`"); + sb.AppendLine($"`{commandPrefix}stats :a_server_emote:`"); + + var eb = EmbedBuilderHelper.BuildSimpleEmbed("Shows server-wide emote stats-or for a specific user", sb.ToString()); + + return ctx.RespondAsync(eb); + } +} diff --git a/Kattbot/CommandModules/ResultFormatters/EmbedBuilderHelper.cs b/Kattbot/CommandModules/ResultFormatters/EmbedBuilderHelper.cs deleted file mode 100644 index 511e8cb..0000000 --- a/Kattbot/CommandModules/ResultFormatters/EmbedBuilderHelper.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using DSharpPlus.Entities; -using Kattbot.Common.Models.Emotes; -using Kattbot.Helpers; - -namespace Kattbot.CommandModules.ResultFormatters; - -public static class EmbedBuilderHelper -{ - public static string EmbedSpacer = "\u200b"; - - public static DiscordEmbed BuildSimpleEmbed(string title, string message) - { - var eb = new DiscordEmbedBuilder(); - - eb.AddField(title, message); - - DiscordEmbed result = eb.Build(); - - return result; - } - - public static List FormatEmoteStats(List emoteStats) - { - int rank = 1; - - var textLines = new List(); - - foreach (EmoteStats emoteUsage in emoteStats) - { - string emoteCode = EmoteHelper.BuildEmoteCode(emoteUsage.EmoteId, emoteUsage.IsAnimated); - - textLines.Add($"{rank}.{emoteCode}:{emoteUsage.Usage}"); - - rank++; - } - - return textLines; - } - - public static List FormatExtendedEmoteStats(List emoteStats) - { - int rank = 1; - - var textLines = new List(); - - foreach (ExtendedEmoteStats emoteUsage in emoteStats) - { - string formattedPercentage = emoteUsage.PercentageOfTotal.ToString("P"); - - textLines.Add($"{rank}.{emoteUsage.EmoteCode}:{emoteUsage.Usage} ({formattedPercentage})"); - - rank++; - } - - return textLines; - } - - public static DiscordEmbed BuildEmbedFromLines(string title, List allLines, int linesPerColumn = 10) - { - int columnCount = (int)Math.Ceiling((double)allLines.Count / linesPerColumn); - - var sbs = new StringBuilder[columnCount]; - - for (int i = 0; i < allLines.Count; i++) - { - int columnToPushTo = i / linesPerColumn; - - if (sbs[columnToPushTo] == null) - { - sbs[columnToPushTo] = new StringBuilder(); - } - - sbs[columnToPushTo].AppendLine(allLines[i]); - } - - var eb = new DiscordEmbedBuilder(); - - eb.WithTitle(title); - - for (int i = 0; i < sbs.Length; i++) - { - string sbString = sbs[i].ToString(); - - eb.AddField(EmbedSpacer, sbString, true); - } - - DiscordEmbed result = eb.Build(); - - return result; - } -} diff --git a/Kattbot/CommandModules/StatsCommandModule.cs b/Kattbot/CommandModules/StatsCommandModule.cs index 49bf8e8..cb42908 100644 --- a/Kattbot/CommandModules/StatsCommandModule.cs +++ b/Kattbot/CommandModules/StatsCommandModule.cs @@ -163,35 +163,6 @@ private Task GetEmoteStats(CommandContext ctx, TempEmote emote, string interval) return _commandQueue.Writer.WriteAsync(request).AsTask(); } - [Command("help")] - [Description("Help about stats")] - public Task GetHelpStats(CommandContext ctx) - { - var sb = new StringBuilder(); - - string commandPrefix = _options.CommandPrefix; - - sb.AppendLine(); - sb.AppendLine($"Command arguments:"); - sb.AppendLine($"`username .. Discord username with # identifier or @mention`"); - sb.AppendLine($"`emote .. Discord emote (server emotes only)`"); - sb.AppendLine($"`-p, --page .. Displays a different page of the result set (default 1st page)`"); - sb.AppendLine($"`-i, --interval .. Limits result set to given interval (default 2 months)`"); - sb.AppendLine($"` Valid interval units: \"m\", \"w\", \"d\"`"); - sb.AppendLine($"` Optionally use interval value \"lifetime\"`"); - sb.AppendLine(); - sb.AppendLine($"Usage examples:"); - sb.AppendLine($"`{commandPrefix}stats best`"); - sb.AppendLine($"`{commandPrefix}stats worst --page 2`"); - sb.AppendLine($"`{commandPrefix}stats User#1234 --interval 3m`"); - sb.AppendLine($"`{commandPrefix}stats me -p 2 -i 2w`"); - sb.AppendLine($"`{commandPrefix}stats :a_server_emote:`"); - - string result = FormattedResultHelper.BuildMessage($"Shows server-wide emote stats-or for a specific user", sb.ToString()); - - return ctx.RespondAsync(result); - } - private bool TryGetDateFromInterval(IntervalValue interval, out DateTime? dateTime) { if (interval.IsLifetime) diff --git a/Kattbot/Helpers/DiscordConstants.cs b/Kattbot/Helpers/DiscordConstants.cs index 5afb424..0375677 100644 --- a/Kattbot/Helpers/DiscordConstants.cs +++ b/Kattbot/Helpers/DiscordConstants.cs @@ -4,4 +4,5 @@ public class DiscordConstants { public const int MaxMessageLength = 2000; public const int MaxEmbedContentLength = 4096; + public const int DefaultEmbedColor = 9648895; // #4d5cac } diff --git a/Kattbot/Helpers/EmbedBuilderHelper.cs b/Kattbot/Helpers/EmbedBuilderHelper.cs new file mode 100644 index 0000000..aa5f18d --- /dev/null +++ b/Kattbot/Helpers/EmbedBuilderHelper.cs @@ -0,0 +1,24 @@ +using System; +using DSharpPlus.Entities; + +namespace Kattbot.Helpers; + +public static class EmbedBuilderHelper +{ + public static DiscordEmbed BuildSimpleEmbed(string title, string message) + { + return BuildSimpleEmbed(title, message, DiscordConstants.DefaultEmbedColor); + } + + public static DiscordEmbed BuildSimpleEmbed(string title, string message, int color) + { + var truncatedMessage = message.Substring(0, Math.Min(message.Length, DiscordConstants.MaxEmbedContentLength)); + + var eb = new DiscordEmbedBuilder() + .WithTitle(title) + .WithDescription(truncatedMessage) + .WithColor(color); + + return eb.Build(); + } +} From c60487031c87ab22dc59e262ed83d473adf8be3a Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 10 Sep 2023 19:00:15 +0200 Subject: [PATCH 04/16] Dallify user --- Kattbot/CommandHandlers/Images/DallePrompt.cs | 8 +- .../CommandHandlers/Images/DallifyImage.cs | 88 ++++++++++++++++++- .../{GetAnimatedImages.cs => PetImage.cs} | 41 ++++----- Kattbot/CommandModules/HelpModule.cs | 6 +- Kattbot/CommandModules/ImageModule.cs | 15 +++- Kattbot/Helpers/StringExtensions.cs | 12 +++ Kattbot/Services/Images/ImageService.cs | 11 ++- 7 files changed, 143 insertions(+), 38 deletions(-) rename Kattbot/CommandHandlers/Images/{GetAnimatedImages.cs => PetImage.cs} (70%) diff --git a/Kattbot/CommandHandlers/Images/DallePrompt.cs b/Kattbot/CommandHandlers/Images/DallePrompt.cs index f1652ce..ac59455 100644 --- a/Kattbot/CommandHandlers/Images/DallePrompt.cs +++ b/Kattbot/CommandHandlers/Images/DallePrompt.cs @@ -1,10 +1,10 @@ using System; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using DSharpPlus.CommandsNext; using DSharpPlus.Entities; +using Kattbot.Helpers; using Kattbot.Services.Dalle; using Kattbot.Services.Images; using MediatR; @@ -52,11 +52,7 @@ public async Task Handle(DallePromptCommand request, CancellationToken cancellat var imageStream = await _imageService.GetImageStream(image); - string safeFileName = new(Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(request.Prompt)) - .Select(c => char.IsLetterOrDigit(c) ? c : '_') - .ToArray()); - - string fileName = $"{safeFileName}.{imageStream.FileExtension}"; + var fileName = request.Prompt.ToSafeFilename(imageStream.FileExtension); var truncatedPrompt = request.Prompt.Length > MaxEmbedTitleLength ? $"{request.Prompt[..(MaxEmbedTitleLength - 3)]}..." diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index df9ba22..1b5820e 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -12,9 +12,9 @@ namespace Kattbot.CommandHandlers.Images; #pragma warning disable SA1402 // File may only contain a single type -public class DallifyImageCommand : CommandRequest +public class DallifyEmoteRequest : CommandRequest { - public DallifyImageCommand(CommandContext ctx, DiscordEmoji emoji) + public DallifyEmoteRequest(CommandContext ctx, DiscordEmoji emoji) : base(ctx) { Emoji = emoji; @@ -23,7 +23,19 @@ public DallifyImageCommand(CommandContext ctx, DiscordEmoji emoji) public DiscordEmoji Emoji { get; set; } } -public class DallifyImageHandler : IRequestHandler +public class DallifyUserRequest : CommandRequest +{ + public DallifyUserRequest(CommandContext ctx, DiscordUser user) + : base(ctx) + { + User = user; + } + + public DiscordUser User { get; set; } +} + +public class DallifyImageHandler : IRequestHandler, + IRequestHandler { private readonly DalleHttpClient _dalleHttpClient; private readonly ImageService _imageService; @@ -34,7 +46,7 @@ public DallifyImageHandler(DalleHttpClient dalleHttpClient, ImageService imageSe _imageService = imageService; } - public async Task Handle(DallifyImageCommand request, CancellationToken cancellationToken) + public async Task Handle(DallifyEmoteRequest request, CancellationToken cancellationToken) { DiscordEmoji emoji = request.Emoji; @@ -85,4 +97,72 @@ public async Task Handle(DallifyImageCommand request, CancellationToken cancella throw; } } + + public async Task Handle(DallifyUserRequest request, CancellationToken cancellationToken) + { + CommandContext ctx = request.Ctx; + DiscordUser user = request.User; + DiscordGuild guild = ctx.Guild; + + DiscordMessage message = await request.Ctx.RespondAsync("Working on it"); + + DiscordMember? userAsMember = await ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); + + string avatarUrl = userAsMember.GuildAvatarUrl ?? userAsMember.AvatarUrl; + + if (string.IsNullOrEmpty(avatarUrl)) + { + throw new Exception("Couldn't load user avatar"); + } + + try + { + using var inputImage = await _imageService.DownloadImage(avatarUrl); + + var avatarAsPng = await ImageService.ConvertImageToPng(inputImage); + + var avatarImageStream = await _imageService.GetImageStream(avatarAsPng); + + string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(avatarImageStream.FileExtension); + + const string size = "256x256"; + + var imageVariationRequest = new CreateImageVariationRequest + { + Image = avatarImageStream.MemoryStream.ToArray(), + Size = size, + User = request.Ctx.User.Id.ToString(), + }; + + var response = await _dalleHttpClient.CreateImageVariation(imageVariationRequest, imageFilename); + + if (response.Data == null || !response.Data.Any()) throw new Exception("Empty result"); + + var imageUrl = response.Data.First(); + + var image = await _imageService.DownloadImage(imageUrl.Url); + + var imageStream = await _imageService.GetImageStream(image); + + DiscordMessageBuilder mb = new DiscordMessageBuilder() + .AddFile(imageFilename, imageStream.MemoryStream) + .WithContent($"There you go {request.Ctx.Member?.Mention ?? "Unknown user"}"); + + await message.DeleteAsync(); + + await request.Ctx.RespondAsync(mb); + } + catch (Exception) + { + await message.DeleteAsync(); + throw; + } + } + + private Task ResolveGuildMember(DiscordGuild guild, ulong userId) + { + bool memberExists = guild.Members.TryGetValue(userId, out DiscordMember? member); + + return memberExists ? Task.FromResult(member) : guild.GetMemberAsync(userId); + } } diff --git a/Kattbot/CommandHandlers/Images/GetAnimatedImages.cs b/Kattbot/CommandHandlers/Images/PetImage.cs similarity index 70% rename from Kattbot/CommandHandlers/Images/GetAnimatedImages.cs rename to Kattbot/CommandHandlers/Images/PetImage.cs index 663990b..e4d2b2f 100644 --- a/Kattbot/CommandHandlers/Images/GetAnimatedImages.cs +++ b/Kattbot/CommandHandlers/Images/PetImage.cs @@ -9,10 +9,11 @@ using Microsoft.Extensions.Logging; namespace Kattbot.CommandHandlers.Images; + #pragma warning disable SA1402 // File may only contain a single type -public class GetAnimatedEmoji : CommandRequest +public class PetEmoteRequest : CommandRequest { - public GetAnimatedEmoji(CommandContext ctx) + public PetEmoteRequest(CommandContext ctx) : base(ctx) { } @@ -22,9 +23,9 @@ public GetAnimatedEmoji(CommandContext ctx) public string? Speed { get; internal set; } } -public class GetAnimatedUserAvatar : CommandRequest +public class PetUserRequest : CommandRequest { - public GetAnimatedUserAvatar(CommandContext ctx) + public PetUserRequest(CommandContext ctx) : base(ctx) { } @@ -34,21 +35,21 @@ public GetAnimatedUserAvatar(CommandContext ctx) public string? Speed { get; internal set; } } -public class GetAnimatedImagesHandlers : IRequestHandler, - IRequestHandler +public class PetImageHandlers : IRequestHandler, + IRequestHandler { private readonly ImageService _imageService; private readonly PetPetClient _petPetClient; - private readonly ILogger _logger; + private readonly ILogger _logger; - public GetAnimatedImagesHandlers(ImageService imageService, PetPetClient petPetClient, ILogger logger) + public PetImageHandlers(ImageService imageService, PetPetClient petPetClient, ILogger logger) { _imageService = imageService; _petPetClient = petPetClient; _logger = logger; } - public async Task Handle(GetAnimatedEmoji request, CancellationToken cancellationToken) + public async Task Handle(PetEmoteRequest request, CancellationToken cancellationToken) { CommandContext ctx = request.Ctx; DiscordEmoji emoji = request.Emoji; @@ -73,18 +74,13 @@ public async Task Handle(GetAnimatedEmoji request, CancellationToken cancellatio await ctx.RespondAsync(responseBuilder); } - public async Task Handle(GetAnimatedUserAvatar request, CancellationToken cancellationToken) + public async Task Handle(PetUserRequest request, CancellationToken cancellationToken) { CommandContext ctx = request.Ctx; DiscordUser user = request.User; DiscordGuild guild = ctx.Guild; - DiscordMember? userAsMember = await ResolveGuildMember(guild, user.Id); - - if (userAsMember == null) - { - throw new Exception("Invalid user"); - } + DiscordMember? userAsMember = await ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); string avatarUrl = userAsMember.GuildAvatarUrl ?? userAsMember.AvatarUrl; @@ -93,24 +89,25 @@ public async Task Handle(GetAnimatedUserAvatar request, CancellationToken cancel throw new Exception("Couldn't load user avatar"); } - // TODO find a nicer filename - string imageName = user.Id.ToString(); - using var inputImage = await _imageService.DownloadImage(avatarUrl); + string extension = _imageService.GetImageFileExtension(inputImage); + + string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(extension); + var croppedImage = _imageService.CropImageToCircle(inputImage); - string imagePath = await _imageService.SaveImageToTempPath(croppedImage, imageName); + string imagePath = await _imageService.SaveImageToTempPath(croppedImage, imageFilename); byte[] animatedEmojiBytes = await _petPetClient.PetPet(imagePath, request.Speed); using var outputImage = ImageService.LoadImage(animatedEmojiBytes); - ImageStreamResult imageStreamResult = await _imageService.GetImageStream(outputImage); + var imageStreamResult = await _imageService.GetImageStream(outputImage); var responseBuilder = new DiscordMessageBuilder(); - responseBuilder.AddFile($"{imageName}.{imageStreamResult.FileExtension}", imageStreamResult.MemoryStream); + responseBuilder.AddFile($"{imageFilename}.{imageStreamResult.FileExtension}", imageStreamResult.MemoryStream); await ctx.RespondAsync(responseBuilder); } diff --git a/Kattbot/CommandModules/HelpModule.cs b/Kattbot/CommandModules/HelpModule.cs index 11a50bc..e1a0fa2 100644 --- a/Kattbot/CommandModules/HelpModule.cs +++ b/Kattbot/CommandModules/HelpModule.cs @@ -32,7 +32,6 @@ public Task GetHelp(CommandContext ctx) sb.AppendLine($"`{commandPrefix}stats me [?interval] [?page]`"); sb.AppendLine($"`{commandPrefix}stats best [?interval] [?page]`"); sb.AppendLine($"`{commandPrefix}stats [emote] [?interval]`"); - sb.AppendLine($"`\"?\" denotes an optional parameter`"); sb.AppendLine($"`{commandPrefix}help stats .. More information about stats command`"); sb.AppendLine(); @@ -46,7 +45,7 @@ public Task GetHelp(CommandContext ctx) sb.AppendLine("Image commands"); sb.AppendLine($"`{commandPrefix}deepfry [emote|user|image]`"); sb.AppendLine($"`{commandPrefix}oilpaint [emote|user|image]`"); - sb.AppendLine($"`{commandPrefix}pet [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}pet [emote|user|image] [?speed]`"); sb.AppendLine($"`{commandPrefix}dallify [emote|user|image]`"); sb.AppendLine($"`{commandPrefix}dalle [text]`"); @@ -54,6 +53,9 @@ public Task GetHelp(CommandContext ctx) sb.AppendLine("Other commands"); sb.AppendLine($"`{commandPrefix}meow`"); + sb.AppendLine(); + sb.AppendLine($"`\"?\" denotes an optional parameter`"); + sb.AppendLine(); sb.AppendLine("Kattbot source code: [github.com/selfdocumentingcode/Kattbot](https://github.com/selfdocumentingcode/Kattbot)"); diff --git a/Kattbot/CommandModules/ImageModule.cs b/Kattbot/CommandModules/ImageModule.cs index 09c2117..eda00c8 100644 --- a/Kattbot/CommandModules/ImageModule.cs +++ b/Kattbot/CommandModules/ImageModule.cs @@ -39,7 +39,7 @@ public Task OilPaintEmote(CommandContext ctx, DiscordEmoji emoji) [Cooldown(5, 10, CooldownBucketType.Global)] public Task PetEmote(CommandContext ctx, DiscordEmoji emoji, string? speed = null) { - var request = new GetAnimatedEmoji(ctx) + var request = new PetEmoteRequest(ctx) { Emoji = emoji, Speed = speed, @@ -52,7 +52,7 @@ public Task PetEmote(CommandContext ctx, DiscordEmoji emoji, string? speed = nul [Cooldown(5, 10, CooldownBucketType.Global)] public Task PetUser(CommandContext ctx, DiscordUser user, string? speed = null) { - var request = new GetAnimatedUserAvatar(ctx) + var request = new PetUserRequest(ctx) { User = user, Speed = speed, @@ -74,7 +74,16 @@ public Task Dalle(CommandContext ctx, [RemainingText] string prompt) [Cooldown(5, 60, CooldownBucketType.Global)] public Task Dallify(CommandContext ctx, DiscordEmoji emoji) { - var request = new DallifyImageCommand(ctx, emoji); + var request = new DallifyEmoteRequest(ctx, emoji); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + + [Command("dallify")] + [Cooldown(5, 60, CooldownBucketType.Global)] + public Task Dallify(CommandContext ctx, DiscordUser user) + { + var request = new DallifyUserRequest(ctx, user); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } diff --git a/Kattbot/Helpers/StringExtensions.cs b/Kattbot/Helpers/StringExtensions.cs index a980c91..ce06eb4 100644 --- a/Kattbot/Helpers/StringExtensions.cs +++ b/Kattbot/Helpers/StringExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; namespace Kattbot.Helpers; @@ -61,4 +62,15 @@ public static List SplitString(this string input, int chunkLength, strin return result; } + + public static string ToSafeFilename(this string input, string extension) + { + string safeFilename = new(Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(input)) + .Select(c => char.IsLetterOrDigit(c) ? c : '_') + .ToArray()); + + string filename = $"{safeFilename}.{extension}"; + + return filename; + } } diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index 031fdb6..657a05c 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -206,7 +206,16 @@ public async Task GetImageStream(Image image) return new ImageStreamResult(outputStream, extensionName); } - private IImageEncoder GetImageEncoderByFileType(string fileType) + public string GetImageFileExtension(Image image) + { + var format = image.Metadata.GetFormatOrDefault(); + + string extensionName = format.FileExtensions.First(); + + return extensionName; + } + + private static IImageEncoder GetImageEncoderByFileType(string fileType) { return fileType switch { From 460e4f63f48d50e35c198e4ecdf2b2b7a377aeaf Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 10 Sep 2023 19:59:27 +0200 Subject: [PATCH 05/16] Deepfry and oilpaint users --- .../CommandHandlers/Images/DallifyImage.cs | 36 +++----- Kattbot/CommandHandlers/Images/PetImage.cs | 25 ++--- .../CommandHandlers/Images/TransformImage.cs | 91 ++++++++++++++++--- Kattbot/CommandModules/ImageModule.cs | 20 +++- .../ResultFormatters/FormattedResultHelper.cs | 7 -- Kattbot/CommandModules/UtilsModule.cs | 17 +--- Kattbot/Helpers/DiscordResolver.cs | 58 ++++++++++++ Kattbot/Helpers/DiscordRoleResolver.cs | 34 ------- Kattbot/Program.cs | 1 + 9 files changed, 175 insertions(+), 114 deletions(-) create mode 100644 Kattbot/Helpers/DiscordResolver.cs delete mode 100644 Kattbot/Helpers/DiscordRoleResolver.cs diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index 1b5820e..4c907cd 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -37,13 +37,17 @@ public DallifyUserRequest(CommandContext ctx, DiscordUser user) public class DallifyImageHandler : IRequestHandler, IRequestHandler { + private const string Size = "256x256"; + private readonly DalleHttpClient _dalleHttpClient; private readonly ImageService _imageService; + private readonly DiscordResolver _discordResolver; - public DallifyImageHandler(DalleHttpClient dalleHttpClient, ImageService imageService) + public DallifyImageHandler(DalleHttpClient dalleHttpClient, ImageService imageService, DiscordResolver discordResolver) { _dalleHttpClient = dalleHttpClient; _imageService = imageService; + _discordResolver = discordResolver; } public async Task Handle(DallifyEmoteRequest request, CancellationToken cancellationToken) @@ -64,12 +68,10 @@ public async Task Handle(DallifyEmoteRequest request, CancellationToken cancella string fileName = $"{Guid.NewGuid()}.png"; - const string size = "256x256"; - var imageVariationRequest = new CreateImageVariationRequest { Image = squaredEmojiImage.MemoryStream.ToArray(), - Size = size, + Size = Size, User = request.Ctx.User.Id.ToString(), }; @@ -106,17 +108,14 @@ public async Task Handle(DallifyUserRequest request, CancellationToken cancellat DiscordMessage message = await request.Ctx.RespondAsync("Working on it"); - DiscordMember? userAsMember = await ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); - - string avatarUrl = userAsMember.GuildAvatarUrl ?? userAsMember.AvatarUrl; - - if (string.IsNullOrEmpty(avatarUrl)) - { - throw new Exception("Couldn't load user avatar"); - } - try { + DiscordMember? userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); + + string avatarUrl = userAsMember.GuildAvatarUrl + ?? userAsMember.AvatarUrl + ?? throw new Exception("Couldn't load user avatar"); + using var inputImage = await _imageService.DownloadImage(avatarUrl); var avatarAsPng = await ImageService.ConvertImageToPng(inputImage); @@ -125,12 +124,10 @@ public async Task Handle(DallifyUserRequest request, CancellationToken cancellat string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(avatarImageStream.FileExtension); - const string size = "256x256"; - var imageVariationRequest = new CreateImageVariationRequest { Image = avatarImageStream.MemoryStream.ToArray(), - Size = size, + Size = Size, User = request.Ctx.User.Id.ToString(), }; @@ -158,11 +155,4 @@ public async Task Handle(DallifyUserRequest request, CancellationToken cancellat throw; } } - - private Task ResolveGuildMember(DiscordGuild guild, ulong userId) - { - bool memberExists = guild.Members.TryGetValue(userId, out DiscordMember? member); - - return memberExists ? Task.FromResult(member) : guild.GetMemberAsync(userId); - } } diff --git a/Kattbot/CommandHandlers/Images/PetImage.cs b/Kattbot/CommandHandlers/Images/PetImage.cs index e4d2b2f..8c7b38f 100644 --- a/Kattbot/CommandHandlers/Images/PetImage.cs +++ b/Kattbot/CommandHandlers/Images/PetImage.cs @@ -6,7 +6,6 @@ using Kattbot.Helpers; using Kattbot.Services.Images; using MediatR; -using Microsoft.Extensions.Logging; namespace Kattbot.CommandHandlers.Images; @@ -40,13 +39,13 @@ public class PetImageHandlers : IRequestHandler, { private readonly ImageService _imageService; private readonly PetPetClient _petPetClient; - private readonly ILogger _logger; + private readonly DiscordResolver _discordResolver; - public PetImageHandlers(ImageService imageService, PetPetClient petPetClient, ILogger logger) + public PetImageHandlers(ImageService imageService, PetPetClient petPetClient, DiscordResolver discordResolver) { _imageService = imageService; _petPetClient = petPetClient; - _logger = logger; + _discordResolver = discordResolver; } public async Task Handle(PetEmoteRequest request, CancellationToken cancellationToken) @@ -80,14 +79,11 @@ public async Task Handle(PetUserRequest request, CancellationToken cancellationT DiscordUser user = request.User; DiscordGuild guild = ctx.Guild; - DiscordMember? userAsMember = await ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); + DiscordMember? userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); - string avatarUrl = userAsMember.GuildAvatarUrl ?? userAsMember.AvatarUrl; - - if (string.IsNullOrEmpty(avatarUrl)) - { - throw new Exception("Couldn't load user avatar"); - } + string avatarUrl = userAsMember.GuildAvatarUrl + ?? userAsMember.AvatarUrl + ?? throw new Exception("Couldn't load user avatar"); using var inputImage = await _imageService.DownloadImage(avatarUrl); @@ -111,11 +107,4 @@ public async Task Handle(PetUserRequest request, CancellationToken cancellationT await ctx.RespondAsync(responseBuilder); } - - private Task ResolveGuildMember(DiscordGuild guild, ulong userId) - { - bool memberExists = guild.Members.TryGetValue(userId, out DiscordMember? member); - - return memberExists ? Task.FromResult(member) : guild.GetMemberAsync(userId); - } } diff --git a/Kattbot/CommandHandlers/Images/TransformImage.cs b/Kattbot/CommandHandlers/Images/TransformImage.cs index 199c0b4..8e9a98d 100644 --- a/Kattbot/CommandHandlers/Images/TransformImage.cs +++ b/Kattbot/CommandHandlers/Images/TransformImage.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Net.Http; using System.Threading; using System.Threading.Tasks; using DSharpPlus.CommandsNext; @@ -11,12 +10,16 @@ namespace Kattbot.CommandHandlers.Images; -public class TransformImageRequest : CommandRequest +#pragma warning disable SA1402 // File may only contain a single type +public enum TransformImageEffect { - public static readonly string EffectDeepFry = "deepfry"; - public static readonly string EffectOilPaint = "oilpaint"; + DeepFry, + OilPaint, +} - public TransformImageRequest(CommandContext ctx, DiscordEmoji emoji, string effect) +public class TransformImageEmoteRequest : CommandRequest +{ + public TransformImageEmoteRequest(CommandContext ctx, DiscordEmoji emoji, TransformImageEffect effect) : base(ctx) { Emoji = emoji; @@ -25,24 +28,40 @@ public TransformImageRequest(CommandContext ctx, DiscordEmoji emoji, string effe public DiscordEmoji Emoji { get; set; } - public string Effect { get; set; } + public TransformImageEffect Effect { get; set; } } -#pragma warning disable SA1402 // File may only contain a single type -public class TransformImageHandler : IRequestHandler +public class TransformImageUserRequest : CommandRequest +{ + public TransformImageUserRequest(CommandContext ctx, DiscordUser user, TransformImageEffect effect) + : base(ctx) + { + User = user; + Effect = effect; + } + + public DiscordUser User { get; set; } + + public TransformImageEffect Effect { get; set; } +} + +public class TransformImageHandler : IRequestHandler, + IRequestHandler { private readonly ImageService _imageService; + private readonly DiscordResolver _discordResolver; - public TransformImageHandler(ImageService imageService) + public TransformImageHandler(ImageService imageService, DiscordResolver discordResolver) { _imageService = imageService; + _discordResolver = discordResolver; } - public async Task Handle(TransformImageRequest request, CancellationToken cancellationToken) + public async Task Handle(TransformImageEmoteRequest request, CancellationToken cancellationToken) { CommandContext ctx = request.Ctx; DiscordEmoji emoji = request.Emoji; - string effect = request.Effect; + var effect = request.Effect; string url = emoji.GetEmojiImageUrl(); @@ -50,11 +69,11 @@ public async Task Handle(TransformImageRequest request, CancellationToken cancel ImageStreamResult imageStreamResult; - if (effect == TransformImageRequest.EffectDeepFry) + if (effect == TransformImageEffect.DeepFry) { imageStreamResult = await _imageService.DeepFryImage(image); } - else if (effect == TransformImageRequest.EffectOilPaint) + else if (effect == TransformImageEffect.OilPaint) { imageStreamResult = await _imageService.OilPaintImage(image); } @@ -68,10 +87,54 @@ public async Task Handle(TransformImageRequest request, CancellationToken cancel var responseBuilder = new DiscordMessageBuilder(); - string fileName = effect; + string fileName = $"{Guid.NewGuid()}.png"; responseBuilder.AddFile($"{fileName}.{fileExtension}", imageStream); await ctx.RespondAsync(responseBuilder); } + + public async Task Handle(TransformImageUserRequest request, CancellationToken cancellationToken) + { + CommandContext ctx = request.Ctx; + DiscordUser user = request.User; + DiscordGuild guild = ctx.Guild; + var effect = request.Effect; + + DiscordMember? userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); + + string avatarUrl = userAsMember.GuildAvatarUrl + ?? userAsMember.AvatarUrl + ?? throw new Exception("Couldn't load user avatar"); + + using var inputImage = await _imageService.DownloadImage(avatarUrl); + + var croppedImage = _imageService.CropImageToCircle(inputImage); + + ImageStreamResult imageStreamResult; + + if (effect == TransformImageEffect.DeepFry) + { + imageStreamResult = await _imageService.DeepFryImage(croppedImage); + } + else if (effect == TransformImageEffect.OilPaint) + { + imageStreamResult = await _imageService.OilPaintImage(croppedImage); + } + else + { + throw new InvalidOperationException($"Unknown effect: {effect}"); + } + + MemoryStream imageStream = imageStreamResult.MemoryStream; + string fileExtension = imageStreamResult.FileExtension; + + var responseBuilder = new DiscordMessageBuilder(); + + string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); + + responseBuilder.AddFile(imageFilename, imageStream); + + await ctx.RespondAsync(responseBuilder); + } } diff --git a/Kattbot/CommandModules/ImageModule.cs b/Kattbot/CommandModules/ImageModule.cs index eda00c8..d9126b3 100644 --- a/Kattbot/CommandModules/ImageModule.cs +++ b/Kattbot/CommandModules/ImageModule.cs @@ -22,7 +22,15 @@ public ImageModule(CommandQueueChannel commandParallelQueue) [Cooldown(5, 10, CooldownBucketType.Global)] public Task DeepFryEmote(CommandContext ctx, DiscordEmoji emoji) { - var request = new TransformImageRequest(ctx, emoji, TransformImageRequest.EffectDeepFry); + var request = new TransformImageEmoteRequest(ctx, emoji, TransformImageEffect.DeepFry); + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + + [Command("deepfry")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task DeepFryEmote(CommandContext ctx, DiscordUser user) + { + var request = new TransformImageUserRequest(ctx, user, TransformImageEffect.DeepFry); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } @@ -30,11 +38,19 @@ public Task DeepFryEmote(CommandContext ctx, DiscordEmoji emoji) [Cooldown(5, 10, CooldownBucketType.Global)] public Task OilPaintEmote(CommandContext ctx, DiscordEmoji emoji) { - var request = new TransformImageRequest(ctx, emoji, TransformImageRequest.EffectOilPaint); + var request = new TransformImageEmoteRequest(ctx, emoji, TransformImageEffect.OilPaint); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } + [Command("oilpaint")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task OilPaintEmote(CommandContext ctx, DiscordUser user) + { + var request = new TransformImageUserRequest(ctx, user, TransformImageEffect.OilPaint); + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + [Command("pet")] [Cooldown(5, 10, CooldownBucketType.Global)] public Task PetEmote(CommandContext ctx, DiscordEmoji emoji, string? speed = null) diff --git a/Kattbot/CommandModules/ResultFormatters/FormattedResultHelper.cs b/Kattbot/CommandModules/ResultFormatters/FormattedResultHelper.cs index 0b76a88..537e0e7 100644 --- a/Kattbot/CommandModules/ResultFormatters/FormattedResultHelper.cs +++ b/Kattbot/CommandModules/ResultFormatters/FormattedResultHelper.cs @@ -96,11 +96,4 @@ public static string BuildBody(List allLines) return sb.ToString(); } - - public static string BuildMessage(string title, string message) - { - string result = $"{title}\r\n{message}"; - - return result; - } } diff --git a/Kattbot/CommandModules/UtilsModule.cs b/Kattbot/CommandModules/UtilsModule.cs index 3b45bc5..a022eea 100644 --- a/Kattbot/CommandModules/UtilsModule.cs +++ b/Kattbot/CommandModules/UtilsModule.cs @@ -13,13 +13,6 @@ namespace Kattbot.CommandModules; [Group("utils")] public class UtilsModule : BaseCommandModule { - private readonly DiscordErrorLogger _discordErrorLogger; - - public UtilsModule(DiscordErrorLogger discordErrorLogger) - { - _discordErrorLogger = discordErrorLogger; - } - [Command("emoji-code")] public Task GetEmojiCode(CommandContext ctx, DiscordEmoji emoji) { @@ -61,16 +54,8 @@ public Task GetEmojiCode(CommandContext ctx, DiscordEmoji emoji) [Command("role-id")] public Task GetRoleId(CommandContext ctx, string roleName) { - TryResolveResult result = DiscordRoleResolver.TryResolveByName(ctx.Guild, roleName, out DiscordRole? discordRole); + TryResolveResult result = DiscordResolver.TryResolveRoleByName(ctx.Guild, roleName, out DiscordRole? discordRole); return !result.Resolved ? ctx.RespondAsync(result.ErrorMessage) : ctx.RespondAsync($"Role {roleName} has id {discordRole.Id}"); } - - [Command("test-log-sync")] - public Task TestLogSync(CommandContext ctx) - { - _discordErrorLogger.LogError("Test error sync"); - - return Task.CompletedTask; - } } diff --git a/Kattbot/Helpers/DiscordResolver.cs b/Kattbot/Helpers/DiscordResolver.cs new file mode 100644 index 0000000..df56cfd --- /dev/null +++ b/Kattbot/Helpers/DiscordResolver.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus; +using DSharpPlus.Entities; +using Kattbot.Services; + +namespace Kattbot.Helpers; + +public class DiscordResolver +{ + private readonly DiscordErrorLogger _discordErrorLogger; + + public DiscordResolver(DiscordErrorLogger discordErrorLogger) + { + _discordErrorLogger = discordErrorLogger; + } + + public static TryResolveResult TryResolveRoleByName(DiscordGuild guild, string discordRoleName, out DiscordRole discordRole) + { + var matchingDiscordRoles = guild.Roles + .Where(kv => kv.Value.Name.Contains(discordRoleName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (matchingDiscordRoles.Count == 0) + { + discordRole = null!; + return new TryResolveResult(false, $"No role matches the name {discordRoleName}"); + } + else if (matchingDiscordRoles.Count > 1) + { + discordRole = null!; + return new TryResolveResult(false, $"More than 1 role matches the name {discordRoleName}"); + } + + discordRole = matchingDiscordRoles[0].Value; + + return new TryResolveResult(true); + } + + public async Task ResolveGuildMember(DiscordGuild guild, ulong userId) + { + var memberExists = guild.Members.TryGetValue(userId, out DiscordMember? member); + + if (memberExists) return member; + + try + { + return (await guild.GetMemberAsync(userId)) ?? throw new ArgumentException($"Missing member with id {userId}"); + } + catch (Exception) + { + _discordErrorLogger.LogError("Missing member"); + + return null; + } + } +} diff --git a/Kattbot/Helpers/DiscordRoleResolver.cs b/Kattbot/Helpers/DiscordRoleResolver.cs deleted file mode 100644 index 1e77cf1..0000000 --- a/Kattbot/Helpers/DiscordRoleResolver.cs +++ /dev/null @@ -1,34 +0,0 @@ -using DSharpPlus.Entities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Kattbot.Helpers -{ - public static class DiscordRoleResolver - { - public static TryResolveResult TryResolveByName(DiscordGuild guild, string discordRoleName, out DiscordRole discordRole) - { - var matchingDiscordRoles = guild.Roles - .Where(kv => kv.Value.Name.Contains(discordRoleName, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (matchingDiscordRoles.Count == 0) - { - discordRole = null!; - return new TryResolveResult(false, $"No role matches the name {discordRoleName}"); - } - else if (matchingDiscordRoles.Count > 1) - { - discordRole = null!; - return new TryResolveResult(false, $"More than 1 role matches the name {discordRoleName}"); - } - - discordRole = matchingDiscordRoles[0].Value; - - return new TryResolveResult(true); - } - } -} diff --git a/Kattbot/Program.cs b/Kattbot/Program.cs index 64bd4a5..9904480 100644 --- a/Kattbot/Program.cs +++ b/Kattbot/Program.cs @@ -123,6 +123,7 @@ private static void AddInternalServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } private static void AddRepositories(IServiceCollection services) From 14703b01222b93b96ff170db5e680738eb2dfd40 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Sun, 10 Sep 2023 20:55:02 +0200 Subject: [PATCH 06/16] Fix cropping transparency bug --- Kattbot.Tests/PetTests.cs | 11 +++++--- Kattbot/Services/Images/ImageService.cs | 35 ++++++++++++++++++++----- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/Kattbot.Tests/PetTests.cs b/Kattbot.Tests/PetTests.cs index de04a93..25fe290 100644 --- a/Kattbot.Tests/PetTests.cs +++ b/Kattbot.Tests/PetTests.cs @@ -32,11 +32,14 @@ public async Task PetPetTest() await image.SaveAsGifAsync(ouputFile); } - [TestMethod] - public async Task CropToCircle() + [DataTestMethod] + [DataRow("froge.png")] + [DataRow("test_working.png")] + [DataRow("test_not_working.png")] + public async Task CropToCircle(string inputFilename) { - string inputFile = Path.Combine(Path.GetTempPath(), "froge.png"); - string ouputFile = Path.Combine(Path.GetTempPath(), "froge_circle.png"); + string inputFile = Path.Combine(Path.GetTempPath(), inputFilename); + string ouputFile = Path.Combine(Path.GetTempPath(), $"cropped_{inputFilename}"); var imageService = new ImageService(null!); diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index 657a05c..79d9b93 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -110,15 +110,36 @@ public Image CropImageToCircle(Image image) { var ellipsePath = new EllipsePolygon(image.Width / 2, image.Height / 2, image.Width, image.Height); - var cloned = image.Clone(i => + Image imageAsPngWithTransparency; + + if (image.Metadata.DecodedImageFormat is not PngFormat || + image.Metadata.GetPngMetadata().ColorType is not PngColorType.RgbWithAlpha) { - i.SetGraphicsOptions(new GraphicsOptions() - { - Antialias = true, - AlphaCompositionMode = PixelAlphaCompositionMode.DestIn, - }); + using var stream = new MemoryStream(); + + image.SaveAsPngAsync(stream, new PngEncoder() { ColorType = PngColorType.RgbWithAlpha }); + + stream.Position = 0; + + imageAsPngWithTransparency = Image.Load(stream); + } + else + { + imageAsPngWithTransparency = image; + } - i.Fill(Color.Red, ellipsePath); + var cloned = imageAsPngWithTransparency.Clone(i => + { + var opts = new DrawingOptions() + { + GraphicsOptions = new GraphicsOptions() + { + Antialias = true, + AlphaCompositionMode = PixelAlphaCompositionMode.DestIn, + }, + }; + + i.Fill(opts, Color.Black, ellipsePath); }); return cloned; From f61d4c984302670d666030adb6d5bcba691f8451 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Mon, 11 Sep 2023 22:21:07 +0200 Subject: [PATCH 07/16] Oilpaint and Deepfry message images --- .../CommandHandlers/Images/TransformImage.cs | 143 +++++++++++++----- Kattbot/CommandModules/HelpModule.cs | 47 +++++- Kattbot/CommandModules/ImageModule.cs | 20 +++ Kattbot/Services/Images/PetPetClient.cs | 2 - 4 files changed, 164 insertions(+), 48 deletions(-) diff --git a/Kattbot/CommandHandlers/Images/TransformImage.cs b/Kattbot/CommandHandlers/Images/TransformImage.cs index 8e9a98d..174482f 100644 --- a/Kattbot/CommandHandlers/Images/TransformImage.cs +++ b/Kattbot/CommandHandlers/Images/TransformImage.cs @@ -1,5 +1,5 @@ using System; -using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using DSharpPlus.CommandsNext; @@ -7,6 +7,7 @@ using Kattbot.Helpers; using Kattbot.Services.Images; using MediatR; +using SixLabors.ImageSharp; namespace Kattbot.CommandHandlers.Images; @@ -45,8 +46,20 @@ public TransformImageUserRequest(CommandContext ctx, DiscordUser user, Transform public TransformImageEffect Effect { get; set; } } +public class TransformImageMessageRequest : CommandRequest +{ + public TransformImageMessageRequest(CommandContext ctx, TransformImageEffect effect) + : base(ctx) + { + Effect = effect; + } + + public TransformImageEffect Effect { get; set; } +} + public class TransformImageHandler : IRequestHandler, - IRequestHandler + IRequestHandler, + IRequestHandler { private readonly ImageService _imageService; private readonly DiscordResolver _discordResolver; @@ -59,30 +72,15 @@ public TransformImageHandler(ImageService imageService, DiscordResolver discordR public async Task Handle(TransformImageEmoteRequest request, CancellationToken cancellationToken) { - CommandContext ctx = request.Ctx; - DiscordEmoji emoji = request.Emoji; + var ctx = request.Ctx; + var emoji = request.Emoji; var effect = request.Effect; - string url = emoji.GetEmojiImageUrl(); + string imageUrl = emoji.GetEmojiImageUrl(); - using var image = await _imageService.DownloadImage(url); + var imageStreamResult = await TransformImage(imageUrl, effect); - ImageStreamResult imageStreamResult; - - if (effect == TransformImageEffect.DeepFry) - { - imageStreamResult = await _imageService.DeepFryImage(image); - } - else if (effect == TransformImageEffect.OilPaint) - { - imageStreamResult = await _imageService.OilPaintImage(image); - } - else - { - throw new InvalidOperationException($"Unknown effect: {effect}"); - } - - MemoryStream imageStream = imageStreamResult.MemoryStream; + using var imageStream = imageStreamResult.MemoryStream; string fileExtension = imageStreamResult.FileExtension; var responseBuilder = new DiscordMessageBuilder(); @@ -96,45 +94,112 @@ public async Task Handle(TransformImageEmoteRequest request, CancellationToken c public async Task Handle(TransformImageUserRequest request, CancellationToken cancellationToken) { - CommandContext ctx = request.Ctx; - DiscordUser user = request.User; - DiscordGuild guild = ctx.Guild; + var ctx = request.Ctx; + var user = request.User; + var guild = ctx.Guild; var effect = request.Effect; - DiscordMember? userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); + var userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) + ?? throw new Exception("Invalid user"); - string avatarUrl = userAsMember.GuildAvatarUrl - ?? userAsMember.AvatarUrl - ?? throw new Exception("Couldn't load user avatar"); + string imageUrl = userAsMember.GuildAvatarUrl + ?? userAsMember.AvatarUrl + ?? throw new Exception("Couldn't load user avatar"); - using var inputImage = await _imageService.DownloadImage(avatarUrl); + var imageStreamResult = await TransformImage(imageUrl, effect, _imageService.CropImageToCircle); - var croppedImage = _imageService.CropImageToCircle(inputImage); + using var imageStream = imageStreamResult.MemoryStream; + string fileExtension = imageStreamResult.FileExtension; + + var responseBuilder = new DiscordMessageBuilder(); + + string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); + + responseBuilder.AddFile(imageFilename, imageStream); + + await ctx.RespondAsync(responseBuilder); + } + + public async Task Handle(TransformImageMessageRequest request, CancellationToken cancellationToken) + { + var ctx = request.Ctx; + var message = ctx.Message; + var effect = request.Effect; + + var imageUrl = GetImageUrlFromMessage(message); + + if (imageUrl == null) + { + await ctx.RespondAsync("I didn't find any images."); + return; + } + + var imageStreamResult = await TransformImage(imageUrl, effect); + + using var imageStream = imageStreamResult.MemoryStream; + string fileExtension = imageStreamResult.FileExtension; + + var responseBuilder = new DiscordMessageBuilder(); + + string fileName = $"{Guid.NewGuid()}.png"; + + responseBuilder.AddFile($"{fileName}.{fileExtension}", imageStream); + + await ctx.RespondAsync(responseBuilder); + } + + private async Task TransformImage(string imageUrl, TransformImageEffect effect, Func? preTransform = null) + { + var inputImage = await _imageService.DownloadImage(imageUrl); + + if (preTransform != null) + { + inputImage = preTransform(inputImage); + } ImageStreamResult imageStreamResult; if (effect == TransformImageEffect.DeepFry) { - imageStreamResult = await _imageService.DeepFryImage(croppedImage); + imageStreamResult = await _imageService.DeepFryImage(inputImage); } else if (effect == TransformImageEffect.OilPaint) { - imageStreamResult = await _imageService.OilPaintImage(croppedImage); + imageStreamResult = await _imageService.OilPaintImage(inputImage); } else { throw new InvalidOperationException($"Unknown effect: {effect}"); } - MemoryStream imageStream = imageStreamResult.MemoryStream; - string fileExtension = imageStreamResult.FileExtension; + return imageStreamResult; + } - var responseBuilder = new DiscordMessageBuilder(); + private string? GetImageUrlFromMessage(DiscordMessage message, bool isRootMessage = true) + { + if (message.Attachments.Count > 0) + { + var imgAttachment = message.Attachments.Where(a => a.MediaType.StartsWith("image")).FirstOrDefault(); - string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); + if (imgAttachment != null) + { + return imgAttachment.Url; + } + } + else if (message.Embeds.Count > 0) + { + var imgEmbed = message.Embeds.Where(e => e.Type == "image").FirstOrDefault(); - responseBuilder.AddFile(imageFilename, imageStream); + if (imgEmbed != null) + { + return imgEmbed.Url.AbsoluteUri; + } + } + else if (isRootMessage == true && message.ReferencedMessage != null) + { + return GetImageUrlFromMessage(message.ReferencedMessage, false); + } - await ctx.RespondAsync(responseBuilder); + return null; } } diff --git a/Kattbot/CommandModules/HelpModule.cs b/Kattbot/CommandModules/HelpModule.cs index e1a0fa2..c6fdace 100644 --- a/Kattbot/CommandModules/HelpModule.cs +++ b/Kattbot/CommandModules/HelpModule.cs @@ -48,6 +48,7 @@ public Task GetHelp(CommandContext ctx) sb.AppendLine($"`{commandPrefix}pet [emote|user|image] [?speed]`"); sb.AppendLine($"`{commandPrefix}dallify [emote|user|image]`"); sb.AppendLine($"`{commandPrefix}dalle [text]`"); + sb.AppendLine($"`{commandPrefix}help images .. More information about image commands`"); sb.AppendLine(); sb.AppendLine("Other commands"); @@ -66,7 +67,7 @@ public Task GetHelp(CommandContext ctx) [Command("stats")] [Description("Help about stats")] - public Task GetHelpStats(CommandContext ctx) + public Task GetStatsHelp(CommandContext ctx) { var sb = new StringBuilder(); @@ -74,17 +75,19 @@ public Task GetHelpStats(CommandContext ctx) sb.AppendLine(); sb.AppendLine($"Command arguments:"); - sb.AppendLine($"`username .. Discord username with # identifier or @mention`"); + sb.AppendLine($"`user .. Discord username with # identifier or @mention`"); sb.AppendLine($"`emote .. Discord emote (server emotes only)`"); - sb.AppendLine($"`-p, --page .. Displays a different page of the result set (default 1st page)`"); - sb.AppendLine($"`-i, --interval .. Limits result set to given interval (default 2 months)`"); - sb.AppendLine($"` Valid interval units: \"m\", \"w\", \"d\"`"); - sb.AppendLine($"` Optionally use interval value \"lifetime\"`"); + sb.AppendLine($"`-p, --page .. Displays a different page of the result set`"); + sb.AppendLine($"` (default 1st page)`"); + sb.AppendLine($"`-i, --interval .. Limits result set to given interval`"); + sb.AppendLine($"` (default 2 months)`"); + sb.AppendLine($"` Valid interval units: \"m\", \"w\", \"d\"`"); + sb.AppendLine($"` Optionally use interval value \"lifetime\"`"); sb.AppendLine(); sb.AppendLine($"Usage examples:"); sb.AppendLine($"`{commandPrefix}stats best`"); sb.AppendLine($"`{commandPrefix}stats worst --page 2`"); - sb.AppendLine($"`{commandPrefix}stats User#1234 --interval 3m`"); + sb.AppendLine($"`{commandPrefix}stats @someUser --interval 3m`"); sb.AppendLine($"`{commandPrefix}stats me -p 2 -i 2w`"); sb.AppendLine($"`{commandPrefix}stats :a_server_emote:`"); @@ -92,4 +95,34 @@ public Task GetHelpStats(CommandContext ctx) return ctx.RespondAsync(eb); } + + [Command("images")] + [Description("Help about images")] + public Task GetImagesHelp(CommandContext ctx) + { + var sb = new StringBuilder(); + + string commandPrefix = _options.CommandPrefix; + + sb.AppendLine(); + sb.AppendLine($"Command arguments:"); + sb.AppendLine($"`user .. Discord username with # identifier or @mention`"); + sb.AppendLine($"`emote .. Discord emote (server emotes only)`"); + sb.AppendLine($"`image .. Attached or linked image in current message.`"); + sb.AppendLine($"` If message contains no images,`"); + sb.AppendLine($"` reply-to message is checked.`"); + sb.AppendLine($"`speed .. Petting speed`"); + sb.AppendLine($"` Valid speeds: \"slow\", \"normal\",`"); + sb.AppendLine($"` \"fast\", \"lightspeed\"`"); + + sb.AppendLine(); + sb.AppendLine($"Usage examples:"); + sb.AppendLine($"`{commandPrefix}deepfry :a_server_emote:`"); + sb.AppendLine($"`{commandPrefix}pet @someUser fast`"); + sb.AppendLine($"`{commandPrefix}dallify `"); + + var eb = EmbedBuilderHelper.BuildSimpleEmbed("Shows server-wide emote stats-or for a specific user", sb.ToString()); + + return ctx.RespondAsync(eb); + } } diff --git a/Kattbot/CommandModules/ImageModule.cs b/Kattbot/CommandModules/ImageModule.cs index d9126b3..ce64dc4 100644 --- a/Kattbot/CommandModules/ImageModule.cs +++ b/Kattbot/CommandModules/ImageModule.cs @@ -34,6 +34,16 @@ public Task DeepFryEmote(CommandContext ctx, DiscordUser user) return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } + [Command("deepfry")] + [Cooldown(5, 10, CooldownBucketType.Global)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + public Task DeepFryEmote(CommandContext ctx, string _ = "") +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + var request = new TransformImageMessageRequest(ctx, TransformImageEffect.DeepFry); + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + [Command("oilpaint")] [Cooldown(5, 10, CooldownBucketType.Global)] public Task OilPaintEmote(CommandContext ctx, DiscordEmoji emoji) @@ -51,6 +61,16 @@ public Task OilPaintEmote(CommandContext ctx, DiscordUser user) return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } + [Command("oilpaint")] + [Cooldown(5, 10, CooldownBucketType.Global)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + public Task OilPaintEmote(CommandContext ctx, string _ = "") +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + var request = new TransformImageMessageRequest(ctx, TransformImageEffect.OilPaint); + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + [Command("pet")] [Cooldown(5, 10, CooldownBucketType.Global)] public Task PetEmote(CommandContext ctx, DiscordEmoji emoji, string? speed = null) diff --git a/Kattbot/Services/Images/PetPetClient.cs b/Kattbot/Services/Images/PetPetClient.cs index 69bc754..2ab6c3f 100644 --- a/Kattbot/Services/Images/PetPetClient.cs +++ b/Kattbot/Services/Images/PetPetClient.cs @@ -77,7 +77,6 @@ private static string ParseSpeed(string? speed = null) const int speedSlow = 8; const int speedNormal = 16; const int speedFast = 32; - const int speedFaster = 48; const int speedLightspeed = 60; return ((speed ?? string.Empty).ToLower() switch @@ -85,7 +84,6 @@ private static string ParseSpeed(string? speed = null) "slow" => speedSlow, "normal" => speedNormal, "fast" => speedFast, - "faster" => speedFaster, "lightspeed" => speedLightspeed, _ => speedNormal, }).ToString(); From 0b36e72a1195e0a10cb2e18d9f1560ff8cf2af27 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Mon, 11 Sep 2023 22:40:31 +0200 Subject: [PATCH 08/16] Fix issue with user prop on dalle request --- Kattbot/Services/Dalle/DalleModels.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Kattbot/Services/Dalle/DalleModels.cs b/Kattbot/Services/Dalle/DalleModels.cs index d262697..1333a30 100644 --- a/Kattbot/Services/Dalle/DalleModels.cs +++ b/Kattbot/Services/Dalle/DalleModels.cs @@ -41,6 +41,7 @@ public record CreateImageRequest /// Gets or sets a unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. /// https://platform.openai.com/docs/api-reference/chat/create#chat/create-user. /// + [JsonPropertyName("user")] public string? User { get; set; } } @@ -81,6 +82,7 @@ public record CreateImageVariationRequest /// Gets or sets a unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. /// https://platform.openai.com/docs/api-reference/images/createVariation#user. /// + [JsonPropertyName("user")] public string? User { get; set; } } From b4fcce91b949d0995840c740f7a19b2a1d324b50 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Tue, 12 Sep 2023 21:39:15 +0200 Subject: [PATCH 09/16] Pet images --- Kattbot.Tests/PetTests.cs | 12 +- Kattbot/CommandHandlers/Images/PetImage.cs | 117 +++++++++++++----- .../CommandHandlers/Images/TransformImage.cs | 44 ++----- Kattbot/CommandModules/HelpModule.cs | 4 +- Kattbot/CommandModules/ImageModule.cs | 37 +++--- Kattbot/Helpers/DiscordExtensions.cs | 29 +++++ 6 files changed, 152 insertions(+), 91 deletions(-) diff --git a/Kattbot.Tests/PetTests.cs b/Kattbot.Tests/PetTests.cs index 25fe290..4d98df9 100644 --- a/Kattbot.Tests/PetTests.cs +++ b/Kattbot.Tests/PetTests.cs @@ -14,7 +14,13 @@ namespace Kattbot.Tests; public class PetTests { [TestMethod] - public async Task PetPetTest() + [DataRow("SamplePNGImage_100kbmb.png")] + [DataRow("SamplePNGImage_500kbmb.png")] + [DataRow("SamplePNGImage_1mbmb.png")] + [DataRow("SamplePNGImage_3mbmb.png")] + [DataRow("SamplePNGImage_10mbmb.png")] + [DataRow("SamplePNGImage_30mbmb.png")] + public async Task PetPetTest(string inputImage) { var puppeteerFactory = new PuppeteerFactory(); @@ -22,8 +28,8 @@ public async Task PetPetTest() var makeEmojiClient = new PetPetClient(puppeteerFactory, logger); - string inputFile = Path.Combine(Path.GetTempPath(), "froge.png"); - string ouputFile = Path.Combine(Path.GetTempPath(), "pet_froge.gif"); + string inputFile = Path.Combine(Path.GetTempPath(), "test_images", inputImage); + string ouputFile = Path.Combine(Path.GetTempPath(), "pet-test-output", $"pet_{inputImage.Split(".")[0]}.gif"); byte[] resultBytes = await makeEmojiClient.PetPet(inputFile); diff --git a/Kattbot/CommandHandlers/Images/PetImage.cs b/Kattbot/CommandHandlers/Images/PetImage.cs index 8c7b38f..fdb13a2 100644 --- a/Kattbot/CommandHandlers/Images/PetImage.cs +++ b/Kattbot/CommandHandlers/Images/PetImage.cs @@ -6,36 +6,53 @@ using Kattbot.Helpers; using Kattbot.Services.Images; using MediatR; +using SixLabors.ImageSharp; namespace Kattbot.CommandHandlers.Images; #pragma warning disable SA1402 // File may only contain a single type public class PetEmoteRequest : CommandRequest { - public PetEmoteRequest(CommandContext ctx) + public PetEmoteRequest(CommandContext ctx, DiscordEmoji emoji, string? speed) : base(ctx) { + Emoji = emoji; + Speed = speed; } - public DiscordEmoji Emoji { get; set; } = null!; + public DiscordEmoji Emoji { get; set; } - public string? Speed { get; internal set; } + public string? Speed { get; set; } } public class PetUserRequest : CommandRequest { - public PetUserRequest(CommandContext ctx) + public PetUserRequest(CommandContext ctx, DiscordUser user, string? speed) : base(ctx) { + User = user; + Speed = speed; } - public DiscordUser User { get; set; } = null!; + public DiscordUser User { get; set; } - public string? Speed { get; internal set; } + public string? Speed { get; set; } +} + +public class PetImageRequest : CommandRequest +{ + public PetImageRequest(CommandContext ctx, string? speed) + : base(ctx) + { + Speed = speed; + } + + public string? Speed { get; set; } } public class PetImageHandlers : IRequestHandler, - IRequestHandler + IRequestHandler, + IRequestHandler { private readonly ImageService _imageService; private readonly PetPetClient _petPetClient; @@ -50,21 +67,17 @@ public PetImageHandlers(ImageService imageService, PetPetClient petPetClient, Di public async Task Handle(PetEmoteRequest request, CancellationToken cancellationToken) { - CommandContext ctx = request.Ctx; - DiscordEmoji emoji = request.Emoji; - - string url = emoji.GetEmojiImageUrl(); - string imageName = emoji.Id != 0 ? emoji.Id.ToString() : emoji.Name; + var ctx = request.Ctx; + var emoji = request.Emoji; - using var image = await _imageService.DownloadImage(url); + var imageUrl = emoji.GetEmojiImageUrl(); - string imagePath = await _imageService.SaveImageToTempPath(image, imageName); + var imageStreamResult = await PetImage(imageUrl, request.Speed); - byte[] animatedEmojiBytes = await _petPetClient.PetPet(imagePath, request.Speed); + using var imageStream = imageStreamResult.MemoryStream; + var fileExtension = imageStreamResult.FileExtension; - using var outputImage = ImageService.LoadImage(animatedEmojiBytes); - - ImageStreamResult imageStreamResult = await _imageService.GetImageStream(outputImage); + var imageName = emoji.Id != 0 ? emoji.Id.ToString() : emoji.Name; var responseBuilder = new DiscordMessageBuilder(); @@ -75,36 +88,76 @@ public async Task Handle(PetEmoteRequest request, CancellationToken cancellation public async Task Handle(PetUserRequest request, CancellationToken cancellationToken) { - CommandContext ctx = request.Ctx; - DiscordUser user = request.User; - DiscordGuild guild = ctx.Guild; + var ctx = request.Ctx; + var user = request.User; + var guild = ctx.Guild; - DiscordMember? userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); + var userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); - string avatarUrl = userAsMember.GuildAvatarUrl + var imageUrl = userAsMember.GuildAvatarUrl ?? userAsMember.AvatarUrl ?? throw new Exception("Couldn't load user avatar"); - using var inputImage = await _imageService.DownloadImage(avatarUrl); + var imageStreamResult = await PetImage(imageUrl, request.Speed, _imageService.CropImageToCircle); - string extension = _imageService.GetImageFileExtension(inputImage); + using var imageStream = imageStreamResult.MemoryStream; + var fileExtension = imageStreamResult.FileExtension; - string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(extension); + var imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); - var croppedImage = _imageService.CropImageToCircle(inputImage); + var responseBuilder = new DiscordMessageBuilder(); - string imagePath = await _imageService.SaveImageToTempPath(croppedImage, imageFilename); + responseBuilder.AddFile(imageFilename, imageStreamResult.MemoryStream); - byte[] animatedEmojiBytes = await _petPetClient.PetPet(imagePath, request.Speed); + await ctx.RespondAsync(responseBuilder); + } + + public async Task Handle(PetImageRequest request, CancellationToken cancellationToken) + { + var ctx = request.Ctx; + var message = ctx.Message; - using var outputImage = ImageService.LoadImage(animatedEmojiBytes); + var imageUrl = message.GetImageUrlFromMessage(); - var imageStreamResult = await _imageService.GetImageStream(outputImage); + if (imageUrl == null) + { + await ctx.RespondAsync("I didn't find any images."); + return; + } + + var imageStreamResult = await PetImage(imageUrl, request.Speed); + + using var imageStream = imageStreamResult.MemoryStream; + var fileExtension = imageStreamResult.FileExtension; + + string imageFilename = $"{Guid.NewGuid()}.{fileExtension}"; var responseBuilder = new DiscordMessageBuilder(); - responseBuilder.AddFile($"{imageFilename}.{imageStreamResult.FileExtension}", imageStreamResult.MemoryStream); + responseBuilder.AddFile(imageFilename, imageStreamResult.MemoryStream); await ctx.RespondAsync(responseBuilder); } + + private async Task PetImage(string imageUrl, string? speed, Func? preTransform = null) + { + var inputImage = await _imageService.DownloadImage(imageUrl); + + if (preTransform != null) + { + inputImage = preTransform(inputImage); + } + + string extension = _imageService.GetImageFileExtension(inputImage); + + string imagePath = await _imageService.SaveImageToTempPath(inputImage, $"{Guid.NewGuid()}.{extension}"); + + byte[] animatedEmojiBytes = await _petPetClient.PetPet(imagePath, speed); + + var outputImage = ImageService.LoadImage(animatedEmojiBytes); + + var ouputImageStream = await _imageService.GetImageStream(outputImage); + + return ouputImageStream; + } } diff --git a/Kattbot/CommandHandlers/Images/TransformImage.cs b/Kattbot/CommandHandlers/Images/TransformImage.cs index 174482f..fe87572 100644 --- a/Kattbot/CommandHandlers/Images/TransformImage.cs +++ b/Kattbot/CommandHandlers/Images/TransformImage.cs @@ -83,10 +83,10 @@ public async Task Handle(TransformImageEmoteRequest request, CancellationToken c using var imageStream = imageStreamResult.MemoryStream; string fileExtension = imageStreamResult.FileExtension; - var responseBuilder = new DiscordMessageBuilder(); - string fileName = $"{Guid.NewGuid()}.png"; + var responseBuilder = new DiscordMessageBuilder(); + responseBuilder.AddFile($"{fileName}.{fileExtension}", imageStream); await ctx.RespondAsync(responseBuilder); @@ -111,10 +111,10 @@ public async Task Handle(TransformImageUserRequest request, CancellationToken ca using var imageStream = imageStreamResult.MemoryStream; string fileExtension = imageStreamResult.FileExtension; - var responseBuilder = new DiscordMessageBuilder(); - string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); + var responseBuilder = new DiscordMessageBuilder(); + responseBuilder.AddFile(imageFilename, imageStream); await ctx.RespondAsync(responseBuilder); @@ -126,7 +126,7 @@ public async Task Handle(TransformImageMessageRequest request, CancellationToken var message = ctx.Message; var effect = request.Effect; - var imageUrl = GetImageUrlFromMessage(message); + var imageUrl = message.GetImageUrlFromMessage(); if (imageUrl == null) { @@ -139,11 +139,11 @@ public async Task Handle(TransformImageMessageRequest request, CancellationToken using var imageStream = imageStreamResult.MemoryStream; string fileExtension = imageStreamResult.FileExtension; - var responseBuilder = new DiscordMessageBuilder(); + string imageFilename = $"{Guid.NewGuid()}.png"; - string fileName = $"{Guid.NewGuid()}.png"; + var responseBuilder = new DiscordMessageBuilder(); - responseBuilder.AddFile($"{fileName}.{fileExtension}", imageStream); + responseBuilder.AddFile($"{imageFilename}.{fileExtension}", imageStream); await ctx.RespondAsync(responseBuilder); } @@ -174,32 +174,4 @@ private async Task TransformImage(string imageUrl, TransformI return imageStreamResult; } - - private string? GetImageUrlFromMessage(DiscordMessage message, bool isRootMessage = true) - { - if (message.Attachments.Count > 0) - { - var imgAttachment = message.Attachments.Where(a => a.MediaType.StartsWith("image")).FirstOrDefault(); - - if (imgAttachment != null) - { - return imgAttachment.Url; - } - } - else if (message.Embeds.Count > 0) - { - var imgEmbed = message.Embeds.Where(e => e.Type == "image").FirstOrDefault(); - - if (imgEmbed != null) - { - return imgEmbed.Url.AbsoluteUri; - } - } - else if (isRootMessage == true && message.ReferencedMessage != null) - { - return GetImageUrlFromMessage(message.ReferencedMessage, false); - } - - return null; - } } diff --git a/Kattbot/CommandModules/HelpModule.cs b/Kattbot/CommandModules/HelpModule.cs index c6fdace..e75376a 100644 --- a/Kattbot/CommandModules/HelpModule.cs +++ b/Kattbot/CommandModules/HelpModule.cs @@ -91,7 +91,7 @@ public Task GetStatsHelp(CommandContext ctx) sb.AppendLine($"`{commandPrefix}stats me -p 2 -i 2w`"); sb.AppendLine($"`{commandPrefix}stats :a_server_emote:`"); - var eb = EmbedBuilderHelper.BuildSimpleEmbed("Shows server-wide emote stats-or for a specific user", sb.ToString()); + var eb = EmbedBuilderHelper.BuildSimpleEmbed("Check out random emote related stats", sb.ToString()); return ctx.RespondAsync(eb); } @@ -121,7 +121,7 @@ public Task GetImagesHelp(CommandContext ctx) sb.AppendLine($"`{commandPrefix}pet @someUser fast`"); sb.AppendLine($"`{commandPrefix}dallify `"); - var eb = EmbedBuilderHelper.BuildSimpleEmbed("Shows server-wide emote stats-or for a specific user", sb.ToString()); + var eb = EmbedBuilderHelper.BuildSimpleEmbed("Stuff you can do with images", sb.ToString()); return ctx.RespondAsync(eb); } diff --git a/Kattbot/CommandModules/ImageModule.cs b/Kattbot/CommandModules/ImageModule.cs index ce64dc4..514c6fb 100644 --- a/Kattbot/CommandModules/ImageModule.cs +++ b/Kattbot/CommandModules/ImageModule.cs @@ -20,7 +20,7 @@ public ImageModule(CommandQueueChannel commandParallelQueue) [Command("deepfry")] [Cooldown(5, 10, CooldownBucketType.Global)] - public Task DeepFryEmote(CommandContext ctx, DiscordEmoji emoji) + public Task DeepFry(CommandContext ctx, DiscordEmoji emoji) { var request = new TransformImageEmoteRequest(ctx, emoji, TransformImageEffect.DeepFry); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); @@ -28,7 +28,7 @@ public Task DeepFryEmote(CommandContext ctx, DiscordEmoji emoji) [Command("deepfry")] [Cooldown(5, 10, CooldownBucketType.Global)] - public Task DeepFryEmote(CommandContext ctx, DiscordUser user) + public Task DeepFry(CommandContext ctx, DiscordUser user) { var request = new TransformImageUserRequest(ctx, user, TransformImageEffect.DeepFry); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); @@ -37,7 +37,7 @@ public Task DeepFryEmote(CommandContext ctx, DiscordUser user) [Command("deepfry")] [Cooldown(5, 10, CooldownBucketType.Global)] #pragma warning disable SA1313 // Parameter names should begin with lower-case letter - public Task DeepFryEmote(CommandContext ctx, string _ = "") + public Task DeepFry(CommandContext ctx, string _ = "") #pragma warning restore SA1313 // Parameter names should begin with lower-case letter { var request = new TransformImageMessageRequest(ctx, TransformImageEffect.DeepFry); @@ -46,7 +46,7 @@ public Task DeepFryEmote(CommandContext ctx, string _ = "") [Command("oilpaint")] [Cooldown(5, 10, CooldownBucketType.Global)] - public Task OilPaintEmote(CommandContext ctx, DiscordEmoji emoji) + public Task OilPaint(CommandContext ctx, DiscordEmoji emoji) { var request = new TransformImageEmoteRequest(ctx, emoji, TransformImageEffect.OilPaint); @@ -55,7 +55,7 @@ public Task OilPaintEmote(CommandContext ctx, DiscordEmoji emoji) [Command("oilpaint")] [Cooldown(5, 10, CooldownBucketType.Global)] - public Task OilPaintEmote(CommandContext ctx, DiscordUser user) + public Task OilPaint(CommandContext ctx, DiscordUser user) { var request = new TransformImageUserRequest(ctx, user, TransformImageEffect.OilPaint); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); @@ -64,7 +64,7 @@ public Task OilPaintEmote(CommandContext ctx, DiscordUser user) [Command("oilpaint")] [Cooldown(5, 10, CooldownBucketType.Global)] #pragma warning disable SA1313 // Parameter names should begin with lower-case letter - public Task OilPaintEmote(CommandContext ctx, string _ = "") + public Task OilPaint(CommandContext ctx, string _ = "") #pragma warning restore SA1313 // Parameter names should begin with lower-case letter { var request = new TransformImageMessageRequest(ctx, TransformImageEffect.OilPaint); @@ -73,26 +73,27 @@ public Task OilPaintEmote(CommandContext ctx, string _ = "") [Command("pet")] [Cooldown(5, 10, CooldownBucketType.Global)] - public Task PetEmote(CommandContext ctx, DiscordEmoji emoji, string? speed = null) + public Task Pet(CommandContext ctx, DiscordEmoji emoji, string? speed = null) { - var request = new PetEmoteRequest(ctx) - { - Emoji = emoji, - Speed = speed, - }; + var request = new PetEmoteRequest(ctx, emoji, speed); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } [Command("pet")] [Cooldown(5, 10, CooldownBucketType.Global)] - public Task PetUser(CommandContext ctx, DiscordUser user, string? speed = null) + public Task Pet(CommandContext ctx, DiscordUser user, string? speed = null) { - var request = new PetUserRequest(ctx) - { - User = user, - Speed = speed, - }; + var request = new PetUserRequest(ctx, user, speed); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + + [Command("pet")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task Pet(CommandContext ctx, string? speed = null) + { + var request = new PetImageRequest(ctx, speed); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } diff --git a/Kattbot/Helpers/DiscordExtensions.cs b/Kattbot/Helpers/DiscordExtensions.cs index f18ee00..419073e 100644 --- a/Kattbot/Helpers/DiscordExtensions.cs +++ b/Kattbot/Helpers/DiscordExtensions.cs @@ -1,4 +1,5 @@ using DSharpPlus.Entities; +using System.Linq; namespace Kattbot.Helpers; @@ -24,4 +25,32 @@ public static string GetEmojiImageUrl(this DiscordEmoji emoji) return isEmote ? emoji.Url : EmoteHelper.GetExternalEmojiImageUrl(emoji.Name); } + + public static string? GetImageUrlFromMessage(this DiscordMessage message, bool isRootMessage = true) + { + if (message.Attachments.Count > 0) + { + var imgAttachment = message.Attachments.Where(a => a.MediaType.StartsWith("image")).FirstOrDefault(); + + if (imgAttachment != null) + { + return imgAttachment.Url; + } + } + else if (message.Embeds.Count > 0) + { + var imgEmbed = message.Embeds.Where(e => e.Type == "image").FirstOrDefault(); + + if (imgEmbed != null) + { + return imgEmbed.Url.AbsoluteUri; + } + } + else if (isRootMessage == true && message.ReferencedMessage != null) + { + return GetImageUrlFromMessage(message.ReferencedMessage, false); + } + + return null; + } } From 6191ad0b6532a46be563e4cf40825c1169dac73c Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Tue, 12 Sep 2023 22:46:24 +0200 Subject: [PATCH 10/16] Dallify images --- .../CommandHandlers/Images/DallifyImage.cs | 161 ++++++++++++------ Kattbot/CommandHandlers/Images/PetImage.cs | 6 +- Kattbot/CommandModules/ImageModule.cs | 17 +- Kattbot/Services/Images/ImageService.cs | 37 +++- 4 files changed, 158 insertions(+), 63 deletions(-) diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index 4c907cd..6e2afbe 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -34,11 +34,22 @@ public DallifyUserRequest(CommandContext ctx, DiscordUser user) public DiscordUser User { get; set; } } +public class DallifyImageRequest : CommandRequest +{ + public DallifyImageRequest(CommandContext ctx) + : base(ctx) + { } +} + public class DallifyImageHandler : IRequestHandler, - IRequestHandler + IRequestHandler, + IRequestHandler { - private const string Size = "256x256"; + public static readonly string Size256 = "256x256"; + public static readonly string Size512 = "512x512"; + public static readonly string Size1024 = "1024x1024"; + private const int MaxImageSizeInMb = 4; private readonly DalleHttpClient _dalleHttpClient; private readonly ImageService _imageService; private readonly DiscordResolver _discordResolver; @@ -52,41 +63,28 @@ public DallifyImageHandler(DalleHttpClient dalleHttpClient, ImageService imageSe public async Task Handle(DallifyEmoteRequest request, CancellationToken cancellationToken) { - DiscordEmoji emoji = request.Emoji; + var ctx = request.Ctx; + var userId = ctx.User.Id; - DiscordMessage message = await request.Ctx.RespondAsync("Working on it"); + var emoji = request.Emoji; + + var message = await request.Ctx.RespondAsync("Working on it"); try { - string url = emoji.GetEmojiImageUrl(); - - using var emojiImage = await _imageService.DownloadImage(url); - - var emojiImageAsPng = await ImageService.ConvertImageToPng(emojiImage); + var imageUrl = emoji.GetEmojiImageUrl(); - var squaredEmojiImage = await _imageService.SquareImage(emojiImageAsPng); + var imageStreamResult = await DallifyImage(imageUrl, userId, Size256); - string fileName = $"{Guid.NewGuid()}.png"; + using var imageStream = imageStreamResult.MemoryStream; + var fileExtension = imageStreamResult.FileExtension; - var imageVariationRequest = new CreateImageVariationRequest - { - Image = squaredEmojiImage.MemoryStream.ToArray(), - Size = Size, - User = request.Ctx.User.Id.ToString(), - }; + var imageName = emoji.Id != 0 ? emoji.Id.ToString() : emoji.Name; - var response = await _dalleHttpClient.CreateImageVariation(imageVariationRequest, fileName); + string fileName = $"{imageName}.{fileExtension}"; - if (response.Data == null || !response.Data.Any()) throw new Exception("Empty result"); - - var imageUrl = response.Data.First(); - - var image = await _imageService.DownloadImage(imageUrl.Url); - - var imageStream = await _imageService.GetImageStream(image); - - DiscordMessageBuilder mb = new DiscordMessageBuilder() - .AddFile(fileName, imageStream.MemoryStream) + var mb = new DiscordMessageBuilder() + .AddFile(fileName, imageStream) .WithContent($"There you go {request.Ctx.Member?.Mention ?? "Unknown user"}"); await message.DeleteAsync(); @@ -102,57 +100,116 @@ public async Task Handle(DallifyEmoteRequest request, CancellationToken cancella public async Task Handle(DallifyUserRequest request, CancellationToken cancellationToken) { - CommandContext ctx = request.Ctx; - DiscordUser user = request.User; - DiscordGuild guild = ctx.Guild; + var ctx = request.Ctx; + var user = request.User; + var guild = ctx.Guild; - DiscordMessage message = await request.Ctx.RespondAsync("Working on it"); + var message = await request.Ctx.RespondAsync("Working on it"); try { - DiscordMember? userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); + var userAsMember = await _discordResolver.ResolveGuildMember(guild, user.Id) ?? throw new Exception("Invalid user"); - string avatarUrl = userAsMember.GuildAvatarUrl + var imageUrl = userAsMember.GuildAvatarUrl ?? userAsMember.AvatarUrl ?? throw new Exception("Couldn't load user avatar"); - using var inputImage = await _imageService.DownloadImage(avatarUrl); + var imageStreamResult = await DallifyImage(imageUrl, user.Id, Size512); - var avatarAsPng = await ImageService.ConvertImageToPng(inputImage); + using var imageStream = imageStreamResult.MemoryStream; + var fileExtension = imageStreamResult.FileExtension; - var avatarImageStream = await _imageService.GetImageStream(avatarAsPng); + var imageFilename = user.GetNicknameOrUsername().ToSafeFilename(fileExtension); - string imageFilename = user.GetNicknameOrUsername().ToSafeFilename(avatarImageStream.FileExtension); + DiscordMessageBuilder mb = new DiscordMessageBuilder() + .AddFile(imageFilename, imageStream) + .WithContent($"There you go {request.Ctx.Member?.Mention ?? "Unknown user"}"); - var imageVariationRequest = new CreateImageVariationRequest - { - Image = avatarImageStream.MemoryStream.ToArray(), - Size = Size, - User = request.Ctx.User.Id.ToString(), - }; + await message.DeleteAsync(); - var response = await _dalleHttpClient.CreateImageVariation(imageVariationRequest, imageFilename); + await request.Ctx.RespondAsync(mb); + } + catch (Exception) + { + await message.DeleteAsync(); + throw; + } + } - if (response.Data == null || !response.Data.Any()) throw new Exception("Empty result"); + public async Task Handle(DallifyImageRequest request, CancellationToken cancellationToken) + { + var ctx = request.Ctx; + var user = ctx.User; + var message = ctx.Message; - var imageUrl = response.Data.First(); + var imageUrl = message.GetImageUrlFromMessage(); - var image = await _imageService.DownloadImage(imageUrl.Url); + if (imageUrl == null) + { + await ctx.RespondAsync("I didn't find any images."); + return; + } - var imageStream = await _imageService.GetImageStream(image); + var wokingOnItMessage = await request.Ctx.RespondAsync("Working on it"); + + try + { + var imageStreamResult = await DallifyImage(imageUrl, user.Id, Size1024); + + using var imageStream = imageStreamResult.MemoryStream; + var fileExtension = imageStreamResult.FileExtension; + + var imageFilename = $"{Guid.NewGuid()}.{fileExtension}"; DiscordMessageBuilder mb = new DiscordMessageBuilder() - .AddFile(imageFilename, imageStream.MemoryStream) + .AddFile(imageFilename, imageStream) .WithContent($"There you go {request.Ctx.Member?.Mention ?? "Unknown user"}"); - await message.DeleteAsync(); + await wokingOnItMessage.DeleteAsync(); await request.Ctx.RespondAsync(mb); } catch (Exception) { - await message.DeleteAsync(); + await wokingOnItMessage.DeleteAsync(); throw; } } + + private async Task DallifyImage(string imageUrl, ulong userId, string resultSize) + { + (var image, var inputSize) = await _imageService.DownloadImageWithSize(imageUrl); + + var sizeInMb = (double)inputSize / (1024 * 1024); + + if (sizeInMb > MaxImageSizeInMb) + { + throw new Exception($"The image is larger than {MaxImageSizeInMb} MB"); + } + + var imageAsPng = await _imageService.ConvertImageToPng(image, MaxImageSizeInMb); + + var squaredImage = await _imageService.SquareImage(imageAsPng); + + var fileName = $"{Guid.NewGuid()}.png"; + + var imageVariationRequest = new CreateImageVariationRequest + { + Image = squaredImage.MemoryStream.ToArray(), + Size = resultSize, + User = userId.ToString(), + }; + + var response = await _dalleHttpClient.CreateImageVariation(imageVariationRequest, fileName); + + if (response.Data == null || !response.Data.Any()) throw new Exception("Empty result"); + + var imageResponseUrl = response.Data.First(); + + var imageResult = await _imageService.DownloadImage(imageResponseUrl.Url); + + var imageStream = await _imageService.GetImageStream(imageResult); + + return imageStream; + } } diff --git a/Kattbot/CommandHandlers/Images/PetImage.cs b/Kattbot/CommandHandlers/Images/PetImage.cs index fdb13a2..379a7de 100644 --- a/Kattbot/CommandHandlers/Images/PetImage.cs +++ b/Kattbot/CommandHandlers/Images/PetImage.cs @@ -79,9 +79,11 @@ public async Task Handle(PetEmoteRequest request, CancellationToken cancellation var imageName = emoji.Id != 0 ? emoji.Id.ToString() : emoji.Name; + string fileName = $"{imageName}.{imageStreamResult.FileExtension}"; + var responseBuilder = new DiscordMessageBuilder(); - responseBuilder.AddFile($"{imageName}.{imageStreamResult.FileExtension}", imageStreamResult.MemoryStream); + responseBuilder.AddFile(fileName, imageStreamResult.MemoryStream); await ctx.RespondAsync(responseBuilder); } @@ -130,7 +132,7 @@ public async Task Handle(PetImageRequest request, CancellationToken cancellation using var imageStream = imageStreamResult.MemoryStream; var fileExtension = imageStreamResult.FileExtension; - string imageFilename = $"{Guid.NewGuid()}.{fileExtension}"; + var imageFilename = $"{Guid.NewGuid()}.{fileExtension}"; var responseBuilder = new DiscordMessageBuilder(); diff --git a/Kattbot/CommandModules/ImageModule.cs b/Kattbot/CommandModules/ImageModule.cs index 514c6fb..6539ace 100644 --- a/Kattbot/CommandModules/ImageModule.cs +++ b/Kattbot/CommandModules/ImageModule.cs @@ -99,7 +99,7 @@ public Task Pet(CommandContext ctx, string? speed = null) } [Command("dalle")] - [Cooldown(5, 60, CooldownBucketType.Global)] + [Cooldown(5, 30, CooldownBucketType.Global)] public Task Dalle(CommandContext ctx, [RemainingText] string prompt) { var request = new DallePromptCommand(ctx, prompt); @@ -108,7 +108,7 @@ public Task Dalle(CommandContext ctx, [RemainingText] string prompt) } [Command("dallify")] - [Cooldown(5, 60, CooldownBucketType.Global)] + [Cooldown(5, 30, CooldownBucketType.Global)] public Task Dallify(CommandContext ctx, DiscordEmoji emoji) { var request = new DallifyEmoteRequest(ctx, emoji); @@ -117,11 +117,22 @@ public Task Dallify(CommandContext ctx, DiscordEmoji emoji) } [Command("dallify")] - [Cooldown(5, 60, CooldownBucketType.Global)] + [Cooldown(5, 30, CooldownBucketType.Global)] public Task Dallify(CommandContext ctx, DiscordUser user) { var request = new DallifyUserRequest(ctx, user); return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } + + [Command("dallify")] + [Cooldown(5, 30, CooldownBucketType.Global)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + public Task Dallify(CommandContext ctx, string _ = "") +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + var request = new DallifyImageRequest(ctx); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } } diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index 79d9b93..8bcaa0f 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -33,7 +33,7 @@ public static Image LoadImage(byte[] imageBytes) return Image.Load(imageBytes); } - public static async Task ConvertImageToPng(Image image) + public async Task ConvertImageToPng(Image image, int? maxSizeInMb = null) { if (image.Metadata.DecodedImageFormat is PngFormat) return image; @@ -42,12 +42,27 @@ public static async Task ConvertImageToPng(Image image) await image.SaveAsPngAsync(pngMemoryStream); + pngMemoryStream.Position = 0; + + var sizeInMb = (double)pngMemoryStream.Length / (1024 * 1024); + var convertedImage = await Image.LoadAsync(pngMemoryStream); + if (maxSizeInMb.HasValue && sizeInMb > maxSizeInMb) + { + double differenceRatio = sizeInMb / (int)maxSizeInMb; + convertedImage = ScaleImageSync(convertedImage, 1 / differenceRatio); + } + return convertedImage; } public async Task DownloadImage(string url) + { + return (await DownloadImageWithSize(url)).Image; + } + + public async Task<(Image Image, int Size)> DownloadImageWithSize(string url) { byte[] imageBytes; @@ -59,7 +74,7 @@ public async Task DownloadImage(string url) var image = Image.Load(imageBytes); - return image; + return (image, imageBytes.Length); } catch (HttpRequestException) { @@ -71,10 +86,20 @@ public async Task DownloadImage(string url) } } - public Task ScaleImage(Image image, uint scaleFactor) + public Image ScaleImageSync(Image image, double scaleFactor) + { + int newWidth = (int)(image.Width * scaleFactor); + int newHeight = (int)(image.Height * scaleFactor); + + image.Mutate(i => i.Resize(newWidth, newHeight, KnownResamplers.Hermite)); + + return image; + } + + public Task ScaleImage(Image image, double scaleFactor) { - int newWidth = image.Width * (int)scaleFactor; - int newHeight = image.Height * (int)scaleFactor; + int newWidth = (int)(image.Width * scaleFactor); + int newHeight = (int)(image.Height * scaleFactor); image.Mutate(i => i.Resize(newWidth, newHeight, KnownResamplers.Hermite)); @@ -151,7 +176,7 @@ public Task SquareImage(Image image) image.Mutate(i => { - i.Resize(newSize, newSize); + i.Crop(newSize, newSize); }); return GetImageStream(image); From 8313ea5a8e97425b21526d1b7b0cf1ea5340f2d1 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Tue, 12 Sep 2023 23:08:56 +0200 Subject: [PATCH 11/16] Scale down dallify input image regardless of type --- .../CommandHandlers/Images/DallifyImage.cs | 9 +----- Kattbot/Services/Images/ImageService.cs | 31 +++++++++---------- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index 6e2afbe..dabe707 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -178,14 +178,7 @@ public async Task Handle(DallifyImageRequest request, CancellationToken cancella private async Task DallifyImage(string imageUrl, ulong userId, string resultSize) { - (var image, var inputSize) = await _imageService.DownloadImageWithSize(imageUrl); - - var sizeInMb = (double)inputSize / (1024 * 1024); - - if (sizeInMb > MaxImageSizeInMb) - { - throw new Exception($"The image is larger than {MaxImageSizeInMb} MB"); - } + var image = await _imageService.DownloadImage(imageUrl); var imageAsPng = await _imageService.ConvertImageToPng(image, MaxImageSizeInMb); diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index 8bcaa0f..3a48d48 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -35,34 +35,33 @@ public static Image LoadImage(byte[] imageBytes) public async Task ConvertImageToPng(Image image, int? maxSizeInMb = null) { - if (image.Metadata.DecodedImageFormat is PngFormat) - return image; - using var pngMemoryStream = new MemoryStream(); await image.SaveAsPngAsync(pngMemoryStream); - pngMemoryStream.Position = 0; - var sizeInMb = (double)pngMemoryStream.Length / (1024 * 1024); - var convertedImage = await Image.LoadAsync(pngMemoryStream); + var imageLargerThanMaxSize = maxSizeInMb.HasValue && sizeInMb > maxSizeInMb; + var imageNotPng = !(image.Metadata.DecodedImageFormat is PngFormat); - if (maxSizeInMb.HasValue && sizeInMb > maxSizeInMb) + if (!imageLargerThanMaxSize && !imageNotPng) { - double differenceRatio = sizeInMb / (int)maxSizeInMb; - convertedImage = ScaleImageSync(convertedImage, 1 / differenceRatio); + return image; } - return convertedImage; - } + pngMemoryStream.Position = 0; + var imageAsPng = await Image.LoadAsync(pngMemoryStream); - public async Task DownloadImage(string url) - { - return (await DownloadImageWithSize(url)).Image; + if (imageLargerThanMaxSize) + { + double differenceRatio = sizeInMb / (int)maxSizeInMb!; + imageAsPng = ScaleImageSync(imageAsPng, 1 / differenceRatio); + } + + return imageAsPng; } - public async Task<(Image Image, int Size)> DownloadImageWithSize(string url) + public async Task DownloadImage(string url) { byte[] imageBytes; @@ -74,7 +73,7 @@ public async Task DownloadImage(string url) var image = Image.Load(imageBytes); - return (image, imageBytes.Length); + return image; } catch (HttpRequestException) { From 8318bce2fc68af6e1dd18705dc10e7138f7b87a2 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Wed, 13 Sep 2023 22:08:44 +0200 Subject: [PATCH 12/16] Stickers and twirling --- Kattbot.Tests/PetTests.cs | 23 ++- .../CommandHandlers/Images/DallifyImage.cs | 2 +- Kattbot/CommandHandlers/Images/GetBigEmote.cs | 11 +- Kattbot/CommandHandlers/Images/PetImage.cs | 7 +- .../CommandHandlers/Images/TransformImage.cs | 22 ++- Kattbot/CommandModules/HelpModule.cs | 14 +- Kattbot/CommandModules/ImageModule.cs | 27 ++++ Kattbot/Helpers/DiscordExtensions.cs | 8 +- Kattbot/Services/Images/ImageService.cs | 145 ++++++++++-------- 9 files changed, 171 insertions(+), 88 deletions(-) diff --git a/Kattbot.Tests/PetTests.cs b/Kattbot.Tests/PetTests.cs index 4d98df9..7ac41a7 100644 --- a/Kattbot.Tests/PetTests.cs +++ b/Kattbot.Tests/PetTests.cs @@ -6,6 +6,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace Kattbot.Tests; @@ -45,13 +46,29 @@ public async Task PetPetTest(string inputImage) public async Task CropToCircle(string inputFilename) { string inputFile = Path.Combine(Path.GetTempPath(), inputFilename); - string ouputFile = Path.Combine(Path.GetTempPath(), $"cropped_{inputFilename}"); + string ouputFile = Path.Combine(Path.GetTempPath(), "kattbot", $"cropped_{inputFilename}"); var imageService = new ImageService(null!); - using var image = Image.Load(inputFile); + using var image = Image.Load(inputFile); - var croppedImage = imageService.CropImageToCircle(image); + var croppedImage = imageService.CropToCircle(image); + + await croppedImage.SaveAsPngAsync(ouputFile); + } + + [DataTestMethod] + [DataRow("froge.png")] + public async Task Twirl(string inputFilename) + { + string inputFile = Path.Combine(Path.GetTempPath(), inputFilename); + string ouputFile = Path.Combine(Path.GetTempPath(), "kattbot", $"twirled_{inputFilename}"); + + var imageService = new ImageService(null!); + + using var image = Image.Load(inputFile); + + var croppedImage = imageService.TwirlImage(image); await croppedImage.SaveAsPngAsync(ouputFile); } diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index dabe707..f28f317 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -182,7 +182,7 @@ private async Task DallifyImage(string imageUrl, ulong userId var imageAsPng = await _imageService.ConvertImageToPng(image, MaxImageSizeInMb); - var squaredImage = await _imageService.SquareImage(imageAsPng); + var squaredImage = await _imageService.CropToSquare(imageAsPng); var fileName = $"{Guid.NewGuid()}.png"; diff --git a/Kattbot/CommandHandlers/Images/GetBigEmote.cs b/Kattbot/CommandHandlers/Images/GetBigEmote.cs index c647db1..a449f76 100644 --- a/Kattbot/CommandHandlers/Images/GetBigEmote.cs +++ b/Kattbot/CommandHandlers/Images/GetBigEmote.cs @@ -41,11 +41,14 @@ public async Task Handle(GetBigEmoteRequest request, CancellationToken cancellat string url = emoji.GetEmojiImageUrl(); - using var image = await _imageService.DownloadImage(url); + var image = await _imageService.DownloadImage(url); - var imageStreamResult = hasScaleFactor - ? await _imageService.ScaleImage(image, request.ScaleFactor!.Value) - : await _imageService.GetImageStream(image); + if (hasScaleFactor) + { + image = _imageService.ScaleImage(image, request.ScaleFactor!.Value); + } + + var imageStreamResult = await _imageService.GetImageStream(image); MemoryStream imageStream = imageStreamResult.MemoryStream; string fileExtension = imageStreamResult.FileExtension; diff --git a/Kattbot/CommandHandlers/Images/PetImage.cs b/Kattbot/CommandHandlers/Images/PetImage.cs index 379a7de..4a5ef84 100644 --- a/Kattbot/CommandHandlers/Images/PetImage.cs +++ b/Kattbot/CommandHandlers/Images/PetImage.cs @@ -7,6 +7,7 @@ using Kattbot.Services.Images; using MediatR; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace Kattbot.CommandHandlers.Images; @@ -100,7 +101,7 @@ public async Task Handle(PetUserRequest request, CancellationToken cancellationT ?? userAsMember.AvatarUrl ?? throw new Exception("Couldn't load user avatar"); - var imageStreamResult = await PetImage(imageUrl, request.Speed, _imageService.CropImageToCircle); + var imageStreamResult = await PetImage(imageUrl, request.Speed, _imageService.CropToCircle); using var imageStream = imageStreamResult.MemoryStream; var fileExtension = imageStreamResult.FileExtension; @@ -141,9 +142,9 @@ public async Task Handle(PetImageRequest request, CancellationToken cancellation await ctx.RespondAsync(responseBuilder); } - private async Task PetImage(string imageUrl, string? speed, Func? preTransform = null) + private async Task PetImage(string imageUrl, string? speed, ImageTransformDelegate? preTransform = null) { - var inputImage = await _imageService.DownloadImage(imageUrl); + var inputImage = await _imageService.DownloadImage(imageUrl); if (preTransform != null) { diff --git a/Kattbot/CommandHandlers/Images/TransformImage.cs b/Kattbot/CommandHandlers/Images/TransformImage.cs index fe87572..bc5a894 100644 --- a/Kattbot/CommandHandlers/Images/TransformImage.cs +++ b/Kattbot/CommandHandlers/Images/TransformImage.cs @@ -8,6 +8,7 @@ using Kattbot.Services.Images; using MediatR; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace Kattbot.CommandHandlers.Images; @@ -16,6 +17,7 @@ public enum TransformImageEffect { DeepFry, OilPaint, + Twirl, } public class TransformImageEmoteRequest : CommandRequest @@ -106,7 +108,7 @@ public async Task Handle(TransformImageUserRequest request, CancellationToken ca ?? userAsMember.AvatarUrl ?? throw new Exception("Couldn't load user avatar"); - var imageStreamResult = await TransformImage(imageUrl, effect, _imageService.CropImageToCircle); + var imageStreamResult = await TransformImage(imageUrl, effect, _imageService.CropToCircle); using var imageStream = imageStreamResult.MemoryStream; string fileExtension = imageStreamResult.FileExtension; @@ -148,30 +150,36 @@ public async Task Handle(TransformImageMessageRequest request, CancellationToken await ctx.RespondAsync(responseBuilder); } - private async Task TransformImage(string imageUrl, TransformImageEffect effect, Func? preTransform = null) + private async Task TransformImage(string imageUrl, TransformImageEffect effect, ImageTransformDelegate? preTransform = null) { - var inputImage = await _imageService.DownloadImage(imageUrl); + var inputImage = await _imageService.DownloadImage(imageUrl); if (preTransform != null) { - inputImage = preTransform(inputImage); + inputImage = (Image)preTransform(inputImage); } - ImageStreamResult imageStreamResult; + Image imageResult; if (effect == TransformImageEffect.DeepFry) { - imageStreamResult = await _imageService.DeepFryImage(inputImage); + imageResult = _imageService.DeepFryImage(inputImage); } else if (effect == TransformImageEffect.OilPaint) { - imageStreamResult = await _imageService.OilPaintImage(inputImage); + imageResult = _imageService.OilPaintImage(inputImage); + } + else if (effect == TransformImageEffect.Twirl) + { + imageResult = _imageService.TwirlImage(inputImage, 90); } else { throw new InvalidOperationException($"Unknown effect: {effect}"); } + var imageStreamResult = await _imageService.GetImageStream(imageResult); + return imageStreamResult; } } diff --git a/Kattbot/CommandModules/HelpModule.cs b/Kattbot/CommandModules/HelpModule.cs index e75376a..8764005 100644 --- a/Kattbot/CommandModules/HelpModule.cs +++ b/Kattbot/CommandModules/HelpModule.cs @@ -32,7 +32,7 @@ public Task GetHelp(CommandContext ctx) sb.AppendLine($"`{commandPrefix}stats me [?interval] [?page]`"); sb.AppendLine($"`{commandPrefix}stats best [?interval] [?page]`"); sb.AppendLine($"`{commandPrefix}stats [emote] [?interval]`"); - sb.AppendLine($"`{commandPrefix}help stats .. More information about stats command`"); + sb.AppendLine($"`{commandPrefix}help stats .. See all stats command`"); sb.AppendLine(); sb.AppendLine("Emote commands"); @@ -44,11 +44,10 @@ public Task GetHelp(CommandContext ctx) sb.AppendLine(); sb.AppendLine("Image commands"); sb.AppendLine($"`{commandPrefix}deepfry [emote|user|image]`"); - sb.AppendLine($"`{commandPrefix}oilpaint [emote|user|image]`"); sb.AppendLine($"`{commandPrefix}pet [emote|user|image] [?speed]`"); sb.AppendLine($"`{commandPrefix}dallify [emote|user|image]`"); sb.AppendLine($"`{commandPrefix}dalle [text]`"); - sb.AppendLine($"`{commandPrefix}help images .. More information about image commands`"); + sb.AppendLine($"`{commandPrefix}help images .. See all image commands`"); sb.AppendLine(); sb.AppendLine("Other commands"); @@ -104,6 +103,15 @@ public Task GetImagesHelp(CommandContext ctx) string commandPrefix = _options.CommandPrefix; + sb.AppendLine(); + sb.AppendLine("Commands"); + sb.AppendLine($"`{commandPrefix}deepfry [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}oilpaint [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}twirl [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}pet [emote|user|image] [?speed]`"); + sb.AppendLine($"`{commandPrefix}dallify [emote|user|image]`"); + sb.AppendLine($"`{commandPrefix}dalle [text]`"); + sb.AppendLine(); sb.AppendLine($"Command arguments:"); sb.AppendLine($"`user .. Discord username with # identifier or @mention`"); diff --git a/Kattbot/CommandModules/ImageModule.cs b/Kattbot/CommandModules/ImageModule.cs index 6539ace..d382e0e 100644 --- a/Kattbot/CommandModules/ImageModule.cs +++ b/Kattbot/CommandModules/ImageModule.cs @@ -71,6 +71,33 @@ public Task OilPaint(CommandContext ctx, string _ = "") return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); } + [Command("twirl")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task Twirl(CommandContext ctx, DiscordEmoji emoji) + { + var request = new TransformImageEmoteRequest(ctx, emoji, TransformImageEffect.Twirl); + + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + + [Command("twirl")] + [Cooldown(5, 10, CooldownBucketType.Global)] + public Task Twirl(CommandContext ctx, DiscordUser user) + { + var request = new TransformImageUserRequest(ctx, user, TransformImageEffect.Twirl); + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + + [Command("twirl")] + [Cooldown(5, 10, CooldownBucketType.Global)] +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + public Task Twirl(CommandContext ctx, string _ = "") +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter + { + var request = new TransformImageMessageRequest(ctx, TransformImageEffect.Twirl); + return _commandParallelQueue.Writer.WriteAsync(request).AsTask(); + } + [Command("pet")] [Cooldown(5, 10, CooldownBucketType.Global)] public Task Pet(CommandContext ctx, DiscordEmoji emoji, string? speed = null) diff --git a/Kattbot/Helpers/DiscordExtensions.cs b/Kattbot/Helpers/DiscordExtensions.cs index 419073e..f8f1156 100644 --- a/Kattbot/Helpers/DiscordExtensions.cs +++ b/Kattbot/Helpers/DiscordExtensions.cs @@ -1,5 +1,5 @@ -using DSharpPlus.Entities; -using System.Linq; +using System.Linq; +using DSharpPlus.Entities; namespace Kattbot.Helpers; @@ -46,6 +46,10 @@ public static string GetEmojiImageUrl(this DiscordEmoji emoji) return imgEmbed.Url.AbsoluteUri; } } + else if (message.Stickers.Count > 0) + { + return message.Stickers[0].StickerUrl; + } else if (isRootMessage == true && message.ReferencedMessage != null) { return GetImageUrlFromMessage(message.ReferencedMessage, false); diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index 3a48d48..85836d8 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; @@ -15,10 +14,12 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Color = SixLabors.ImageSharp.Color; -using Point = SixLabors.ImageSharp.Point; namespace Kattbot.Services.Images; +public delegate Image ImageTransformDelegate(Image input) + where TPixel : unmanaged, IPixel; + public class ImageService { private readonly IHttpClientFactory _httpClientFactory; @@ -42,7 +43,7 @@ public async Task ConvertImageToPng(Image image, int? maxSizeInMb = null) var sizeInMb = (double)pngMemoryStream.Length / (1024 * 1024); var imageLargerThanMaxSize = maxSizeInMb.HasValue && sizeInMb > maxSizeInMb; - var imageNotPng = !(image.Metadata.DecodedImageFormat is PngFormat); + var imageNotPng = image.Metadata.DecodedImageFormat is not PngFormat; if (!imageLargerThanMaxSize && !imageNotPng) { @@ -55,7 +56,7 @@ public async Task ConvertImageToPng(Image image, int? maxSizeInMb = null) if (imageLargerThanMaxSize) { double differenceRatio = sizeInMb / (int)maxSizeInMb!; - imageAsPng = ScaleImageSync(imageAsPng, 1 / differenceRatio); + imageAsPng = ScaleImage(imageAsPng, 1 / differenceRatio); } return imageAsPng; @@ -63,17 +64,26 @@ public async Task ConvertImageToPng(Image image, int? maxSizeInMb = null) public async Task DownloadImage(string url) { - byte[] imageBytes; + var bytes = await DownloadImageBytes(url); + + return Image.Load(bytes); + } + + public async Task> DownloadImage(string url) + where TPixel : unmanaged, IPixel + { + var bytes = await DownloadImageBytes(url); + + return Image.Load(bytes); + } + private async Task DownloadImageBytes(string url) + { try { HttpClient client = _httpClientFactory.CreateClient(); - imageBytes = await client.GetByteArrayAsync(url); - - var image = Image.Load(imageBytes); - - return image; + return await client.GetByteArrayAsync(url); } catch (HttpRequestException) { @@ -85,7 +95,7 @@ public async Task DownloadImage(string url) } } - public Image ScaleImageSync(Image image, double scaleFactor) + public Image ScaleImage(Image image, double scaleFactor) { int newWidth = (int)(image.Width * scaleFactor); int newHeight = (int)(image.Height * scaleFactor); @@ -95,17 +105,7 @@ public Image ScaleImageSync(Image image, double scaleFactor) return image; } - public Task ScaleImage(Image image, double scaleFactor) - { - int newWidth = (int)(image.Width * scaleFactor); - int newHeight = (int)(image.Height * scaleFactor); - - image.Mutate(i => i.Resize(newWidth, newHeight, KnownResamplers.Hermite)); - - return GetImageStream(image); - } - - public Task DeepFryImage(Image image) + public Image DeepFryImage(Image image) { image.Mutate(i => { @@ -115,10 +115,10 @@ public Task DeepFryImage(Image image) i.Saturate(5f); }); - return GetImageStream(image); + return image; } - public Task OilPaintImage(Image image) + public Image OilPaintImage(Image image) { int paintLevel = 25; @@ -127,10 +127,61 @@ public Task OilPaintImage(Image image) i.OilPaint(paintLevel, paintLevel); }); - return GetImageStream(image); + return image; } - public Image CropImageToCircle(Image image) + /// + /// Twirls an image + /// Source: jhlabs.com. + /// + /// Source image. + /// Angle in degrees. + /// Twirled image. + public Image TwirlImage(Image src, float angleDeg = 180) + { + var dest = new Image(src.Width, src.Height); + + var centerX = src.Width / 2; + var centerY = src.Height / 2; + var radius = Math.Min(centerX, centerY); + var radius2 = radius * radius; + float angleRad = (float)(angleDeg * Math.PI / 180); + + var transformFn = (int x, int y) => + { + int newX = x; + int newY = x; + + float dx = x - centerX; + float dy = y - centerY; + float distance = (dx * dx) + (dy * dy); + + if (distance <= radius2) + { + distance = (float)Math.Sqrt(distance); + float a = (float)Math.Atan2(dy, dx) + (angleRad * (radius - distance) / radius); + + newX = (int)Math.Floor(centerX + (distance * (float)Math.Cos(a))); + newY = (int)Math.Floor(centerY + (distance * (float)Math.Sin(a))); + } + + return (x: newX, y: newY); + }; + + for (int x = 0; x < src.Width; x++) + { + for (int y = 0; y < src.Height; y++) + { + var trans = transformFn(x, y); + dest[x, y] = src[trans.x, trans.y]; + } + } + + return dest; + } + + public Image CropToCircle(Image image) + where TPixel : unmanaged, IPixel { var ellipsePath = new EllipsePolygon(image.Width / 2, image.Height / 2, image.Width, image.Height); @@ -145,7 +196,7 @@ public Image CropImageToCircle(Image image) stream.Position = 0; - imageAsPngWithTransparency = Image.Load(stream); + imageAsPngWithTransparency = Image.Load(stream); } else { @@ -166,10 +217,10 @@ public Image CropImageToCircle(Image image) i.Fill(opts, Color.Black, ellipsePath); }); - return cloned; + return (Image)cloned; } - public Task SquareImage(Image image) + public Task CropToSquare(Image image) { int newSize = Math.Min(image.Width, image.Height); @@ -181,42 +232,6 @@ public Task SquareImage(Image image) return GetImageStream(image); } - public async Task CombineImages(string[] base64Images) - { - IEnumerable bytesImages = base64Images.Select(Convert.FromBase64String); - - var images = bytesImages.Select(x => Image.Load(x)).ToList(); - - // Assume all images have the same size. If this turns out to not be true, - // might have to upscale/downscale them to get them to be the same size. - int imageWidth = images.First().Width; - int imageHeight = images.First().Height; - - int gridSize = (int)Math.Ceiling(Math.Sqrt(images.Count)); - - int canvasWidth = imageWidth * gridSize; - int canvasHeight = imageHeight * gridSize; - - var outputImage = new Image(canvasWidth, canvasHeight); - - for (int i = 0; i < images.Count; i++) - { - int x = i % gridSize; - int y = i / gridSize; - - Image image = images[i]; - - int positionX = imageWidth * x; - int positionY = imageHeight * y; - - outputImage.Mutate(x => x.DrawImage(image, new Point(positionX, positionY), 1f)); - } - - ImageStreamResult outputImageStream = await GetImageStream(outputImage); - - return outputImageStream; - } - public async Task SaveImageToTempPath(Image image, string filename) { var format = image.Metadata.GetFormatOrDefault(); From 6c819a47dad0382122ad6c485ea831de1b62e987 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Wed, 13 Sep 2023 22:29:12 +0200 Subject: [PATCH 13/16] Limit dallify result image size to not be larger than input image size --- .../CommandHandlers/Images/DallifyImage.cs | 21 ++++++++++++------- Kattbot/Services/Images/ImageService.cs | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index f28f317..9af3a88 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -45,11 +45,14 @@ public class DallifyImageHandler : IRequestHandler, IRequestHandler, IRequestHandler { - public static readonly string Size256 = "256x256"; - public static readonly string Size512 = "512x512"; - public static readonly string Size1024 = "1024x1024"; + public static readonly int Size256 = 256; + public static readonly int Size512 = 512; + public static readonly int Size1024 = 1024; private const int MaxImageSizeInMb = 4; + + private static readonly int[] ValidSizes = { Size256, Size512, Size1024 }; + private readonly DalleHttpClient _dalleHttpClient; private readonly ImageService _imageService; private readonly DiscordResolver _discordResolver; @@ -176,20 +179,24 @@ public async Task Handle(DallifyImageRequest request, CancellationToken cancella } } - private async Task DallifyImage(string imageUrl, ulong userId, string resultSize) + private async Task DallifyImage(string imageUrl, ulong userId, int maxSize) { var image = await _imageService.DownloadImage(imageUrl); var imageAsPng = await _imageService.ConvertImageToPng(image, MaxImageSizeInMb); - var squaredImage = await _imageService.CropToSquare(imageAsPng); + var squaredImage = _imageService.CropToSquare(imageAsPng); + + var resultSize = Math.Min(maxSize, ValidSizes.Reverse().First(s => squaredImage.Height > s)); var fileName = $"{Guid.NewGuid()}.png"; + var inputImageStream = await _imageService.GetImageStream(squaredImage); + var imageVariationRequest = new CreateImageVariationRequest { - Image = squaredImage.MemoryStream.ToArray(), - Size = resultSize, + Image = inputImageStream.MemoryStream.ToArray(), + Size = $"{resultSize}x{resultSize}", User = userId.ToString(), }; diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index 85836d8..f250f23 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -220,7 +220,7 @@ public Image CropToCircle(Image image) return (Image)cloned; } - public Task CropToSquare(Image image) + public Image CropToSquare(Image image) { int newSize = Math.Min(image.Width, image.Height); @@ -229,7 +229,7 @@ public Task CropToSquare(Image image) i.Crop(newSize, newSize); }); - return GetImageStream(image); + return image; } public async Task SaveImageToTempPath(Image image, string filename) From 2db07263cd411bf43aeb7738822e813791a95b1b Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Wed, 13 Sep 2023 23:22:59 +0200 Subject: [PATCH 14/16] Update DallifyImage.cs --- Kattbot/CommandHandlers/Images/DallifyImage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index 9af3a88..7ab2417 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -187,7 +187,7 @@ private async Task DallifyImage(string imageUrl, ulong userId var squaredImage = _imageService.CropToSquare(imageAsPng); - var resultSize = Math.Min(maxSize, ValidSizes.Reverse().First(s => squaredImage.Height > s)); + var resultSize = Math.Min(maxSize, ValidSizes.Reverse().FirstOrDefault(s => squaredImage.Height > s) ?? ValidSizes[0]); var fileName = $"{Guid.NewGuid()}.png"; From 946f4bd8a19ad785dfc2c9d42a65e3e8decca8a0 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Wed, 13 Sep 2023 23:29:28 +0200 Subject: [PATCH 15/16] Update DallifyImage.cs --- Kattbot/CommandHandlers/Images/DallifyImage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index 7ab2417..000a67e 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -187,7 +187,7 @@ private async Task DallifyImage(string imageUrl, ulong userId var squaredImage = _imageService.CropToSquare(imageAsPng); - var resultSize = Math.Min(maxSize, ValidSizes.Reverse().FirstOrDefault(s => squaredImage.Height > s) ?? ValidSizes[0]); + var resultSize = Math.Min(maxSize, Math.Max(ValidSizes.Reverse().FirstOrDefault(s => squaredImage.Height > s), ValidSizes[0])); var fileName = $"{Guid.NewGuid()}.png"; From f4238bee616ac552f82c6b05c352ab7651fa8ab4 Mon Sep 17 00:00:00 2001 From: Bob Loblaw Date: Thu, 14 Sep 2023 23:54:18 +0200 Subject: [PATCH 16/16] Wait for message embeds to load for up to 5 seconds --- .../CommandHandlers/Images/DallifyImage.cs | 2 +- Kattbot/CommandHandlers/Images/PetImage.cs | 2 +- .../CommandHandlers/Images/TransformImage.cs | 2 +- Kattbot/Helpers/DiscordExtensions.cs | 70 +++++++++++++++---- Kattbot/Services/Images/ImageService.cs | 4 +- 5 files changed, 63 insertions(+), 17 deletions(-) diff --git a/Kattbot/CommandHandlers/Images/DallifyImage.cs b/Kattbot/CommandHandlers/Images/DallifyImage.cs index 9af3a88..3df4804 100644 --- a/Kattbot/CommandHandlers/Images/DallifyImage.cs +++ b/Kattbot/CommandHandlers/Images/DallifyImage.cs @@ -145,7 +145,7 @@ public async Task Handle(DallifyImageRequest request, CancellationToken cancella var user = ctx.User; var message = ctx.Message; - var imageUrl = message.GetImageUrlFromMessage(); + var imageUrl = await message.GetImageUrlFromMessage(); if (imageUrl == null) { diff --git a/Kattbot/CommandHandlers/Images/PetImage.cs b/Kattbot/CommandHandlers/Images/PetImage.cs index 4a5ef84..bec3904 100644 --- a/Kattbot/CommandHandlers/Images/PetImage.cs +++ b/Kattbot/CommandHandlers/Images/PetImage.cs @@ -120,7 +120,7 @@ public async Task Handle(PetImageRequest request, CancellationToken cancellation var ctx = request.Ctx; var message = ctx.Message; - var imageUrl = message.GetImageUrlFromMessage(); + var imageUrl = await message.GetImageUrlFromMessage(); if (imageUrl == null) { diff --git a/Kattbot/CommandHandlers/Images/TransformImage.cs b/Kattbot/CommandHandlers/Images/TransformImage.cs index bc5a894..af659af 100644 --- a/Kattbot/CommandHandlers/Images/TransformImage.cs +++ b/Kattbot/CommandHandlers/Images/TransformImage.cs @@ -128,7 +128,7 @@ public async Task Handle(TransformImageMessageRequest request, CancellationToken var message = ctx.Message; var effect = request.Effect; - var imageUrl = message.GetImageUrlFromMessage(); + var imageUrl = await message.GetImageUrlFromMessage(); if (imageUrl == null) { diff --git a/Kattbot/Helpers/DiscordExtensions.cs b/Kattbot/Helpers/DiscordExtensions.cs index f8f1156..f5230e3 100644 --- a/Kattbot/Helpers/DiscordExtensions.cs +++ b/Kattbot/Helpers/DiscordExtensions.cs @@ -1,4 +1,8 @@ -using System.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using DSharpPlus.Entities; namespace Kattbot.Helpers; @@ -26,7 +30,30 @@ public static string GetEmojiImageUrl(this DiscordEmoji emoji) return isEmote ? emoji.Url : EmoteHelper.GetExternalEmojiImageUrl(emoji.Name); } - public static string? GetImageUrlFromMessage(this DiscordMessage message, bool isRootMessage = true) + public static async Task GetImageUrlFromMessage(this DiscordMessage message) + { + var imgUrl = message.GetAttachmentOrStickerImage(); + + if (imgUrl != null) + return imgUrl; + + if (message.ReferencedMessage != null) + imgUrl = message.ReferencedMessage.GetAttachmentOrStickerImage(); + + if (imgUrl != null) + return imgUrl; + + var waitTasks = new List> { message.WaitForEmbedImage() }; + + if (message.ReferencedMessage != null) + waitTasks.Add(message.ReferencedMessage.WaitForEmbedImage()); + + imgUrl = await (await Task.WhenAny(waitTasks)); + + return imgUrl; + } + + private static string? GetAttachmentOrStickerImage(this DiscordMessage message) { if (message.Attachments.Count > 0) { @@ -37,22 +64,41 @@ public static string GetEmojiImageUrl(this DiscordEmoji emoji) return imgAttachment.Url; } } - else if (message.Embeds.Count > 0) + else if (message.Stickers.Count > 0) { - var imgEmbed = message.Embeds.Where(e => e.Type == "image").FirstOrDefault(); + return message.Stickers[0].StickerUrl; + } - if (imgEmbed != null) + return null; + } + + private static async Task WaitForEmbedImage(this DiscordMessage message) + { + const int maxWaitDurationms = 5 * 1000; + const int delayMs = 100; + + var cts = new CancellationTokenSource(maxWaitDurationms); + + try + { + while (!cts.IsCancellationRequested) { - return imgEmbed.Url.AbsoluteUri; + if (message.Embeds.Count > 0) + { + var imgEmbed = message.Embeds.Where(e => e.Type == "image").FirstOrDefault(); + + if (imgEmbed != null) + { + return imgEmbed.Url.AbsoluteUri; + } + } + + await Task.Delay(delayMs); } } - else if (message.Stickers.Count > 0) - { - return message.Stickers[0].StickerUrl; - } - else if (isRootMessage == true && message.ReferencedMessage != null) + catch (OperationCanceledException) { - return GetImageUrlFromMessage(message.ReferencedMessage, false); + return null; } return null; diff --git a/Kattbot/Services/Images/ImageService.cs b/Kattbot/Services/Images/ImageService.cs index f250f23..5349ba7 100644 --- a/Kattbot/Services/Images/ImageService.cs +++ b/Kattbot/Services/Images/ImageService.cs @@ -185,7 +185,7 @@ public Image CropToCircle(Image image) { var ellipsePath = new EllipsePolygon(image.Width / 2, image.Height / 2, image.Width, image.Height); - Image imageAsPngWithTransparency; + Image imageAsPngWithTransparency; if (image.Metadata.DecodedImageFormat is not PngFormat || image.Metadata.GetPngMetadata().ColorType is not PngColorType.RgbWithAlpha) @@ -217,7 +217,7 @@ public Image CropToCircle(Image image) i.Fill(opts, Color.Black, ellipsePath); }); - return (Image)cloned; + return cloned; } public Image CropToSquare(Image image)