summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-02-28 19:49:44 +0100
committerRory& <root@rory.gay>2026-02-28 19:49:44 +0100
commit829ada260753e0e45e0ac1db20c8f19afd2420cf (patch)
treebe4a34e9c0c45f9c65f93021427bf4c54f896b53
parentonVoiceStateUpdate: change to VoiceState.merge() for copying properties (diff)
downloadserver-ts-829ada260753e0e45e0ac1db20c8f19afd2420cf.tar.xz
nix/cdn work
-rw-r--r--.idea/workspace.xml105
-rw-r--r--extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs18
-rw-r--r--extra/admin-api/Spacebar.Cdn/Controllers/StaticAssetController.cs35
-rw-r--r--extra/admin-api/Spacebar.Cdn/Controllers/UserController.cs77
-rw-r--r--extra/admin-api/Spacebar.Cdn/Extensions/ImageController.cs30
-rw-r--r--extra/admin-api/Spacebar.Cdn/Program.cs3
-rw-r--r--extra/admin-api/Spacebar.UApi/Controllers/GuildStickerController.cs54
-rw-r--r--extra/admin-api/Spacebar.UApi/Controllers/Messages/MessagesController.cs72
-rw-r--r--extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs20
-rw-r--r--flake.nix8
-rw-r--r--nix/modules/default/config-file.nix2
-rw-r--r--nix/modules/default/cs/cdn-cs.nix65
-rw-r--r--nix/modules/default/cs/uapi.nix1
-rw-r--r--nix/modules/default/default.nix6
-rw-r--r--nix/modules/default/lib.nix7
-rw-r--r--nix/modules/default/makeServerTsService.nix1
-rw-r--r--nix/testVm/configuration.nix79
17 files changed, 408 insertions, 175 deletions
diff --git a/.idea/workspace.xml b/.idea/workspace.xml

index 0cae9b37..51f40ff2 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml
@@ -17,6 +17,13 @@ <entry key="_/home/Rory/git/spacebar/server-master" value="2oJ9u2nkEFq1qQW1NFF69ECjiYu" /> </persistenceIdMap> </component> + <component name="CopilotUserSelectedModel"> + <selectedModels> + <entry key="edit-panel" value="GPT-5 mini" /> + <entry key="chat-panel" value="Gemini 3 Pro (Preview)" /> + <entry key="agent-panel" value="Gemini 3 Pro (Preview)" /> + </selectedModels> + </component> <component name="FileTemplateManagerImpl"> <option name="RECENT_TEMPLATES"> <list> @@ -43,6 +50,9 @@ <setting file="file://$PROJECT_DIR$/node_modules/typeorm/query-builder/WhereExpressionBuilder.d.ts" root0="SKIP_INSPECTION" /> <setting file="file://$PROJECT_DIR$/node_modules/typeorm/repository/BaseEntity.d.ts" root0="SKIP_INSPECTION" /> </component> + <component name="NamedScopeManager"> + <scope name="src" pattern="!file[server]:*/||file[server]:src//*" /> + </component> <component name="ProblemsViewState"> <option name="selectedTabId" value="ProjectErrors" /> </component> @@ -55,52 +65,52 @@ <option name="hideEmptyMiddlePackages" value="true" /> <option name="showLibraryContents" value="true" /> </component> - <component name="PropertiesComponent"><![CDATA[{ - "keyToString": { - "NIXITCH_NIXPKGS_CONFIG": "/etc/nix/nixpkgs-config.nix", - "NIXITCH_NIX_CONF_DIR": "", - "NIXITCH_NIX_OTHER_STORES": "", - "NIXITCH_NIX_PATH": "/home/Rory/.nix-defexpr/channels:nixpkgs=/nix/store/wb6agba4kfsxpbnb5hzlq58vkjzvbsk6-source", - "NIXITCH_NIX_PROFILES": "/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/Rory /home/Rory/.local/state/nix/profile /nix/profile /home/Rory/.nix-profile", - "NIXITCH_NIX_REMOTE": "", - "NIXITCH_NIX_USER_PROFILE_DIR": "/nix/var/nix/profiles/per-user/Rory", - "Node.js.Server.ts.executor": "Debug", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.git.unshallow": "true", - "RunOnceActivity.typescript.service.memoryLimit.init": "true", - "git-widget-placeholder": "master", - "javascript.nodejs.core.library.configured.version": "24.11.1", - "javascript.nodejs.core.library.typings.version": "24.10.9", - "last_opened_file_path": "/home/Rory/git/spacebar/server-master/nix/modules/default", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.standard": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.standard": "", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_interpreter_path": "node", - "nodejs_package_manager_path": "npm", - "npm.Start API.executor": "Run", - "npm.Start CDN.executor": "Run", - "npm.Start Gateway.executor": "Run", - "npm.build.executor": "Run", - "npm.build:src.executor": "Run", - "npm.build:src:tsgo.executor": "Run", - "npm.build:tsgo.executor": "Run", - "npm.start.executor": "Debug", - "prettierjs.PrettierConfiguration.Package": "/home/Rory/git/spacebar/server-master/node_modules/prettier", - "ts.external.directory.path": "/home/Rory/git/spacebar/server-master/node_modules/typescript/lib" + <component name="PropertiesComponent">{ + &quot;keyToString&quot;: { + &quot;NIXITCH_NIXPKGS_CONFIG&quot;: &quot;/etc/nix/nixpkgs-config.nix&quot;, + &quot;NIXITCH_NIX_CONF_DIR&quot;: &quot;&quot;, + &quot;NIXITCH_NIX_OTHER_STORES&quot;: &quot;&quot;, + &quot;NIXITCH_NIX_PATH&quot;: &quot;/home/Rory/.nix-defexpr/channels:nixpkgs=/nix/store/wb6agba4kfsxpbnb5hzlq58vkjzvbsk6-source&quot;, + &quot;NIXITCH_NIX_PROFILES&quot;: &quot;/run/current-system/sw /nix/var/nix/profiles/default /etc/profiles/per-user/Rory /home/Rory/.local/state/nix/profile /nix/profile /home/Rory/.nix-profile&quot;, + &quot;NIXITCH_NIX_REMOTE&quot;: &quot;&quot;, + &quot;NIXITCH_NIX_USER_PROFILE_DIR&quot;: &quot;/nix/var/nix/profiles/per-user/Rory&quot;, + &quot;Node.js.Server.ts.executor&quot;: &quot;Debug&quot;, + &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, + &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, + &quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;, + &quot;git-widget-placeholder&quot;: &quot;master&quot;, + &quot;javascript.nodejs.core.library.configured.version&quot;: &quot;24.11.1&quot;, + &quot;javascript.nodejs.core.library.typings.version&quot;: &quot;24.10.9&quot;, + &quot;last_opened_file_path&quot;: &quot;/home/Rory/git/spacebar/server-master/nix/modules/default&quot;, + &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, + &quot;node.js.detected.package.standard&quot;: &quot;true&quot;, + &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, + &quot;node.js.selected.package.standard&quot;: &quot;&quot;, + &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, + &quot;nodejs_interpreter_path&quot;: &quot;node&quot;, + &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, + &quot;npm.Start API.executor&quot;: &quot;Run&quot;, + &quot;npm.Start CDN.executor&quot;: &quot;Run&quot;, + &quot;npm.Start Gateway.executor&quot;: &quot;Run&quot;, + &quot;npm.build.executor&quot;: &quot;Run&quot;, + &quot;npm.build:src.executor&quot;: &quot;Run&quot;, + &quot;npm.build:src:tsgo.executor&quot;: &quot;Run&quot;, + &quot;npm.build:tsgo.executor&quot;: &quot;Run&quot;, + &quot;npm.start.executor&quot;: &quot;Debug&quot;, + &quot;prettierjs.PrettierConfiguration.Package&quot;: &quot;/home/Rory/git/spacebar/server-master/node_modules/prettier&quot;, + &quot;ts.external.directory.path&quot;: &quot;/home/Rory/git/spacebar/server-master/node_modules/typescript/lib&quot; }, - "keyToStringList": { - "DatabaseDriversLRU": [ - "postgresql" + &quot;keyToStringList&quot;: { + &quot;DatabaseDriversLRU&quot;: [ + &quot;postgresql&quot; ], - "GitStage.ChangesTree.GroupingKeys": [ - "directory", - "module", - "repository" + &quot;GitStage.ChangesTree.GroupingKeys&quot;: [ + &quot;directory&quot;, + &quot;module&quot;, + &quot;repository&quot; ] } -}]]></component> +}</component> <component name="RecentsManager"> <key name="CopyFile.RECENT_KEYS"> <recent name="$PROJECT_DIR$/nix/modules/default" /> @@ -191,7 +201,8 @@ <workItem from="1769278836997" duration="449000" /> <workItem from="1771558270982" duration="82802000" /> <workItem from="1771818832744" duration="44139000" /> - <workItem from="1771996450006" duration="61321000" /> + <workItem from="1771996450006" duration="81615000" /> + <workItem from="1772272118101" duration="20496000" /> </task> <servers /> </component> @@ -230,4 +241,12 @@ </breakpoints> </breakpoint-manager> </component> + <component name="github-copilot-workspace"> + <instructionFileLocations> + <option value=".github/instructions" /> + </instructionFileLocations> + <promptFileLocations> + <option value=".github/prompts" /> + </promptFileLocations> + </component> </project> \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs b/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs
index 9a7ddf23..8e64100a 100644 --- a/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs +++ b/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs
@@ -7,25 +7,17 @@ using Spacebar.Interop.Cdn.Abstractions; namespace Spacebar.Cdn.Controllers; [ApiController] -public class GetImageController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ControllerBase { - [HttpGet("/avatars/{_:required}")] +public class GetImageController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ImageController { + // [HttpGet("/avatars/{_:required}")] [HttpGet("/emojis/{emoji_id:required}.{ext:required}")] [HttpGet("/stickers/{sticker_id:required}.{ext:required}")] - [HttpGet("/avatars/{user_id:required}/{avatar_hash:required}.{ext:required}")] + // [HttpGet("/avatars/{user_id:required}/{avatar_hash:required}.{ext:required}")] [HttpGet("/banners/{user_id:required}/{user_banner:required}.{ext:required}")] public async Task<IActionResult> GetImage(string? ext) { var originalKey = fs.BaseUrl + Request.Path; var cacheKey = Request.Path + Request.QueryString; - - DiscordImageResizeParams resizeParams = new() { - Size = Request.Query.ContainsKey("size") && uint.TryParse(Request.Query["size"], out uint size) ? size : null, - Quality = Request.Query.ContainsKey("quality") && Enum.TryParse<DiscordImageResizeQuality>(Request.Query["quality"], true, out var quality) ? quality : DiscordImageResizeQuality.High, - KeepAspectRatio = !Request.Query.ContainsKey("keepAspectRatio") || !bool.TryParse(Request.Query["keepAspectRatio"], out bool kar) || kar, - Passthrough = Request.Query.ContainsKey("passthrough") && bool.TryParse(Request.Query["passthrough"], out bool pt) && pt, - Animated = Request.Query.ContainsKey("animated") && bool.TryParse(Request.Query["animated"], out bool an) && an, - SpacebarAllowUpscale = Request.Query.ContainsKey("allowUpscale") && bool.TryParse(Request.Query["allowUpscale"], out bool au) && au, - SpacebarOptimiseGif = Request.Query.ContainsKey("optimiseGif") && bool.TryParse(Request.Query["optimiseGif"], out bool og) && og - }; + + DiscordImageResizeParams resizeParams = GetResizeParams(); var entry = await lfc.GetOrAdd(cacheKey, async () => { var original = await fs.GetFile(Request.Path); diff --git a/extra/admin-api/Spacebar.Cdn/Controllers/StaticAssetController.cs b/extra/admin-api/Spacebar.Cdn/Controllers/StaticAssetController.cs new file mode 100644
index 00000000..e2ad1377 --- /dev/null +++ b/extra/admin-api/Spacebar.Cdn/Controllers/StaticAssetController.cs
@@ -0,0 +1,35 @@ +using System.Collections.Immutable; +using ArcaneLibs.Extensions.Streams; +using Microsoft.AspNetCore.Mvc; +using Spacebar.AdminApi.TestClient.Services.Services; +using Spacebar.Cdn.Extensions; +using Spacebar.Interop.Cdn.Abstractions; + +namespace Spacebar.Cdn.Controllers; + +[ApiController] +public class StaticAssetController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ImageController { + private static readonly Dictionary<string, string> defaultAvatarHashMap = new() { + { "0", "4a8562cf00887030c416d3ec2d46385a" }, + { "1", "9b0bb198936784c45c72833cc426cc55" }, + { "2", "22341bdb500c7b63a93bbce957d1601e" }, + { "3", "d9977836b82058bf2f74eebd50edc095" }, + { "4", "9d6ddb4e4d899a533a8cc617011351c9" }, + { "5", "7213ab6677377974697dfdfbaf5f6a6f" }, + }; + + private static readonly Dictionary<string, string> defaultGroupDMAvatarHashMap = new() { + { "0", "3b70bb66089c60f8be5e214bf8574c9d" }, + { "1", "9581acd31832465bdeaa5385b0e919a3" }, + { "2", "a8a4727cf2dc2939bd3c657fad4463fa" }, + { "3", "2e46fe14586f8e95471c0917f56726b5" }, + { "4", "fac7e78de9753d4a37083bba74c1d9ef" }, + { "5", "4ab900144b0865430dc9be825c838faa" }, + { "6", "1276374a404452756f3c9cc2601508a5" }, + { "7", "904bf9f1b61f53ef4a3b7a893afeabe3" }, + }; + // [HttpGet("/embed/avatars/{userIndex}")] + // public async Task<IActionResult> GetDefaultUserAvatar(string userIndex) { + // + // } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Cdn/Controllers/UserController.cs b/extra/admin-api/Spacebar.Cdn/Controllers/UserController.cs new file mode 100644
index 00000000..97626609 --- /dev/null +++ b/extra/admin-api/Spacebar.Cdn/Controllers/UserController.cs
@@ -0,0 +1,77 @@ +using ArcaneLibs.Extensions.Streams; +using Microsoft.AspNetCore.Mvc; +using Spacebar.AdminApi.TestClient.Services.Services; +using Spacebar.Cdn.Extensions; +using Spacebar.Interop.Cdn.Abstractions; + +namespace Spacebar.Cdn.Controllers; + +[ApiController] +public class UserController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ImageController { + [HttpGet("/avatars/{userId}/{hash}.{ext}")] + public async Task<IActionResult> GetUserAvatar(string userId, string hash, string ext) { + var originalKey = fs.BaseUrl + Request.Path; + var cacheKey = Request.Path + Request.QueryString; + + DiscordImageResizeParams resizeParams = GetResizeParams(); + + var entry = await lfc.GetOrAdd(cacheKey, async () => { + var original = await fs.GetFile(Request.Path); + + if (Request.Query.Any()) { + using var img = await original.ToMagickImageCollectionAsync(); + dirs.Apply(img, resizeParams); + + var outStream = new MemoryStream(); + await img.WriteAsync(outStream, img.First().Format); + outStream.Position = 0; + + return new LruFileCache.Entry() { + Data = outStream.ReadToEnd().ToArray(), + MimeType = original.MimeType + }; + } + + return new LruFileCache.Entry() { + Data = original.Stream.ReadToEnd().ToArray(), + MimeType = original.MimeType + }; + }); + + // byte array with mime type result + return new FileContentResult(entry.Data, entry.MimeType); + } + [HttpGet("/banners/{userId}/{hash}.{ext}")] + public async Task<IActionResult> GetUserBanner(string userId, string hash, string ext) { + var originalKey = fs.BaseUrl + Request.Path; + var cacheKey = Request.Path + Request.QueryString; + + DiscordImageResizeParams resizeParams = GetResizeParams(); + + var entry = await lfc.GetOrAdd(cacheKey, async () => { + var original = await fs.GetFile(Request.Path); + + if (Request.Query.Any()) { + using var img = await original.ToMagickImageCollectionAsync(); + dirs.Apply(img, resizeParams); + + var outStream = new MemoryStream(); + await img.WriteAsync(outStream, img.First().Format); + outStream.Position = 0; + + return new LruFileCache.Entry() { + Data = outStream.ReadToEnd().ToArray(), + MimeType = original.MimeType + }; + } + + return new LruFileCache.Entry() { + Data = original.Stream.ReadToEnd().ToArray(), + MimeType = original.MimeType + }; + }); + + // byte array with mime type result + return new FileContentResult(entry.Data, entry.MimeType); + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Cdn/Extensions/ImageController.cs b/extra/admin-api/Spacebar.Cdn/Extensions/ImageController.cs new file mode 100644
index 00000000..21531e90 --- /dev/null +++ b/extra/admin-api/Spacebar.Cdn/Extensions/ImageController.cs
@@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Mvc; +using Spacebar.AdminApi.TestClient.Services.Services; + +namespace Spacebar.Cdn.Extensions; + +public class ImageController : ControllerBase { + protected DiscordImageResizeParams GetResizeParams() { + return new() { + Size = Request.Query.ContainsKey("size") && uint.TryParse(Request.Query["size"], out uint size) ? size : null, + Quality = Request.Query.ContainsKey("quality") && Enum.TryParse<DiscordImageResizeQuality>(Request.Query["quality"], true, out var quality) + ? quality + : DiscordImageResizeQuality.High, + KeepAspectRatio = !Request.Query.ContainsKey("keepAspectRatio") || !bool.TryParse(Request.Query["keepAspectRatio"], out bool kar) || kar, + Passthrough = Request.Query.ContainsKey("passthrough") && bool.TryParse(Request.Query["passthrough"], out bool pt) && pt, + Animated = Request.Query.ContainsKey("animated") && bool.TryParse(Request.Query["animated"], out bool an) && an, + SpacebarAllowUpscale = Request.Query.ContainsKey("allowUpscale") && bool.TryParse(Request.Query["allowUpscale"], out bool au) && au, + SpacebarOptimiseGif = Request.Query.ContainsKey("optimiseGif") && bool.TryParse(Request.Query["optimiseGif"], out bool og) && og + }; + } + + protected void SetSuccessCacheHeader() { + int cacheDuration = (int)TimeSpan.FromHours(6).TotalSeconds; + Response.Headers.CacheControl = $"public, max-age={cacheDuration}, s-maxage={cacheDuration}, immutable"; + } + + protected void SetFailureCacheHeader() { + int cacheDuration = (int)TimeSpan.FromMinutes(5).TotalSeconds; + Response.Headers.CacheControl = $"public, max-age={cacheDuration}, s-maxage={cacheDuration}, immutable"; + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Cdn/Program.cs b/extra/admin-api/Spacebar.Cdn/Program.cs
index 47b51833..61a5c07b 100644 --- a/extra/admin-api/Spacebar.Cdn/Program.cs +++ b/extra/admin-api/Spacebar.Cdn/Program.cs
@@ -10,7 +10,8 @@ if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("APPSETTINGS_P builder.Configuration.AddJsonFile(Environment.GetEnvironmentVariable("APPSETTINGS_PATH")!); // Add services to the container. -builder.Services.AddSingleton<IFileSource>(new ProxyFileSource("http://cdn.old.server.spacebar.chat")); +// builder.Services.AddSingleton<IFileSource>(new ProxyFileSource("http://cdn.old.server.spacebar.chat")); +builder.Services.AddSingleton<IFileSource>(new FilesystemFileSource(Environment.GetEnvironmentVariable("STORAGE_PATH") ?? throw new InvalidOperationException("STORAGE_PATH not set!"))); builder.Services.AddSingleton<LruFileCache>(new LruFileCache(1 * 1024 * 1024 * 1024)); builder.Services.AddSingleton<PixelArtDetectionService>(); builder.Services.AddSingleton<DiscordImageResizeService>(); diff --git a/extra/admin-api/Spacebar.UApi/Controllers/GuildStickerController.cs b/extra/admin-api/Spacebar.UApi/Controllers/GuildStickerController.cs
index 9a8ee068..6ce1fc31 100644 --- a/extra/admin-api/Spacebar.UApi/Controllers/GuildStickerController.cs +++ b/extra/admin-api/Spacebar.UApi/Controllers/GuildStickerController.cs
@@ -12,31 +12,31 @@ namespace Spacebar.UApi.Controllers; [Route("/api/v{_}/guilds/{guildId}/stickers/")] public class GuildStickerController(ILogger<MessagesController> logger, SpacebarDbContext db, SpacebarAspNetAuthenticationService authService, UApiConfiguration cfg, PermissionService permService, Config sbCfg) : ControllerBase { // TODO proper response type - [HttpPost] - public async Task<Sticker> UploadGuildSticker(string guildId, MultipartFormDataContent content) { - - var sticker = new Sticker() { - GuildId = guildId - }; - - foreach (var item in content) { - switch (item.Headers.ContentDisposition.Name.Trim('"')) { - case "name": - sticker.Name = await item.ReadAsStringAsync(); - break; - case "description": - sticker.Description = await item.ReadAsStringAsync(); - break; - case "tags": - sticker.Tags = await item.ReadAsStringAsync(); - break; - case "file": - var fileContent = await item.ReadAsStreamAsync(); - - break; - } - } - - return sticker; - } + // [HttpPost] + // public async Task<Sticker> UploadGuildSticker(string guildId, MultipartFormDataContent content) { + // + // var sticker = new Sticker() { + // GuildId = guildId + // }; + // + // foreach (var item in content) { + // switch (item.Headers.ContentDisposition.Name.Trim('"')) { + // case "name": + // sticker.Name = await item.ReadAsStringAsync(); + // break; + // case "description": + // sticker.Description = await item.ReadAsStringAsync(); + // break; + // case "tags": + // sticker.Tags = await item.ReadAsStringAsync(); + // break; + // case "file": + // var fileContent = await item.ReadAsStreamAsync(); + // + // break; + // } + // } + // + // return sticker; + // } } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.UApi/Controllers/Messages/MessagesController.cs b/extra/admin-api/Spacebar.UApi/Controllers/Messages/MessagesController.cs
index 6ef3d429..c76ef7ae 100644 --- a/extra/admin-api/Spacebar.UApi/Controllers/Messages/MessagesController.cs +++ b/extra/admin-api/Spacebar.UApi/Controllers/Messages/MessagesController.cs
@@ -11,42 +11,42 @@ namespace Spacebar.UApi.Controllers.Messages; [ApiController] [Route("/api/v{_}/channels/{channelId}/messages")] public partial class MessagesController(ILogger<MessagesController> logger, SpacebarDbContext db, SpacebarAspNetAuthenticationService authService, UApiConfiguration cfg) : ControllerBase { - [Consumes("multipart/form-data")] - [HttpPost] - public async Task CreateMessageWithAttachments(string channelId, MultipartFormDataContent formData) { - // Generic proxy doesnt work with multipart/form-data for some reason, so handle them specially - JsonObject jsonPayload = null!; - - foreach (var content in formData) - { - if (content.Headers.ContentDisposition?.Name == "payload_json") { - jsonPayload = await content.ReadFromJsonAsync<JsonObject>(); - break; - } - if (FileNameRegex().IsMatch(content.Headers.ContentDisposition?.Name ?? "")) { - - break; - } - throw new InvalidOperationException("Invalid multipart/form-data payload: missing payload_json or file attachments"); - } - - var client = new StreamingHttpClient(); - var requestMessage = new HttpRequestMessage( - new HttpMethod(Request.Method), - cfg.FallbackApiEndpoint + Request.Path + Request.QueryString - ) { - Content = new StreamContent(Request.Body) - }; - Console.WriteLine(requestMessage.RequestUri); - - var responseMessage = await client.SendUnhandledAsync(requestMessage, CancellationToken.None); - Response.StatusCode = (int)responseMessage.StatusCode; - - foreach (var header in responseMessage.Headers) Response.Headers[header.Key] = header.Value.ToArray(); - foreach (var header in responseMessage.Content.Headers) Response.Headers[header.Key] = header.Value.ToArray(); - - await responseMessage.Content.CopyToAsync(Response.Body); - } + // [Consumes("multipart/form-data")] + // [HttpPost] + // public async Task CreateMessageWithAttachments(string channelId, MultipartFormDataContent formData) { + // // Generic proxy doesnt work with multipart/form-data for some reason, so handle them specially + // JsonObject jsonPayload = null!; + // + // foreach (var content in formData) + // { + // if (content.Headers.ContentDisposition?.Name == "payload_json") { + // jsonPayload = await content.ReadFromJsonAsync<JsonObject>(); + // break; + // } + // if (FileNameRegex().IsMatch(content.Headers.ContentDisposition?.Name ?? "")) { + // + // break; + // } + // throw new InvalidOperationException("Invalid multipart/form-data payload: missing payload_json or file attachments"); + // } + // + // var client = new StreamingHttpClient(); + // var requestMessage = new HttpRequestMessage( + // new HttpMethod(Request.Method), + // cfg.FallbackApiEndpoint + Request.Path + Request.QueryString + // ) { + // Content = new StreamContent(Request.Body) + // }; + // Console.WriteLine(requestMessage.RequestUri); + // + // var responseMessage = await client.SendUnhandledAsync(requestMessage, CancellationToken.None); + // Response.StatusCode = (int)responseMessage.StatusCode; + // + // foreach (var header in responseMessage.Headers) Response.Headers[header.Key] = header.Value.ToArray(); + // foreach (var header in responseMessage.Content.Headers) Response.Headers[header.Key] = header.Value.ToArray(); + // + // await responseMessage.Content.CopyToAsync(Response.Body); + // } [GeneratedRegex(@"files\[\d+\]")] private static partial Regex FileNameRegex(); diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs
index 6247df3b..add630e5 100644 --- a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs +++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs
@@ -33,7 +33,7 @@ public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory servi } private async Task RunFsckAsync(string name, string path, IQueryable<FsckItem> items) { - int i = 0, count = await items.CountAsync(); + int i = 0, count = await items.CountAsync(), missing = 0; List<Task> tasks = []; await foreach (var item in items.AsAsyncEnumerable()) { @@ -46,12 +46,16 @@ public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory servi i++; if (!item.IsSingleSubDirFile) { - if (!await fs.FileExists(item.Path)) + if (!await fs.FileExists(item.Path)) { logger.LogWarning("{itemType} {itemId} is missing at {path}", name, item.ItemId, item.Path); + missing++; + } } else if (item.IsSingleSubDirFile && fs is FilesystemFileSource ffs) { - if(!await ffs.DirectoryExists(Path.GetDirectoryName(item.Path))) + if (!await ffs.DirectoryExists(Path.GetDirectoryName(item.Path))) { logger.LogWarning("{itemType} {itemId} is missing at {path} (directory missing)", name, item.ItemId, item.Path); + missing++; + } } else { logger.LogWarning("Unhandled case: {itemType} {itemId} -> {path} (IsSingleSubDirFile: {isSingleSubDirFile}, fstype: {fsType})", name, item.ItemId, item.Path, item.IsSingleSubDirFile, fs.GetType().Name); @@ -62,7 +66,7 @@ public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory servi } await Task.WhenAll(tasks); - logger.LogInformation("Validated {count} items for {path}.", i, path); + logger.LogInformation("Validated {count} items for {path}, {missing} missing.", i, path, missing); } #region User Assets @@ -112,17 +116,15 @@ public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory servi .OrderBy(x => x.Id) .Select(x => new FsckItem { Path = $"/stickers/{x.Id}", - ItemId = x.Id, - IsSingleSubDirFile = fs is FilesystemFileSource + ItemId = x.Id }); public IQueryable<FsckItem> EnumerateEmojiPathsAsync() => _db.Emojis .OrderBy(x => x.Id) .Select(x => new FsckItem { - Path = $"/emojis/{x.Id}/", - ItemId = x.Id, - IsSingleSubDirFile = fs is FilesystemFileSource + Path = $"/emojis/{x.Id}", + ItemId = x.Id }); #endregion diff --git a/flake.nix b/flake.nix
index 5182ef62..89a0e209 100644 --- a/flake.nix +++ b/flake.nix
@@ -16,7 +16,7 @@ self, nixpkgs, flake-utils, - pion-webrtc + pion-webrtc, }: nixpkgs.lib.recursiveUpdate ( @@ -102,6 +102,10 @@ // { nixosModules.default = import ./nix/modules/default self; nixosConfigurations.testVm = import ./nix/testVm/default.nix { inherit self nixpkgs; }; + nixosConfigurations.test = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ ]; + }; checks = let pkgs = import nixpkgs { system = "x86_64-linux"; }; @@ -125,4 +129,4 @@ inherit self nixpkgs flake-utils; } ); -} \ No newline at end of file +} diff --git a/nix/modules/default/config-file.nix b/nix/modules/default/config-file.nix
index 1b321f0a..55a558f0 100644 --- a/nix/modules/default/config-file.nix +++ b/nix/modules/default/config-file.nix
@@ -25,7 +25,7 @@ let }; } // ( - if cfg.enableAdminApi then + if cfg.adminApi.enable then { adminApi = { endpointPublic = "http${if cfg.adminApiEndpoint.useSsl then "s" else ""}://${cfg.adminApiEndpoint.host}:${toString cfg.adminApiEndpoint.publicPort}"; diff --git a/nix/modules/default/cs/cdn-cs.nix b/nix/modules/default/cs/cdn-cs.nix new file mode 100644
index 00000000..42432dab --- /dev/null +++ b/nix/modules/default/cs/cdn-cs.nix
@@ -0,0 +1,65 @@ +self: +{ + config, + lib, + pkgs, + spacebar, + ... +}: + +let + secrets = import ../secrets.nix { inherit lib config; }; + cfg = config.services.spacebarchat-server; + jsonFormat = pkgs.formats.json { }; +in +{ + imports = [ ]; + options.services.spacebarchat-server.cdnCs = lib.mkOption { + default = { }; + description = "Configuration for C# cdn."; + type = lib.types.submodule { + options = { + enable = lib.mkEnableOption "Enable experimental C# CDN."; + extraConfiguration = lib.mkOption { + type = jsonFormat.type; + default = import ./default-appsettings-json.nix; + description = "Extra appsettings.json configuration for the gateway offload daemon."; + }; + }; + }; + }; + + config = lib.mkIf cfg.cdnCs.enable ( + let + makeServerTsService = import ../makeServerTsService.nix { inherit cfg lib secrets; }; + in + { + assertions = [ + (import ./assert-has-connection-string.nix "Admin API" cfg.adminApi.extraConfiguration) + ]; + + systemd.services.spacebar-cdn = makeServerTsService { + description = "Spacebar Server - CDN (C#)"; + environment = builtins.mapAttrs (_: val: builtins.toString val) ( + { + # things we set by default... + EVENT_TRANSMISSION = "unix"; + EVENT_SOCKET_PATH = "/run/spacebar/"; + } + // cfg.extraEnvironment + // { + # things we force... + # CONFIG_PATH = configFile; + CONFIG_READONLY = 1; + ASPNETCORE_URLS = "http://0.0.0.0:${toString cfg.cdnEndpoint.localPort}"; + STORAGE_LOCATION = cfg.cdnPath; + APPSETTINGS_PATH = jsonFormat.generate "appsettings.spacebar-cdn.json" (lib.recursiveUpdate (import ./default-appsettings-json.nix) cfg.cdnCs.extraConfiguration); + } + ); + serviceConfig = { + ExecStart = "${self.packages.${pkgs.stdenv.hostPlatform.system}.Spacebar-AdminApi}/bin/Spacebar.AdminApi"; + }; + }; + } + ); +} diff --git a/nix/modules/default/cs/uapi.nix b/nix/modules/default/cs/uapi.nix
index 9cd76b62..00caa214 100644 --- a/nix/modules/default/cs/uapi.nix +++ b/nix/modules/default/cs/uapi.nix
@@ -45,6 +45,7 @@ in systemd.services.spacebar-uapi = makeServerTsService { description = "Spacebar Server - C# API overlay"; + # after = [ "spacebar-api.service" ]; environment = builtins.mapAttrs (_: val: builtins.toString val) ( { # things we set by default... diff --git a/nix/modules/default/default.nix b/nix/modules/default/default.nix
index fb84c8f6..11899aeb 100644 --- a/nix/modules/default/default.nix +++ b/nix/modules/default/default.nix
@@ -18,6 +18,7 @@ in ./integration-nginx.nix ./users.nix (import ./pion-sfu.nix self) + (import ./cs/cdn-cs.nix self) (import ./cs/gateway-offload-cs.nix self) (import ./cs/admin-api.nix self) (import ./cs/uapi.nix self) @@ -28,8 +29,6 @@ in in { enable = lib.mkEnableOption "Spacebar server"; - enableAdminApi = lib.mkEnableOption "Spacebar server Admin API"; - enableCdnCs = lib.mkEnableOption "Spacebar's experimental CDN rewrite"; package = lib.mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "spacebar-server" { default = "default"; }; databaseFile = lib.mkOption { type = lib.types.nullOr lib.types.path; @@ -129,6 +128,7 @@ in systemd.services.spacebar-gateway = makeServerTsService { description = "Spacebar Server - Gateway"; + # after = [ "spacebar-api.service" ]; environment = builtins.mapAttrs (_: val: builtins.toString val) ( { # things we set by default... @@ -150,7 +150,7 @@ in }; }; - systemd.services.spacebar-cdn = lib.mkIf (!cfg.enableCdnCs) (makeServerTsService { + systemd.services.spacebar-cdn = lib.mkIf (!cfg.cdnCs.enable) (makeServerTsService { description = "Spacebar Server - CDN"; environment = builtins.mapAttrs (_: val: builtins.toString val) ( { diff --git a/nix/modules/default/lib.nix b/nix/modules/default/lib.nix
index 886b78fc..cd7a7fb4 100644 --- a/nix/modules/default/lib.nix +++ b/nix/modules/default/lib.nix
@@ -11,4 +11,11 @@ else 80; }; + + mkEndpointRaw = domain: port: publicPort: ssl: { + host = domain; + localPort = port; + useSsl = ssl; + publicPort = publicPort; + }; } diff --git a/nix/modules/default/makeServerTsService.nix b/nix/modules/default/makeServerTsService.nix
index ae54e3f7..7a611251 100644 --- a/nix/modules/default/makeServerTsService.nix +++ b/nix/modules/default/makeServerTsService.nix
@@ -75,6 +75,7 @@ lib.recursiveUpdate StateDirectoryMode = "0750"; RuntimeDirectory = "spacebar"; RuntimeDirectoryMode = "0750"; + RuntimeDirectoryPreserve = "yes"; ReadWritePaths = [ cfg.cdnPath ]; NoExecPaths = [ cfg.cdnPath ]; diff --git a/nix/testVm/configuration.nix b/nix/testVm/configuration.nix
index db31cbe7..7d33dc71 100644 --- a/nix/testVm/configuration.nix +++ b/nix/testVm/configuration.nix
@@ -1,4 +1,9 @@ -{ pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let nginxTestSigning = { addSSL = true; @@ -11,72 +16,66 @@ let in { networking.hostName = "sbtest"; - services.nginx.virtualHosts."sb.localhost" = nginxTestSigning; - services.nginx.virtualHosts."api.sb.localhost" = nginxTestSigning; - services.nginx.virtualHosts."gw.sb.localhost" = nginxTestSigning; - services.nginx.virtualHosts."cdn.sb.localhost" = nginxTestSigning; - services.nginx.virtualHosts."webrtc.sb.localhost" = nginxTestSigning; - services.nginx.virtualHosts."admin.sb.localhost" = nginxTestSigning; + services.nginx.virtualHosts.${config.services.spacebarchat-server.serverName} = nginxTestSigning; + services.nginx.virtualHosts.${config.services.spacebarchat-server.adminApiEndpoint.host} = nginxTestSigning; + services.nginx.virtualHosts.${config.services.spacebarchat-server.apiEndpoint.host} = nginxTestSigning; + services.nginx.virtualHosts.${config.services.spacebarchat-server.cdnEndpoint.host} = nginxTestSigning; + services.nginx.virtualHosts.${config.services.spacebarchat-server.gatewayEndpoint.host} = nginxTestSigning; + services.nginx.virtualHosts.${config.services.spacebarchat-server.webrtcEndpoint.host} = nginxTestSigning; services.spacebarchat-server = let + sbLib = import ../modules/default/lib.nix; csConnectionString = "Host=127.0.0.1; Username=postgres; Password=postgres; Database=spacebar; Port=5432; Include Error Detail=true; Maximum Pool Size=1000; Command Timeout=6000; Timeout=600;"; cfg = { enable = true; - apiEndpoint = { - useSsl = false; - host = "api.sb.localhost"; - localPort = 3001; - publicPort = 8080; - }; - gatewayEndpoint = { - useSsl = false; - host = "gw.sb.localhost"; - localPort = 3002; - publicPort = 8080; - }; - cdnEndpoint = { - useSsl = false; - host = "cdn.sb.localhost"; - localPort = 3003; - publicPort = 8080; - }; - adminApiEndpoint = { - useSsl = false; - host = "admin.sb.localhost"; - localPort = 3004; - publicPort = 8080; - }; - webrtcEndpoint = { - useSsl = false; - host = "voice.sb.localhost"; - localPort = 3005; - publicPort = 8080; - }; + apiEndpoint = sbLib.mkEndpointRaw "api.sb.localhost" 3001 8080 false; + gatewayEndpoint = sbLib.mkEndpointRaw "gw.sb.localhost" 3002 8080 false; + cdnEndpoint = sbLib.mkEndpointRaw "cdn.sb.localhost" 3003 8080 false; + adminApiEndpoint = sbLib.mkEndpointRaw "admin.sb.localhost" 3004 8080 false; + webrtcEndpoint = sbLib.mkEndpointRaw "voice.sb.localhost" 3005 8080 false; nginx.enable = true; serverName = "sb.localhost"; + settings = { + security = { + requestSignature = "meow"; + cdnSignatureDuration = "5m"; + cdnSignatureIncludeIp = true; + cdnSignatureIncludeUserAgent = false; + cdnSignatureKey = "meow"; + }; + }; + gatewayOffload = { enable = true; enableGuildSync = true; extraConfiguration.ConnectionStrings.Spacebar = csConnectionString; }; + adminApi = { enable = true; extraConfiguration.ConnectionStrings.Spacebar = csConnectionString; }; + + cdnCs = { + enable = false; + extraConfiguration.ConnectionStrings.Spacebar = csConnectionString; + }; + uApi = { enable = true; extraConfiguration.ConnectionStrings.Spacebar = csConnectionString; }; + pion-sfu = { enable = true; publicIp = "127.0.0.1"; }; + extraEnvironment = { DATABASE = "postgres://postgres:postgres@127.0.0.1/spacebar"; - #WEBRTC_PORT_RANGE=60000-61000; - #PUBLIC_IP=216.230.228.60; - LOG_REQUESTS = "-200,204,304"; +# LOG_REQUESTS = "-200,204,304"; + LOG_REQUESTS = "-"; LOG_VALIDATION_ERRORS = true; #DB_LOGGING=true; #LOG_GATEWAY_TRACES=true;