From 16e90d228e6186d102f13560cc3ed6985cb63db3 Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 29 Dec 2025 11:40:02 +0100 Subject: [PATCH] Add sticker manager in non admin section of admin api test client --- .../Pages/NonAdmin/StickerManager.razor | 236 ++++++++++++++++++ .../Spacebar.AdminApi.TestClient/Program.cs | 8 +- .../Services/StreamingHttpClient.cs | 11 +- 3 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor new file mode 100644 index 000000000..73ce4ec2d --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor @@ -0,0 +1,236 @@ +@page "/NonAdmin/Guilds/{GuildId}/StickerManager" +@using System.Diagnostics +@using System.Diagnostics.CodeAnalysis +@using System.Net.Http.Headers +@using System.Text.Json.Serialization +@using ArcaneLibs.Blazor.Components +@using ArcaneLibs.Blazor.Components.Services +@using ArcaneLibs.Extensions +@using Microsoft.JSInterop.WebAssembly +@using Spacebar.AdminApi.TestClient.Services +@inject Config Config +@inject WebAssemblyJSRuntime JSRuntime +@inject JsConsoleService JsConsole +

StickerManager

+ +@* drop zone for uploads *@ + + +

Drag and drop files here, or click to select.

+
+ +

+ @Stickers.Count stickers, of which @Stickers.Count(x => x is LocalSticker) are not yet uploaded. + Save all +

+ +@if (SaveProgress.End.Value > 0) { + + @SaveProgress.Start.Value / @Stickers.Count +} + +@foreach (var sticker in Stickers.OrderBy(x => ulong.Parse(x.Id))) { +
+ +
+ ID: @sticker.Id
+ Name:
+ Description:
+ Tags:
+ Available:
+ Type: @sticker.Type.ToString()
+ Format Type: @sticker.FormatType.ToString()
+ @if (sticker is LocalSticker ls2) { + Upload + } + else { + Save + } + Delete +
+
+
+} + +@code { + + [Parameter] + public required string GuildId { get; set; } + + private List Stickers { get; set; } = []; + + private Range SaveProgress { get; set; } = ..0; + + protected override async Task OnInitializedAsync() { + var hc = new StreamingHttpClient(); + hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken); + Stickers = await hc.GetFromJsonAsync>($"{Config.ApiUrl}/api/v9/guilds/{GuildId}/stickers"); + } + + private class Sticker { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("description")] + public string Description { get; set; } + + [JsonPropertyName("tags")] + public string Tags { get; set; } + + [JsonPropertyName("available")] + public bool Available { get; set; } + + [JsonPropertyName("type")] + public StickerType Type { get; set; } + + [JsonPropertyName("format_type")] + public StickerFormatType FormatType { get; set; } + } + + enum StickerType { + Standard = 1, + Guild = 2 + } + + enum StickerFormatType { + Png = 1, + Apng = 2, + Lottie = 3, + Gif = 4 + } + + private class LocalSticker : Sticker { + [JsonIgnore] + public required byte[] Data { get; set; } + + [JsonIgnore] + public required string FileName { get; set; } + + [JsonIgnore] + public required string ContentType { get; set; } + + [JsonIgnore] + public required string BlobUri { get; set; } + } + + private async Task SaveChangesAsync(Sticker sticker) { + var hc = new StreamingHttpClient(); + hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken); + var res = await hc.PatchAsJsonAsync($"{Config.ApiUrl}/api/v9/guilds/{GuildId}/stickers/{sticker.Id}", new { + name = sticker.Name, + description = sticker.Description, + tags = sticker.Tags + // available = sticker.Available + }); + return await res.Content.ReadFromJsonAsync(); + } + + private async Task DeleteAsync(Sticker sticker) { + var hc = new StreamingHttpClient(); + hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken); + if ((await hc.DeleteAsync($"{Config.ApiUrl}/api/v9/guilds/{GuildId}/stickers/{sticker.Id}")).IsSuccessStatusCode) + Stickers.Remove(sticker); + StateHasChanged(); + } + + private async Task UploadAsync(LocalSticker sticker) { + var hc = new StreamingHttpClient(); + hc.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Config.AccessToken); + + var req = new HttpRequestMessage(HttpMethod.Post, $"{Config.ApiUrl}/api/v9/guilds/{GuildId}/stickers"); + MultipartFormDataContent content; + req.Content = content = new MultipartFormDataContent(); + content.Add(new StringContent(sticker.Name), "name"); + content.Add(new StringContent(sticker.Description ?? ""), "description"); + content.Add(new StringContent(sticker.Tags ?? ""), "tags"); + + content.Add(new ByteArrayContent(sticker.Data) { + Headers = { + ContentType = new MediaTypeHeaderValue(sticker.ContentType) + } + }, "file", sticker.FileName); + + var res = await hc.SendAsync(req); + return await res.Content.ReadFromJsonAsync(); + } + + private async Task HandleFilesDropped(InputFileChangeEventArgs arg) { + var tasks = arg.GetMultipleFiles(10000).Select(async file => { + await using var ms = new MemoryStream(); + await using var stream = file.OpenReadStream(10 * 1024 * 1024); + await stream.CopyToAsync(ms); + + return new LocalSticker { + Id = "0", + Name = Path.GetFileNameWithoutExtension(file.Name), + Available = true, + Type = StickerType.Guild, + FormatType = file.ContentType switch { + "image/png" => StickerFormatType.Png, + "image/apng" => StickerFormatType.Apng, + "application/json" => StickerFormatType.Lottie, + "image/gif" => StickerFormatType.Gif, + _ => throw new Exception("Unsupported sticker format: " + file.ContentType) + }, + FileName = file.Name, + ContentType = file.ContentType, + Data = ms.ToArray(), + // DataUri = $"data:{file.ContentType};base64,{Convert.ToBase64String(ms.ToArray())}" + BlobUri = await GetBlobUriAsync(ms.ToArray(), file.ContentType) + }; + }).ToList().ToAsyncResultEnumerable(); + await foreach (var sticker in tasks) { + Stickers.Add(sticker); + if (Stickers.Count % 25 == 0) StateHasChanged(); + } + + StateHasChanged(); + } + + private async Task GetBlobUriAsync(byte[] data, string type = "application/octet-stream") { + await using var jsBlob = JSRuntime.InvokeConstructor("Blob", (byte[][])[data], new { type }); + var uri = JSRuntime.Invoke("URL.createObjectURL", jsBlob); + return uri; + } + + private async Task SaveAll() { + SaveProgress = ..Stickers.Count; + StateHasChanged(); + var ss = new SemaphoreSlim(32, 32); + var tasks = Stickers.ToList().Select(async s => { + return await ProcessSticker(); + + async Task<(Sticker Old, Sticker? New)> ProcessSticker() { + await ss.WaitAsync(); + try { + return (Old: s, New: await (s is LocalSticker ls ? UploadAsync(ls) : SaveChangesAsync(s))); + } + finally { + ss.Release(); + } + } + }).ToList().ToAsyncResultEnumerable(); + + var sw = Stopwatch.StartNew(); + await foreach (var r in tasks) { + SaveProgress = (SaveProgress.Start.Value + 1)..SaveProgress.End.Value; + Stickers[Stickers.IndexOf(r.Old)] = r.New ?? r.Old; + if (r.Old is LocalSticker ls) + JSRuntime.InvokeVoid("URL.revokeObjectURL", ls.BlobUri); + + if (sw.ElapsedMilliseconds > 2000) { + StateHasChanged(); + await Task.Delay(100); + sw.Restart(); + } + } + + SaveProgress = ..0; + StateHasChanged(); + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs index d28066da6..ba12a172c 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Program.cs @@ -2,9 +2,12 @@ using System.Net; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; +using ArcaneLibs.Blazor.Components.Services; using Blazored.LocalStorage; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; +using Microsoft.JSInterop; +using Microsoft.JSInterop.WebAssembly; using Spacebar.AdminApi.TestClient; using Spacebar.AdminApi.TestClient.Services; @@ -52,8 +55,11 @@ builder.Services.AddBlazoredLocalStorageAsSingleton(config => { config = new Config(); await localStorage.SetItemAsync("sb_admin_tc_config", config); } + builder.Services.AddSingleton(config); } - +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => (WebAssemblyJSRuntime)sp.GetRequiredService()); +builder.Services.AddSingleton(sp => (IJSInProcessRuntime)sp.GetRequiredService()); await builder.Build().RunAsync(); \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs index 0f2ade713..311eb4579 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs @@ -282,9 +282,9 @@ public class StreamingHttpClient { return await SendAsync(request, cancellationToken); } - public async Task DeleteAsync(string url) { + public async Task DeleteAsync(string url) { var request = new HttpRequestMessage(HttpMethod.Delete, url); - await SendAsync(request); + return await SendAsync(request); } public async Task DeleteAsJsonAsync(string url, T payload) { @@ -293,5 +293,12 @@ public class StreamingHttpClient { }; return await SendAsync(request); } + + public async Task PatchAsJsonAsync(string url, T payload) { + var request = new HttpRequestMessage(new HttpMethod("PATCH"), url) { + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") + }; + return await SendAsync(request); + } } #endif \ No newline at end of file