diff --git a/assets/openapi.json b/assets/openapi.json index 3b7ea5a..9cf9ac7 100644 --- a/assets/openapi.json +++ b/assets/openapi.json @@ -4051,15 +4051,16 @@ 13, 14, 15, - 16, - 17, - 18, 2, 255, 3, + 33, + 34, + 35, 4, 5, 6, + 64, 7, 8, 9 @@ -4139,15 +4140,6 @@ }, "video_quality_mode": { "type": "integer" - }, - "auto_archive_duration": { - "type": "integer" - }, - "archived": { - "type": "boolean" - }, - "locked": { - "type": "boolean" } } }, @@ -6433,12 +6425,7 @@ "type": "string" }, "type": { - "enum": [ - 10, - 11, - 12 - ], - "type": "number" + "type": "integer" }, "location": { "type": "string" @@ -7421,7 +7408,7 @@ "default": 0 }, "thread_metadata": { - "$ref": "#/components/schemas/ThreadMetadata" + "$ref": "#/components/schemas/threadMetadata" }, "member_count": { "type": "integer" @@ -7470,9 +7457,10 @@ 13, 14, 15, - 16, - 17, - 18, + 33, + 34, + 35, + 64, 255 ] }, @@ -9906,7 +9894,7 @@ "user_id" ] }, - "ThreadMetadata": { + "threadMetadata": { "type": "object", "properties": { "archived": { @@ -10167,52 +10155,13 @@ 14, 15, 16, - 17, - 18, 19, 20, 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, 41, 42, 43, - 44, - 45, - 46, - 47, - 48, - 49, 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, 63, 255 ] @@ -22492,150 +22441,6 @@ ] } }, - "/channels/{channel_id}/thread-members/": { - "get": { - "x-permission-required": "VIEW_CHANNEL", - "security": [ - { - "bearer": [] - } - ], - "responses": { - "200": { - "description": "No description available" - }, - "403": { - "description": "No description available" - } - }, - "parameters": [ - { - "name": "channel_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "channel_id" - } - ], - "tags": [ - "channels" - ] - } - }, - "/channels/{channel_id}/thread-members/{user_id}": { - "post": { - "x-permission-required": "VIEW_CHANNEL", - "security": [ - { - "bearer": [] - } - ], - "responses": { - "200": { - "description": "No description available" - }, - "403": { - "description": "No description available" - } - }, - "parameters": [ - { - "name": "channel_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "channel_id" - }, - { - "name": "user_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "user_id" - } - ], - "tags": [ - "channels" - ] - }, - "delete": { - "x-permission-required": "VIEW_CHANNEL", - "security": [ - { - "bearer": [] - } - ], - "responses": { - "200": { - "description": "No description available" - }, - "403": { - "description": "No description available" - } - }, - "parameters": [ - { - "name": "channel_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "channel_id" - }, - { - "name": "user_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "user_id" - } - ], - "tags": [ - "channels" - ] - } - }, - "/channels/{channel_id}/thread-members/@me/settings": { - "patch": { - "x-permission-required": "VIEW_CHANNEL", - "security": [ - { - "bearer": [] - } - ], - "responses": { - "200": { - "description": "No description available" - }, - "403": { - "description": "No description available" - } - }, - "parameters": [ - { - "name": "channel_id", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "channel_id" - } - ], - "tags": [ - "channels" - ] - } - }, "/channels/{channel_id}/recipients/{user_id}": { "put": { "security": [ diff --git a/assets/schemas.json b/assets/schemas.json index b3af1c5..71631da 100644 --- a/assets/schemas.json +++ b/assets/schemas.json @@ -4294,15 +4294,16 @@ 13, 14, 15, - 16, - 17, - 18, 2, 255, 3, + 33, + 34, + 35, 4, 5, 6, + 64, 7, 8, 9 @@ -4386,15 +4387,6 @@ }, "video_quality_mode": { "type": "integer" - }, - "auto_archive_duration": { - "type": "integer" - }, - "archived": { - "type": "boolean" - }, - "locked": { - "type": "boolean" } }, "additionalProperties": false, @@ -6862,12 +6854,7 @@ "type": "string" }, "type": { - "enum": [ - 10, - 11, - 12 - ], - "type": "number" + "type": "integer" }, "location": { "type": "string" @@ -7888,7 +7875,7 @@ "default": 0 }, "thread_metadata": { - "$ref": "#/definitions/ThreadMetadata" + "$ref": "#/definitions/threadMetadata" }, "member_count": { "type": "integer" @@ -7939,9 +7926,10 @@ 13, 14, 15, - 16, - 17, - 18, + 33, + 34, + 35, + 64, 255 ], "$schema": "http://json-schema.org/draft-07/schema#" @@ -10473,7 +10461,7 @@ ], "$schema": "http://json-schema.org/draft-07/schema#" }, - "ThreadMetadata": { + "threadMetadata": { "type": "object", "properties": { "archived": { @@ -10746,52 +10734,13 @@ 14, 15, 16, - 17, - 18, 19, 20, 21, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, 41, 42, 43, - 44, - 45, - 46, - 47, - 48, - 49, 50, - 51, - 52, - 53, - 54, - 55, - 56, - 57, - 58, - 59, - 60, - 61, - 62, 63, 255 ], diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs index a319907..a5cee2c 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs @@ -3,34 +3,42 @@ namespace Spacebar.Interop.Cdn.Abstractions; public class FilesystemFileSource(string baseUrl) : IFileSource { - private static LruFileCache _cache = new(100 * 1024 * 1024); // 100 MB - private readonly StreamingHttpClient _httpClient = new() { BaseAddress = new Uri(baseUrl) }; public string BaseUrl => baseUrl; - public async Task GetFile(string path, CancellationToken? cancellationToken = null) { - var res = await _cache.GetOrAdd(path, async () => { - var res = await _httpClient.SendUnhandledAsync(new(HttpMethod.Get, path), cancellationToken); - res.EnsureSuccessStatusCode(); - var ms = new MemoryStream(); - await res.Content.CopyToAsync(ms); - return new LruFileCache.Entry { - Data = ms.ToArray(), - MimeType = res.Content.Headers.ContentType?.MediaType ?? "application/octet-stream" - }; - }); + public async Task Init(CancellationToken? cancellationToken = null) { + foreach (var dir in new[] { "avatars", "banners", "icons", "stickers", "emojis" }) { + var fullPath = Path.Join(baseUrl, dir); + if (!Directory.Exists(fullPath)) + Directory.CreateDirectory(fullPath); + } + } + public async Task GetFile(string path, CancellationToken? cancellationToken = null) { + await using var rs = File.OpenRead(Path.Join(baseUrl, path)); + var ms = new MemoryStream(); + await rs.CopyToAsync(ms); return new() { - Stream = new MemoryStream(res.Data), - MimeType = res.MimeType + Stream = ms, + MimeType = "MIME/TYPE" }; } public Task FileExists(string path, CancellationToken? cancellationToken = null) { - return Task.FromResult(File.Exists(Path.Combine(baseUrl, path))); + return Task.FromResult(File.Exists(Path.Join(baseUrl, path))); + } + + public async Task WriteFile(string path, Stream stream) { + var fullPath = Path.Join(baseUrl, path); + // Console.WriteLine($"Writing file to {fullPath}... "); + if (!Directory.Exists(Path.GetDirectoryName(fullPath)!)) + Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!); + + await using var fs = File.Create(fullPath); + await stream.CopyToAsync(fs); } // private string GetMimeType(Stream stream) diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs index 2ffa9ac..a69764d 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs @@ -2,8 +2,10 @@ public interface IFileSource { public string BaseUrl { get; } + public Task Init(CancellationToken? cancellationToken = null); public Task GetFile(string path, CancellationToken? cancellationToken = null); public Task FileExists(string path, CancellationToken? cancellationToken = null); + public Task WriteFile(string path, Stream stream); } public class FileInfo : IDisposable, IAsyncDisposable { diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs index af7cecf..6a84bd4 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs @@ -1,7 +1,14 @@ +using System.Net; using ArcaneLibs; +using ArcaneLibs.Extensions; +using ArcaneLibs.Extensions.Streams; namespace Spacebar.Interop.Cdn.Abstractions; +/// +/// Class only exists as a stepping stone between old cdn and migrations... +/// +/// public class ProxyFileSource(string baseUrl) : IFileSource { private static LruFileCache _cache = new(100 * 1024 * 1024); // 100 MB @@ -11,6 +18,8 @@ public string BaseUrl => baseUrl; + public async Task Init(CancellationToken? cancellationToken = null) { } + public async Task GetFile(string path, CancellationToken? cancellationToken = null) { var res = await _cache.GetOrAdd(path, async () => { var res = await _httpClient.SendUnhandledAsync(new(HttpMethod.Get, path), cancellationToken); @@ -31,6 +40,14 @@ public async Task FileExists(string path, CancellationToken? cancellationToken = null) { var res = await _httpClient.SendUnhandledAsync(new(HttpMethod.Head, path), cancellationToken); + if (!res.IsSuccessStatusCode) { + await using var s = await res.Content.ReadAsStreamAsync(); + Console.WriteLine($"Got {res.StatusCode}: ({res.Content.Headers.ContentLength}b of {res.Content.Headers.ContentType})\n{s.ReadToEnd().AsString()}"); + } return res.IsSuccessStatusCode; } + + public async Task WriteFile(string path, Stream stream) { + Console.WriteLine("Can't write to HTTP store! Ignoring."); + } } \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/CdnSignature.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/CdnSignature.cs new file mode 100644 index 0000000..b0b2a7e --- /dev/null +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/CdnSignature.cs @@ -0,0 +1,22 @@ +namespace Spacebar.Interop.Cdn.Signing; + +public class CdnSignature { + public string Path { get; set; } = string.Empty; + public string? IpAddress { get; set; } = string.Empty; + public string? UserAgent { get; set; } = string.Empty; +} + +public class CdnSignatureResult : CdnSignature { + // hm + public required string Signature { get; set; } + + // is + public required DateTimeOffset CreatedAt { get; set; } + + // ex + public required DateTimeOffset ExpiresAt { get; set; } + + public string GetSignedPath() { + return $"{Path}?is={CreatedAt.ToUnixTimeMilliseconds():x}&ex={ExpiresAt.ToUnixTimeMilliseconds():x}&hm={Signature}"; + } +} \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/CdnSigningService.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/CdnSigningService.cs new file mode 100644 index 0000000..1fb32b9 --- /dev/null +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/CdnSigningService.cs @@ -0,0 +1,60 @@ +using System.Security.Cryptography; +using ArcaneLibs.Extensions; +using Microsoft.Extensions.Logging; + +namespace Spacebar.Interop.Cdn.Signing; + +public class CdnSigningService(ILogger logger, byte[] signatureKey, bool requireUserAgent, bool requireIpAddress, TimeSpan expiryTime) { + public CdnSignatureResult Sign(CdnSignature data) { + if (requireIpAddress && string.IsNullOrEmpty(data.IpAddress)) { + logger.LogWarning("Signing request missing required IP address"); + throw new ArgumentException("IP address is required for signing"); + } + + if (requireUserAgent && string.IsNullOrEmpty(data.UserAgent)) { + logger.LogWarning("Signing request missing required User-Agent"); + throw new ArgumentException("User-Agent is required for signing"); + } + + var now = DateTimeOffset.UtcNow; + var expiresAt = now.Add(expiryTime); + + return Hash(new() { + Path = data.Path, + IpAddress = data.IpAddress, + UserAgent = data.UserAgent, + CreatedAt = now, + ExpiresAt = expiresAt, + Signature = null! + }); + } + + private CdnSignatureResult Hash(CdnSignatureResult data) { + byte[] signatureData = [ + .. data.Path.AsBytes(), + .. data.CreatedAt.ToUnixTimeMilliseconds().ToString("x").AsBytes(), + .. data.ExpiresAt.ToUnixTimeMilliseconds().ToString("x").AsBytes(), + .. (requireIpAddress ? data.IpAddress?.AsBytes() : []) ?? [], + .. (requireUserAgent ? data.UserAgent?.AsBytes() : []) ?? [] + ]; + var hash = HMACSHA256.HashData(signatureKey, signatureData).AsHexString().Replace(" ", "").ToLower(); + + logger.LogTrace("Hash: creating new hash for {path}", data.Path); + if (logger.IsEnabled(LogLevel.Trace)) { + signatureData.HexDump(); + } + + var sr = new CdnSignatureResult() { + Path = data.Path, + IpAddress = data.IpAddress, + UserAgent = data.UserAgent, + CreatedAt = data.CreatedAt, + ExpiresAt = data.ExpiresAt, + Signature = hash, + }; + + logger.LogTrace("Hash: created new hash for {path}, valid between {start} .. {end}: {json}", data.Path, data.CreatedAt, data.ExpiresAt, sr.ToJson()); + + return sr; + } +} \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/Spacebar.Interop.Cdn.Signing.csproj b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/Spacebar.Interop.Cdn.Signing.csproj new file mode 100644 index 0000000..afa8afb --- /dev/null +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Signing/Spacebar.Interop.Cdn.Signing.csproj @@ -0,0 +1,14 @@ + + + + net10.0 + enable + enable + + + + + + + + diff --git a/extra/admin-api/Spacebar.Cdn/Services/DiscordImageResizeService.cs b/extra/admin-api/Spacebar.Cdn/Services/DiscordImageResizeService.cs index 06f5d4f..2a35c40 100644 --- a/extra/admin-api/Spacebar.Cdn/Services/DiscordImageResizeService.cs +++ b/extra/admin-api/Spacebar.Cdn/Services/DiscordImageResizeService.cs @@ -12,7 +12,7 @@ public bool Animated { get; set; } = true; public bool SpacebarAllowUpscale { get; set; } = false; - public bool SpacebarOptimiseGif {get;set;} = true; + public bool SpacebarOptimiseGif { get; set; } = true; } public enum DiscordImageResizeQuality { @@ -34,7 +34,7 @@ resizeParams.Size = 4096; if (img.Max(x => Math.Max(x.Height, x.Width)) > resizeParams.Size || resizeParams.SpacebarAllowUpscale) { - Parallel.ForEach(img, new ParallelOptions(){MaxDegreeOfParallelism = 8}, frame => { + Parallel.ForEach(img, new ParallelOptions() { MaxDegreeOfParallelism = 8 }, frame => { if (resizeParams.Size.HasValue) { uint oldWidth = frame.Width, oldHeight = frame.Height; frame.Resize(resizeParams.Size.Value, resizeParams.Size.Value, pads.IsPixelArt(frame) ? FilterType.Point : FilterType.Gaussian);