summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-01-26 10:20:52 +0100
committerRory& <root@rory.gay>2026-01-26 10:20:52 +0100
commit56d8aa2326382633be81a014530b11f14aaa2a62 (patch)
treedc66d4f2140655f196222dea0647719b34c8ea6d
parentMake bot invites configurable (diff)
downloadserver-ts-56d8aa2326382633be81a014530b11f14aaa2a62.tar.xz
C# dependency updates, CDN-CS work
-rw-r--r--.idea/workspace.xml20
-rw-r--r--extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml4
-rw-r--r--extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml6
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs41
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs (renamed from extra/admin-api/Spacebar.Cdn/Services/IFileSource.cs)24
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/LruFileCache.cs35
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs (renamed from extra/admin-api/Spacebar.Cdn/Services/ProxyFileSource.cs)9
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj12
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/deps.json1
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj2
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj4
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Db/deps.json185
-rw-r--r--extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj19
-rw-r--r--extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs4
-rw-r--r--extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs5
-rw-r--r--extra/admin-api/Spacebar.Cdn/Extensions/FileSourceExtensions.cs25
-rw-r--r--extra/admin-api/Spacebar.Cdn/Program.cs11
-rw-r--r--extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json10
-rw-r--r--extra/admin-api/Spacebar.Cdn/Services/CdnStorageRebuildService.cs5
-rw-r--r--extra/admin-api/Spacebar.Cdn/Services/LruFileCache.cs58
-rw-r--r--extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj5
-rw-r--r--extra/admin-api/Spacebar.Cdn/StreamingHttpClient.cs304
-rw-r--r--extra/admin-api/SpacebarAdminAPI.sln30
-rw-r--r--extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj2
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj2
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor1
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor1
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor1
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor1
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor1
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor1
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs304
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj4
-rw-r--r--extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj2
-rw-r--r--extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs117
-rw-r--r--extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs21
-rw-r--r--extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Properties/launchSettings.json19
-rw-r--r--extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj19
-rw-r--r--extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.Development.json8
-rw-r--r--extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.json8
-rw-r--r--extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json1
-rw-r--r--extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json196
-rw-r--r--extra/admin-api/nuget.config4
-rw-r--r--extra/admin-api/outputs.nix12
-rw-r--r--nix/modules/default/cdn-cs.nix333
-rw-r--r--nix/modules/default/default.nix57
46 files changed, 861 insertions, 1073 deletions
diff --git a/.idea/workspace.xml b/.idea/workspace.xml

index 741d94a6..b9d89f05 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml
@@ -12,6 +12,11 @@ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="LAST_RESOLUTION" value="IGNORE" /> </component> + <component name="CopilotPersistence"> + <persistenceIdMap> + <entry key="_/home/Rory/git/spacebar/server-master" value="2oJ9u2nkEFq1qQW1NFF69ECjiYu" /> + </persistenceIdMap> + </component> <component name="FileTemplateManagerImpl"> <option name="RECENT_TEMPLATES"> <list> @@ -62,10 +67,11 @@ &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.4&quot;, - &quot;last_opened_file_path&quot;: &quot;/home/Rory/git/spacebar/server-master/src/util/migration/postgres&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;, @@ -80,7 +86,6 @@ &quot;npm.build:src.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;settings.editor.selected.configurable&quot;: &quot;com.github.copilot.settings.customInstructions.CopilotInstructionsConfigurable&quot;, &quot;ts.external.directory.path&quot;: &quot;/home/Rory/git/spacebar/server-master/node_modules/typescript/lib&quot; }, &quot;keyToStringList&quot;: { @@ -96,11 +101,11 @@ }</component> <component name="RecentsManager"> <key name="CopyFile.RECENT_KEYS"> + <recent name="$PROJECT_DIR$/nix/modules/default" /> + <recent name="$PROJECT_DIR$/src/api/routes/warp" /> <recent name="$PROJECT_DIR$/src/util/migration/postgres" /> <recent name="$PROJECT_DIR$/src/schemas/api/users" /> <recent name="$PROJECT_DIR$/src/util/entities" /> - <recent name="$PROJECT_DIR$/src/gateway/opcodes" /> - <recent name="$PROJECT_DIR$/src/api/routes/users/@me/billing" /> </key> <key name="MoveFile.RECENT_KEYS"> <recent name="$PROJECT_DIR$/src/schemas/api/guilds" /> @@ -143,7 +148,7 @@ <component name="SharedIndexes"> <attachedChunks> <set> - <option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-WS-252.27397.92" /> + <option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-WS-253.29346.242" /> </set> </attachedChunks> </component> @@ -177,6 +182,9 @@ <workItem from="1765861713706" duration="18045000" /> <workItem from="1766715967914" duration="40995000" /> <workItem from="1766823293461" duration="11580000" /> + <workItem from="1769106282329" duration="7551000" /> + <workItem from="1769177782123" duration="8132000" /> + <workItem from="1769278836997" duration="449000" /> </task> <servers /> </component> diff --git a/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml b/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml
index 7b08163c..0e04cc86 100644 --- a/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml +++ b/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/indexLayout.xml
@@ -1,7 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="UserContentModel"> - <attachedFolders /> + <attachedFolders> + <Path>../../nix</Path> + </attachedFolders> <explicitIncludes /> <explicitExcludes /> </component> diff --git a/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml b/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml new file mode 100644
index 00000000..0f3b7c20 --- /dev/null +++ b/extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml
@@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="SqlDialectMappings"> + <file url="file://$PROJECT_DIR$/Spacebar.AdminApi/Controllers/UserController.cs" dialect="PostgreSQL" /> + </component> +</project> \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs new file mode 100644
index 00000000..a3199071 --- /dev/null +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/FilesystemFileSource.cs
@@ -0,0 +1,41 @@ +using ArcaneLibs; + +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<FileInfo> 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" + }; + }); + + return new() { + Stream = new MemoryStream(res.Data), + MimeType = res.MimeType + }; + } + + public Task<bool> FileExists(string path, CancellationToken? cancellationToken = null) { + return Task.FromResult(File.Exists(Path.Combine(baseUrl, path))); + } + + // private string GetMimeType(Stream stream) + // { + // using var mic = new MagickImageCollection(stream); + // return Mimes.GetMime(mic.First().Format); + // } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Cdn/Services/IFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs
index e1032872..2ffa9acf 100644 --- a/extra/admin-api/Spacebar.Cdn/Services/IFileSource.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/IFileSource.cs
@@ -1,10 +1,9 @@ -using ImageMagick; - -namespace Spacebar.AdminApi.TestClient.Services.Services; +namespace Spacebar.Interop.Cdn.Abstractions; public interface IFileSource { public string BaseUrl { get; } public Task<FileInfo> GetFile(string path, CancellationToken? cancellationToken = null); + public Task<bool> FileExists(string path, CancellationToken? cancellationToken = null); } public class FileInfo : IDisposable, IAsyncDisposable { @@ -28,23 +27,4 @@ public class FileInfo : IDisposable, IAsyncDisposable { await DisposeAsyncCore(); GC.SuppressFinalize(this); } - - public async Task<MagickImageCollection> ToMagickImageCollectionAsync() { - var ms = new MemoryStream(); - Stream.Position = 0; - await Stream.CopyToAsync(ms); - ms.Position = 0; - var img = MimeType switch { - "image/apng" => new MagickImageCollection(ms, MagickFormat.APng), - _ => new MagickImageCollection(ms) - }; - - // if (img.First().Format == MagickFormat.Png) { - // img.Dispose(); - // ms.Position = 0; - // img = new MagickImageCollection(ms, MagickFormat.APng); - // } - - return img; - } } \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/LruFileCache.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/LruFileCache.cs new file mode 100644
index 00000000..3bd4ec09 --- /dev/null +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/LruFileCache.cs
@@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Spacebar.Interop.Cdn.Abstractions; + +public class LruFileCache(int maxSizeBytes) { + private readonly Dictionary<string, Entry> _entries = new(); + + public async Task<Entry?> GetOrAdd(string key, Func<Task<Entry>> factory) { + if (_entries.TryGetValue(key, out var entry)) { + entry.LastAccessed = DateTimeOffset.UtcNow; + return entry; + } + + entry = await factory(); + if (entry.Data.Length > 0 && entry.Data.Length <= maxSizeBytes) + _entries[key] = entry; + + if (_entries.Sum(kv => kv.Value.Data.Length) > maxSizeBytes) { + var oldestKey = _entries.OrderBy(kv => kv.Value.LastAccessed).First().Key; + _entries.Remove(oldestKey); + } + + return entry; + } + + public class Entry { + public DateTimeOffset LastAccessed { get; set; } + public byte[] Data { get; set; } + public string MimeType { get; set; } + } +} + diff --git a/extra/admin-api/Spacebar.Cdn/Services/ProxyFileSource.cs b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs
index bc6ab2ae..af7cecf2 100644 --- a/extra/admin-api/Spacebar.Cdn/Services/ProxyFileSource.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/ProxyFileSource.cs
@@ -1,4 +1,6 @@ -namespace Spacebar.AdminApi.TestClient.Services.Services; +using ArcaneLibs; + +namespace Spacebar.Interop.Cdn.Abstractions; public class ProxyFileSource(string baseUrl) : IFileSource { private static LruFileCache _cache = new(100 * 1024 * 1024); // 100 MB @@ -26,4 +28,9 @@ public class ProxyFileSource(string baseUrl) : IFileSource { MimeType = res.MimeType }; } + + public async Task<bool> FileExists(string path, CancellationToken? cancellationToken = null) { + var res = await _httpClient.SendUnhandledAsync(new(HttpMethod.Head, path), cancellationToken); + return res.IsSuccessStatusCode; + } } \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj new file mode 100644
index 00000000..daf6beb6 --- /dev/null +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj
@@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net10.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260126-091403" /> + </ItemGroup> +</Project> diff --git a/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/deps.json b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/deps.json new file mode 100644
index 00000000..0637a088 --- /dev/null +++ b/extra/admin-api/Interop/Spacebar.Interop.Cdn.Abstractions/deps.json
@@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj b/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj
index ea3a4079..7b463eaa 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj +++ b/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/Spacebar.Interop.Replication.RabbitMq.csproj
@@ -11,7 +11,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251207-164820" /> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260126-091403" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" /> <PackageReference Include="RabbitMQ.Client" Version="7.2.0" /> </ItemGroup> diff --git a/extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj b/extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj
index 320274c0..3a588b36 100644 --- a/extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj +++ b/extra/admin-api/Models/Spacebar.Models.Db/Spacebar.Models.Db.csproj
@@ -8,7 +8,9 @@ <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.2"/> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" /> + + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/extra/admin-api/Models/Spacebar.Models.Db/deps.json b/extra/admin-api/Models/Spacebar.Models.Db/deps.json
index 9088516a..c4a9edf5 100644 --- a/extra/admin-api/Models/Spacebar.Models.Db/deps.json +++ b/extra/admin-api/Models/Spacebar.Models.Db/deps.json
@@ -5,39 +5,14 @@ "hash": "sha256-EXvojddPu+9JKgOG9NSQgUTfWq1RpOYw7adxDPKDJ6o=" }, { - "pname": "Microsoft.Build", - "version": "17.7.2", - "hash": "sha256-k35nFdPxC8t0zAltVSmAJtsepp/ubNIjPOsJ6k8jSqM=" - }, - { "pname": "Microsoft.Build.Framework", - "version": "17.14.28", - "hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM=" + "version": "17.11.31", + "hash": "sha256-YS4oASrmC5dmZrx5JPS7SfKmUpIJErlUpVDsU3VrfFE=" }, { "pname": "Microsoft.Build.Framework", - "version": "17.7.2", - "hash": "sha256-fNWmVQYFTJDveAGmxEdNqJRAczV6+Ep8RA8clKBJFqw=" - }, - { - "pname": "Microsoft.Build.Tasks.Core", - "version": "17.14.28", - "hash": "sha256-M9zRXYijH2HtLlRXbrUK1a1LQ9zkT+DC9ZmMiiVZwv0=" - }, - { - "pname": "Microsoft.Build.Tasks.Core", - "version": "17.7.2", - "hash": "sha256-OrV/qWgZHzGlNUmaSfX5wDBcmg1aQeF3/OUHpSH+uZU=" - }, - { - "pname": "Microsoft.Build.Utilities.Core", - "version": "17.14.28", - "hash": "sha256-VFfO+UpyTpw2X/qiCCOCYzvMLuu7B+XVSSpJZQLkPzU=" - }, - { - "pname": "Microsoft.Build.Utilities.Core", - "version": "17.7.2", - "hash": "sha256-oatF0KfuP1nb4+OLNKg2/R/ZLO4EiACaO5leaxMEY4A=" + "version": "18.0.2", + "hash": "sha256-fO31KAdDs2J0RUYD1ov9UB3ucsbALan7K0YdWW+yg7A=" }, { "pname": "Microsoft.CodeAnalysis.Analyzers", @@ -46,28 +21,28 @@ }, { "pname": "Microsoft.CodeAnalysis.Common", - "version": "4.14.0", - "hash": "sha256-ne/zxH3GqoGB4OemnE8oJElG5mai+/67ASaKqwmL2BE=" + "version": "5.0.0", + "hash": "sha256-g4ALvBSNyHEmSb1l5TFtWW7zEkiRmhqLx4XWZu9sr2U=" }, { "pname": "Microsoft.CodeAnalysis.CSharp", - "version": "4.14.0", - "hash": "sha256-5Mzj3XkYYLkwDWh17r1NEXSbXwwWYQPiOmkSMlgo1JY=" + "version": "5.0.0", + "hash": "sha256-ctBCkQGFpH/xT5rRE3xibu9YxPD108RuC4a4Z25koG8=" }, { "pname": "Microsoft.CodeAnalysis.CSharp.Workspaces", - "version": "4.14.0", - "hash": "sha256-aNbV1a0yYBs0fpQawG6LXcbyoE8en+YFSpV5vcYE4J4=" + "version": "5.0.0", + "hash": "sha256-yWVcLt/f2CouOfFy966glGdtSFy+RcgrU1dd9UtlL/Q=" }, { "pname": "Microsoft.CodeAnalysis.Workspaces.Common", - "version": "4.14.0", - "hash": "sha256-0YfeaJe01WBUm9avy4a8FacQJXA1NkpnDpiXu4yz88I=" + "version": "5.0.0", + "hash": "sha256-Bir5e1gEhgQQ6upQmVKQHAKLRfenAu60DAzNupNnZsQ=" }, { "pname": "Microsoft.CodeAnalysis.Workspaces.MSBuild", - "version": "4.14.0", - "hash": "sha256-5SJfpRqzqCK0UbkmAaJpA/r1XJb0YAriMMeQHYC4d+o=" + "version": "5.0.0", + "hash": "sha256-+58+iqTayTiE0pDaog1U8mjaDA8bNNDLA8gjCQZZudo=" }, { "pname": "Microsoft.EntityFrameworkCore", @@ -86,18 +61,13 @@ }, { "pname": "Microsoft.EntityFrameworkCore.Design", - "version": "10.0.1", - "hash": "sha256-GGNZIGNEMhSGaMRFkRN4bOuCUBs5YVnX8klXarm319U=" - }, - { - "pname": "Microsoft.EntityFrameworkCore.Relational", - "version": "10.0.0", - "hash": "sha256-vOP2CE5YA551BlpbOuIy6RuAiAEPEpCVS1cEE33/zN4=" + "version": "10.0.2", + "hash": "sha256-bTShsGux0y/49PIIMb/4ZX3x5+rPacvT5/NcooNCI1Y=" }, { "pname": "Microsoft.EntityFrameworkCore.Relational", - "version": "10.0.1", - "hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY=" + "version": "10.0.2", + "hash": "sha256-Y4jPpoYhKizg5wF6QfkBX4sYlE2FU1bYhfoDN3xkhKM=" }, { "pname": "Microsoft.Extensions.Caching.Abstractions", @@ -106,18 +76,13 @@ }, { "pname": "Microsoft.Extensions.Caching.Memory", - "version": "10.0.1", - "hash": "sha256-Qb7xK6VEZDas0lJFaW1suKdFjtkSYwLHHxkQEfWIU2A=" - }, - { - "pname": "Microsoft.Extensions.Caching.Memory", "version": "10.0.2", "hash": "sha256-sRUF7DM0s1yzZnfjM/hF9A/IysE6Er23gZ6jST+RWh0=" }, { "pname": "Microsoft.Extensions.Configuration.Abstractions", - "version": "10.0.1", - "hash": "sha256-s4PDp+vtzdxKIxnOT3+dDRoTDopyl8kqmmw4KDnkOtQ=" + "version": "10.0.2", + "hash": "sha256-P+0kaDGO+xB9KxF9eWHDJ4hzi05sUGM/uMNEX5NdBTE=" }, { "pname": "Microsoft.Extensions.DependencyInjection", @@ -136,13 +101,8 @@ }, { "pname": "Microsoft.Extensions.DependencyModel", - "version": "10.0.1", - "hash": "sha256-XIj2jEURe25YA4RhBSuCqQpic0YP+TZaO/dbBPCjad8=" - }, - { - "pname": "Microsoft.Extensions.Logging", - "version": "10.0.1", - "hash": "sha256-zuLP3SIpCToMOlIPOEv3Kq8y/minecd8k8GSkxFo13E=" + "version": "10.0.2", + "hash": "sha256-w/dGIjtZiGH+KW3969BPOdQpQEV+WB7RPTa2MK2DavE=" }, { "pname": "Microsoft.Extensions.Logging", @@ -181,11 +141,6 @@ }, { "pname": "Microsoft.Extensions.Primitives", - "version": "10.0.1", - "hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc=" - }, - { - "pname": "Microsoft.Extensions.Primitives", "version": "10.0.2", "hash": "sha256-8Ccrjjv9cFVf9RyCc7GS/Byt8+DXdSNea0UX3A5BEdA=" }, @@ -195,14 +150,9 @@ "hash": "sha256-ZNLusK1CRuq5BZYZMDqaz04PIKScE2Z7sS2tehU7EJs=" }, { - "pname": "Microsoft.NET.StringTools", - "version": "17.14.28", - "hash": "sha256-UzREyvDxkiOQ4cEOQ5UCjkwXGrldIDCcbefECTPGjXI=" - }, - { - "pname": "Microsoft.NET.StringTools", - "version": "17.7.2", - "hash": "sha256-hQE07TCgcQuyu9ZHVq2gPDb0+xe8ECJUdrgh17bJP4o=" + "pname": "Microsoft.VisualStudio.SolutionPersistence", + "version": "1.0.52", + "hash": "sha256-KZGPtOXe6Hv8RrkcsgoLKTRyaCScIpQEa2NhNB3iOXw=" }, { "pname": "Mono.TextTemplating", @@ -230,16 +180,6 @@ "hash": "sha256-uPetUFZyHfxjScu5x4agjk9pIhbCkt5rG4Axj25npcQ=" }, { - "pname": "System.CodeDom", - "version": "7.0.0", - "hash": "sha256-7IPt39cY+0j0ZcRr/J45xPtEjnSXdUJ/5ai3ebaYQiE=" - }, - { - "pname": "System.CodeDom", - "version": "9.0.0", - "hash": "sha256-578lcBgswW0eM16r0EnJzfGodPx86RxxFoZHc2PSzsw=" - }, - { "pname": "System.Composition", "version": "9.0.0", "hash": "sha256-FehOkQ2u1p8mQ0/wn3cZ+24HjhTLdck8VZYWA1CcgbM=" @@ -268,80 +208,5 @@ "pname": "System.Composition.TypedParts", "version": "9.0.0", "hash": "sha256-F5fpTUs3Rr7yP/NyIzr+Xn5NdTXXp8rrjBnF9UBBUog=" - }, - { - "pname": "System.Configuration.ConfigurationManager", - "version": "7.0.0", - "hash": "sha256-SgBexTTjRn23uuXvkzO0mz0qOfA23MiS4Wv+qepMLZE=" - }, - { - "pname": "System.Configuration.ConfigurationManager", - "version": "9.0.0", - "hash": "sha256-+pLnTC0YDP6Kjw5DVBiFrV/Q3x5is/+6N6vAtjvhVWk=" - }, - { - "pname": "System.Diagnostics.EventLog", - "version": "9.0.0", - "hash": "sha256-tPvt6yoAp56sK/fe+/ei8M65eavY2UUhRnbrREj/Ems=" - }, - { - "pname": "System.Formats.Nrbf", - "version": "9.0.0", - "hash": "sha256-c4qf6CocQUZB0ySGQd8s15PXY7xfrjQqMGXxkwytKyw=" - }, - { - "pname": "System.Reflection.MetadataLoadContext", - "version": "7.0.0", - "hash": "sha256-VYl6SFD130K9Aw4eJH16ApJ9Sau4Xu0dcxEip2veuTI=" - }, - { - "pname": "System.Resources.Extensions", - "version": "9.0.0", - "hash": "sha256-y2gLEMuAy6QfEyNJxABC/ayMWGnwlpX735jsUQLktho=" - }, - { - "pname": "System.Security.Cryptography.Pkcs", - "version": "7.0.0", - "hash": "sha256-3J3vL9hcKSuZjT2GKappa2A9p2xJm1nH2asTNAl8ZCA=" - }, - { - "pname": "System.Security.Cryptography.Pkcs", - "version": "7.0.2", - "hash": "sha256-qS5Z/Yo8J+f3ExVX5Qkcpj1Z57oUZqz5rWa1h5bVpl8=" - }, - { - "pname": "System.Security.Cryptography.Pkcs", - "version": "9.0.0", - "hash": "sha256-AjG14mGeSc2Ka4QSelGBM1LrGBW3VJX60lnihKyJjGY=" - }, - { - "pname": "System.Security.Cryptography.ProtectedData", - "version": "9.0.0", - "hash": "sha256-gPgPU7k/InTqmXoRzQfUMEKL3QuTnOKowFqmXTnWaBQ=" - }, - { - "pname": "System.Security.Cryptography.Xml", - "version": "7.0.1", - "hash": "sha256-CH8+JVC8LyCSW75/6ZQ7ecMbSOAE1c16z4dG8JTp01w=" - }, - { - "pname": "System.Security.Cryptography.Xml", - "version": "9.0.0", - "hash": "sha256-SQJWwAFrJUddEU6JiZB52FM9tGjRlJAYH8oYVzG5IJU=" - }, - { - "pname": "System.Security.Permissions", - "version": "7.0.0", - "hash": "sha256-DOFoX+AKRmrkllykHheR8FfUXYx/Ph+I/HYuReQydXI=" - }, - { - "pname": "System.Security.Permissions", - "version": "9.0.0", - "hash": "sha256-BFrA9ottmQtLIAiKiGRbfSUpzNJwuaOCeFRDN4Z0ku0=" - }, - { - "pname": "System.Windows.Extensions", - "version": "9.0.0", - "hash": "sha256-RErD+Ju15qtnwdwB7E0SjjJGAnhXwJyC7UPcl24Z3Vs=" } ] diff --git a/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj
index 53b438f3..6c1b2e18 100644 --- a/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj +++ b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj
@@ -4,21 +4,22 @@ <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251207-164820" /> - <PackageReference Include="ArcaneLibs.StringNormalisation" Version="1.0.0-preview.20251207-164820" /> - <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" /> - <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" /> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260126-091403" /> + <PackageReference Include="ArcaneLibs.StringNormalisation" Version="1.0.1-preview.20260126-091403" /> + <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2"/> + <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0"/> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\Interop\Spacebar.Interop.Replication.Abstractions\Spacebar.Interop.Replication.Abstractions.csproj" /> - <ProjectReference Include="..\Interop\Spacebar.Interop.Replication.UnixSocket\Spacebar.Interop.Replication.UnixSocket.csproj" /> - <ProjectReference Include="..\Models\Spacebar.Models.AdminApi\Spacebar.Models.AdminApi.csproj" /> - <ProjectReference Include="..\Models\Spacebar.Models.Config\Spacebar.Models.Config.csproj" /> - <ProjectReference Include="..\Models\Spacebar.Models.Db\Spacebar.Models.Db.csproj" /> + <ProjectReference Include="..\Interop\Spacebar.Interop.Replication.Abstractions\Spacebar.Interop.Replication.Abstractions.csproj"/> + <ProjectReference Include="..\Interop\Spacebar.Interop.Replication.UnixSocket\Spacebar.Interop.Replication.UnixSocket.csproj"/> + <ProjectReference Include="..\Models\Spacebar.Models.AdminApi\Spacebar.Models.AdminApi.csproj"/> + <ProjectReference Include="..\Models\Spacebar.Models.Config\Spacebar.Models.Config.csproj"/> + <ProjectReference Include="..\Models\Spacebar.Models.Db\Spacebar.Models.Db.csproj"/> </ItemGroup> </Project> diff --git a/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs b/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs
index 13b5da68..2f014926 100644 --- a/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs +++ b/extra/admin-api/Spacebar.Cdn/Controllers/GetImageController.cs
@@ -1,8 +1,10 @@ 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.AdminApi.TestClient.Services.Controllers; +namespace Spacebar.Cdn.Controllers; [ApiController] public class GetImageController(LruFileCache lfc, IFileSource fs, DiscordImageResizeService dirs) : ControllerBase { diff --git a/extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs b/extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs
index 4063e79c..dd8d4255 100644 --- a/extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs +++ b/extra/admin-api/Spacebar.Cdn/Controllers/Internal/GetImageController.cs
@@ -1,8 +1,11 @@ +using ArcaneLibs.Collections; using ImageMagick; using Microsoft.AspNetCore.Mvc; using Spacebar.AdminApi.TestClient.Services.Services; +using Spacebar.Cdn.Extensions; +using Spacebar.Interop.Cdn.Abstractions; -namespace Spacebar.AdminApi.TestClient.Services.Controllers; +namespace Spacebar.Cdn.Controllers.Internal; [ApiController] public class IsPixelArtController(LruFileCache lfc, IFileSource fs, PixelArtDetectionService pads, DiscordImageResizeService dirs) : ControllerBase { diff --git a/extra/admin-api/Spacebar.Cdn/Extensions/FileSourceExtensions.cs b/extra/admin-api/Spacebar.Cdn/Extensions/FileSourceExtensions.cs new file mode 100644
index 00000000..218ba6a2 --- /dev/null +++ b/extra/admin-api/Spacebar.Cdn/Extensions/FileSourceExtensions.cs
@@ -0,0 +1,25 @@ +using ImageMagick; +using FileInfo = Spacebar.Interop.Cdn.Abstractions.FileInfo; + +namespace Spacebar.Cdn.Extensions; + +public static class FileSourceExtensions { + public static async Task<MagickImageCollection> ToMagickImageCollectionAsync(this FileInfo fileInfo) { + var ms = new MemoryStream(); + fileInfo.Stream.Position = 0; + await fileInfo.Stream.CopyToAsync(ms); + ms.Position = 0; + var img = fileInfo.MimeType switch { + "image/apng" => new MagickImageCollection(ms, MagickFormat.APng), + _ => new MagickImageCollection(ms) + }; + + // if (img.First().Format == MagickFormat.Png) { + // img.Dispose(); + // ms.Position = 0; + // img = new MagickImageCollection(ms, MagickFormat.APng); + // } + + return img; + } +} \ 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 34dc8b13..f7a319d3 100644 --- a/extra/admin-api/Spacebar.Cdn/Program.cs +++ b/extra/admin-api/Spacebar.Cdn/Program.cs
@@ -1,6 +1,9 @@ +using ArcaneLibs; using ImageMagick; -using Spacebar.AdminApi.TestClient.Services; +using Microsoft.EntityFrameworkCore; using Spacebar.AdminApi.TestClient.Services.Services; +using Spacebar.Interop.Cdn.Abstractions; +using Spacebar.Models.Db.Contexts; var builder = WebApplication.CreateBuilder(args); @@ -10,6 +13,12 @@ builder.Services.AddSingleton<LruFileCache>(new LruFileCache(1*1024*1024*1024)); builder.Services.AddSingleton<PixelArtDetectionService>(); builder.Services.AddSingleton<DiscordImageResizeService>(); +builder.Services.AddDbContextPool<SpacebarDbContext>(options => { + options + .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar")) + .EnableDetailedErrors(); +}); + builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi(); diff --git a/extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json b/extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json
index 5205c88e..b9fe4cc3 100644 --- a/extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json +++ b/extra/admin-api/Spacebar.Cdn/Properties/launchSettings.json
@@ -10,6 +10,16 @@ "ASPNETCORE_ENVIRONMENT": "Development", "LD_LIBRARY_PATH": "/home/Rory/git/spacebar/server-master/extra/admin-api/Spacebar.Cdn/result-lib/lib/" } + }, + "Local": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5114", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Local", + "LD_LIBRARY_PATH": "/home/Rory/git/spacebar/server-master/extra/admin-api/Spacebar.Cdn/result-lib/lib/" + } } } } diff --git a/extra/admin-api/Spacebar.Cdn/Services/CdnStorageRebuildService.cs b/extra/admin-api/Spacebar.Cdn/Services/CdnStorageRebuildService.cs new file mode 100644
index 00000000..0cf0ef1a --- /dev/null +++ b/extra/admin-api/Spacebar.Cdn/Services/CdnStorageRebuildService.cs
@@ -0,0 +1,5 @@ +namespace Spacebar.AdminApi.TestClient.Services.Services; + +public class CdnFileStorageRebuildService() { + +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Cdn/Services/LruFileCache.cs b/extra/admin-api/Spacebar.Cdn/Services/LruFileCache.cs deleted file mode 100644
index 3a25b1e4..00000000 --- a/extra/admin-api/Spacebar.Cdn/Services/LruFileCache.cs +++ /dev/null
@@ -1,58 +0,0 @@ -namespace Spacebar.AdminApi.TestClient.Services.Services; - -public class LruFileCache(int maxSizeBytes) { - private readonly Dictionary<string, Entry> _entries = new(); - - public async Task<Entry?> GetOrAdd(string key, Func<Task<Entry>> factory) { - if (_entries.TryGetValue(key, out var entry)) { - entry.LastAccessed = DateTimeOffset.UtcNow; - return entry; - } - - entry = await factory(); - if (entry.Data.Length > 0) - _entries[key] = entry; - - if (_entries.Sum(kv => kv.Value.Data.Length) > maxSizeBytes) { - var oldestKey = _entries.OrderBy(kv => kv.Value.LastAccessed).First().Key; - _entries.Remove(oldestKey); - } - - return entry; - } - - public class Entry { - public DateTimeOffset LastAccessed { get; set; } - public byte[] Data { get; set; } - public string MimeType { get; set; } - } -} - -public class LruCache<T>(int maxItems) { - private readonly Dictionary<string, CacheItem> _items = new(); - - public async Task<T?> GetOrAddAsync(string key, Func<Task<T>> factory) { - if (_items.TryGetValue(key, out var cacheItem)) { - cacheItem.LastAccessed = DateTimeOffset.UtcNow; - return cacheItem.Value; - } - - var value = await factory(); - _items[key] = new CacheItem { - Value = value, - LastAccessed = DateTimeOffset.UtcNow - }; - - if (_items.Count > maxItems) { - var oldestKey = _items.OrderBy(kv => kv.Value.LastAccessed).First().Key; - _items.Remove(oldestKey); - } - - return value; - } - - private class CacheItem { - public T Value { get; set; } - public DateTimeOffset LastAccessed { get; set; } - } -} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj b/extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj
index 35d71d45..6a9ec98f 100644 --- a/extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj +++ b/extra/admin-api/Spacebar.Cdn/Spacebar.Cdn.csproj
@@ -7,13 +7,14 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251207-164820" /> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260126-091403" /> <!-- <PackageReference Include="Magick.NET-Q16-HDRI-AnyCPU" Version="14.10.1" />--> - <PackageReference Include="Magick.NET-Q16-HDRI-OpenMP-x64" Version="14.10.1" /> + <PackageReference Include="Magick.NET-Q16-HDRI-OpenMP-x64" Version="14.10.2" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.2" /> </ItemGroup> <ItemGroup> + <ProjectReference Include="..\Interop\Spacebar.Interop.Cdn.Abstractions\Spacebar.Interop.Cdn.Abstractions.csproj" /> <ProjectReference Include="..\Models\Spacebar.Models.Db\Spacebar.Models.Db.csproj" /> </ItemGroup> diff --git a/extra/admin-api/Spacebar.Cdn/StreamingHttpClient.cs b/extra/admin-api/Spacebar.Cdn/StreamingHttpClient.cs deleted file mode 100644
index 8090c053..00000000 --- a/extra/admin-api/Spacebar.Cdn/StreamingHttpClient.cs +++ /dev/null
@@ -1,304 +0,0 @@ -#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances -// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging -using System.Data; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http.Headers; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using ArcaneLibs; -using ArcaneLibs.Extensions; - -namespace Spacebar.AdminApi.TestClient.Services; - -#if SINGLE_HTTPCLIENT -// TODO: Add URI wrapper for -public class StreamingHttpClient { - private static readonly HttpClient Client; - - static StreamingHttpClient() { - try { - var handler = new SocketsHttpHandler { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - MaxConnectionsPerServer = 4096, - EnableMultipleHttp2Connections = true - }; - Client = new HttpClient(handler) { - DefaultRequestVersion = new Version(3, 0), - Timeout = TimeSpan.FromDays(1) - }; - } - catch (PlatformNotSupportedException e) { - Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!"); - Console.WriteLine("Original exception (safe to ignore!):"); - Console.WriteLine(e); - - Client = new HttpClient { - DefaultRequestVersion = new Version(3, 0) - }; - } - catch (Exception e) { - Console.WriteLine("Failed to create HttpClient:"); - Console.WriteLine(e); - throw; - } - } - -#if SYNC_HTTPCLIENT - internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1); -#endif - - public static bool LogRequests = true; - public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new(); - - public Uri? BaseAddress { get; set; } - - // default headers, not bound to client - public HttpRequestHeaders DefaultRequestHeaders { get; set; } = - typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [], null)?.Invoke([]) as HttpRequestHeaders ?? - throw new InvalidOperationException("Failed to create HttpRequestHeaders"); - - private static JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions(); - // options.Converters.Add(new JsonFloatStringConverter()); - // options.Converters.Add(new JsonDoubleStringConverter()); - // options.Converters.Add(new JsonDecimalStringConverter()); - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - return options; - } - - public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken? cancellationToken) { - if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); - // if (!request.RequestUri.IsAbsoluteUri) - request.RequestUri = request.RequestUri.EnsureAbsolute(BaseAddress!); - var swWait = Stopwatch.StartNew(); -#if SYNC_HTTPCLIENT - await _rateLimitSemaphore.WaitAsync(cancellationToken); -#endif - - if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); - if (!request.RequestUri.IsAbsoluteUri) - request.RequestUri = new Uri(BaseAddress ?? throw new InvalidOperationException("Relative URI passed, but no BaseAddress is specified!"), request.RequestUri); - swWait.Stop(); - var swExec = Stopwatch.StartNew(); - - foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value); - foreach (var (key, value) in DefaultRequestHeaders) { - if (request.Headers.Contains(key)) continue; - request.Headers.Add(key, value); - } - - request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true); - - if (LogRequests) - Console.WriteLine("Sending " + request.Summarise(includeHeaders: true, includeQuery: true, includeContentIfText: false, hideHeaders: ["Accept"])); - - HttpResponseMessage? responseMessage; - try { - responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken ?? CancellationToken.None); - } - catch (Exception e) { - if (e is TaskCanceledException or TimeoutException) { - if (request.Method == HttpMethod.Get && !(cancellationToken?.IsCancellationRequested ?? false)) { - await Task.Delay(Random.Shared.Next(500, 2500), cancellationToken ?? CancellationToken.None); - request.ResetSendStatus(); - return await SendAsync(request, cancellationToken ?? CancellationToken.None); - } - } - else if (!e.ToString().StartsWith("TypeError: NetworkError")) - Console.WriteLine( - $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); - - throw; - } -#if SYNC_HTTPCLIENT - finally { - _rateLimitSemaphore.Release(); - } -#endif - - // Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.GetContentLength())}, WAIT={swWait.ElapsedMilliseconds}ms, EXEC={swExec.ElapsedMilliseconds}ms)"); - if (LogRequests) - Console.WriteLine("Received " + responseMessage.Summarise(includeHeaders: true, includeContentIfText: false, hideHeaders: [ - "Server", - "Date", - "Transfer-Encoding", - "Connection", - "Vary", - "Content-Length", - "Access-Control-Allow-Origin", - "Access-Control-Allow-Methods", - "Access-Control-Allow-Headers", - "Access-Control-Expose-Headers", - "Cache-Control", - "Cross-Origin-Resource-Policy", - "X-Content-Security-Policy", - "Referrer-Policy", - "X-Robots-Tag", - "Content-Security-Policy" - ])); - - return responseMessage; - } - - public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var responseMessage = await SendUnhandledAsync(request, cancellationToken); - if (responseMessage.IsSuccessStatusCode) return responseMessage; - - //retry on gateway timeout - // if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) { - // request.ResetSendStatus(); - // return await SendAsync(request, cancellationToken); - // } - - //error handling - var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken); - if (content.Length == 0) - throw new DataException("Content was empty"); - // throw new MatrixException() { - // ErrorCode = "M_UNKNOWN", - // Error = "Unknown error, server returned no content" - // }; - - // if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content); - if (!content.TrimStart().StartsWith('{')) { - responseMessage.EnsureSuccessStatusCode(); - throw new InvalidDataException("Encountered invalid data:\n" + content); - } - //we have a matrix error - - throw new Exception("Unknown http exception"); - // MatrixException? ex; - // try { - // ex = JsonSerializer.Deserialize<MatrixException>(content); - // } - // catch (JsonException e) { - // throw new LibMatrixException() { - // ErrorCode = "M_INVALID_JSON", - // Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken) - // }; - // } - // - // Debug.Assert(ex != null, nameof(ex) + " != null"); - // ex.RawContent = content; - // // Console.WriteLine($"Failed to send request: {ex}"); - // if (ex.RetryAfterMs is null) throw ex!; - // //we have a ratelimit error - // await Task.Delay(ex.RetryAfterMs.Value, cancellationToken); - request.ResetSendStatus(); - return await SendAsync(request, cancellationToken); - } - - // GetAsync - public Task<HttpResponseMessage> GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) => - SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None); - - // GetFromJsonAsync - public async Task<T?> TryGetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - try { - return await GetFromJsonAsync<T>(requestUri, options, cancellationToken); - } - catch (JsonException e) { - Console.WriteLine($"Failed to deserialize response from {requestUri}: {e.Message}"); - return default; - } - catch (HttpRequestException e) { - Console.WriteLine($"Failed to get {requestUri}: {e.Message}"); - return default; - } - } - - public async Task<T> GetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - options = GetJsonSerializerOptions(options); - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var response = await SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); - - return await JsonSerializer.DeserializeAsync<T>(responseStream, options, cancellationToken) ?? - throw new InvalidOperationException("Failed to deserialize response"); - } - - // GetStreamAsync - public async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) { - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var response = await SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStreamAsync(cancellationToken); - } - - public async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) where T : notnull { - options = GetJsonSerializerOptions(options); - var request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), - Encoding.UTF8, "application/json"); - return await SendAsync(request, cancellationToken); - } - - public async Task<HttpResponseMessage> PostAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) where T : notnull { - options ??= new JsonSerializerOptions(); - // options.Converters.Add(new JsonFloatStringConverter()); - // options.Converters.Add(new JsonDoubleStringConverter()); - // options.Converters.Add(new JsonDecimalStringConverter()); - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), - Encoding.UTF8, "application/json"); - return await SendAsync(request, cancellationToken); - } - - public async IAsyncEnumerable<T?> GetAsyncEnumerableFromJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) { - options = GetJsonSerializerOptions(options); - var res = await GetAsync(requestUri); - options.PropertyNameCaseInsensitive = true; - var result = JsonSerializer.DeserializeAsyncEnumerable<T>(await res.Content.ReadAsStreamAsync(), options); - await foreach (var resp in result) yield return resp; - } - - public static async Task<bool> CheckSuccessStatus(string url) { - //cors causes failure, try to catch - try { - var resp = await Client.GetAsync(url); - return resp.IsSuccessStatusCode; - } - catch (Exception e) { - Console.WriteLine($"Failed to check success status: {e.Message}"); - return false; - } - } - - public async Task<HttpResponseMessage> PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) { - var request = new HttpRequestMessage(HttpMethod.Post, uri) { - Content = content - }; - return await SendAsync(request, cancellationToken); - } - - public async Task<HttpResponseMessage> DeleteAsync(string url) { - var request = new HttpRequestMessage(HttpMethod.Delete, url); - return await SendAsync(request); - } - - public async Task<HttpResponseMessage> DeleteAsJsonAsync<T>(string url, T payload) { - var request = new HttpRequestMessage(HttpMethod.Delete, url) { - Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") - }; - return await SendAsync(request); - } - - public async Task<HttpResponseMessage> PatchAsJsonAsync<T>(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 diff --git a/extra/admin-api/SpacebarAdminAPI.sln b/extra/admin-api/SpacebarAdminAPI.sln
index 3ec737fb..3235d6d2 100644 --- a/extra/admin-api/SpacebarAdminAPI.sln +++ b/extra/admin-api/SpacebarAdminAPI.sln
@@ -37,6 +37,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Interop.Replicatio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Interop.Replication.UnixSocket", "Interop\Spacebar.Interop.Replication.UnixSocket\Spacebar.Interop.Replication.UnixSocket.csproj", "{2D8F75C2-C4DC-4DBC-A880-5BF021D637E5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Cdn.Fsck", "Utilities\Spacebar.Cdn.Fsck\Spacebar.Cdn.Fsck.csproj", "{05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spacebar.Interop.Cdn.Abstractions", "Interop\Spacebar.Interop.Cdn.Abstractions\Spacebar.Interop.Cdn.Abstractions.csproj", "{B494C58D-2D53-49F4-9E3E-1DCF60828802}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -215,6 +219,30 @@ Global {2D8F75C2-C4DC-4DBC-A880-5BF021D637E5}.Release|x64.Build.0 = Release|Any CPU {2D8F75C2-C4DC-4DBC-A880-5BF021D637E5}.Release|x86.ActiveCfg = Release|Any CPU {2D8F75C2-C4DC-4DBC-A880-5BF021D637E5}.Release|x86.Build.0 = Release|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|x64.ActiveCfg = Debug|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|x64.Build.0 = Debug|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|x86.ActiveCfg = Debug|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Debug|x86.Build.0 = Debug|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|Any CPU.Build.0 = Release|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|x64.ActiveCfg = Release|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|x64.Build.0 = Release|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|x86.ActiveCfg = Release|Any CPU + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D}.Release|x86.Build.0 = Release|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|x64.ActiveCfg = Debug|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|x64.Build.0 = Debug|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|x86.ActiveCfg = Debug|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Debug|x86.Build.0 = Debug|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|Any CPU.Build.0 = Release|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|x64.ActiveCfg = Release|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|x64.Build.0 = Release|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|x86.ActiveCfg = Release|Any CPU + {B494C58D-2D53-49F4-9E3E-1DCF60828802}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -232,5 +260,7 @@ Global {6F2A4C4B-8EAA-4469-9320-B52C23D9640F} = {16DBEA54-D51A-4D91-84DF-C701B6B4786F} {2FAE6018-CF95-4B44-A7F7-D6FAF596076D} = {16DBEA54-D51A-4D91-84DF-C701B6B4786F} {2D8F75C2-C4DC-4DBC-A880-5BF021D637E5} = {16DBEA54-D51A-4D91-84DF-C701B6B4786F} + {05B1FEDA-1112-49C8-B7E8-539B5ED8C05D} = {04787943-EBB6-4DE4-96D5-4CFB4A2CEE99} + {B494C58D-2D53-49F4-9E3E-1DCF60828802} = {16DBEA54-D51A-4D91-84DF-C701B6B4786F} EndGlobalSection EndGlobal diff --git a/extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj b/extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj
index 72d306b3..7ed79afd 100644 --- a/extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj +++ b/extra/admin-api/Utilities/DiscordEmojiConverter/DiscordEmojiConverter.csproj
@@ -8,7 +8,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251207-164820" /> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260126-091403" /> </ItemGroup> </Project> diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj
index 5daabdd0..546875d2 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.PrepareTestData/Spacebar.AdminApi.PrepareTestData.csproj
@@ -10,7 +10,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251207-164820" /> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260126-091403" /> </ItemGroup> </Project> diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor
index f8cc15dd..b105968e 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Guilds.razor
@@ -1,6 +1,7 @@ @page "/Guilds" @using System.Net.Http.Headers @using System.Reflection +@using ArcaneLibs @using Spacebar.Models.AdminApi @using Spacebar.AdminApi.TestClient.Services @using ArcaneLibs.Blazor.Components diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor
index b70f4efb..91aec659 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/HttpTestClient.razor
@@ -1,5 +1,6 @@ @page "/HttpTestClient" @using System.Text.Json +@using ArcaneLibs @using ArcaneLibs.Blazor.Components @using ArcaneLibs.Extensions @using Spacebar.AdminApi.TestClient.Classes.OpenAPI 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
index 73ce4ec2..3e613104 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/NonAdmin/StickerManager.razor
@@ -3,6 +3,7 @@ @using System.Diagnostics.CodeAnalysis @using System.Net.Http.Headers @using System.Text.Json.Serialization +@using ArcaneLibs @using ArcaneLibs.Blazor.Components @using ArcaneLibs.Blazor.Components.Services @using ArcaneLibs.Extensions diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor
index 7a83bd52..4ecbf9d3 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/ServerConfig.razor
@@ -2,6 +2,7 @@ @inject Config Config @using System.Net.Http.Headers @using System.Text.Json +@using ArcaneLibs @using Spacebar.AdminApi.TestClient.Services @using Spacebar.ConfigModel.Extensions <h3>Server Config</h3> diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor
index 939c5dca..fa8ad23a 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/Users.razor
@@ -1,6 +1,7 @@ @page "/Users" @using System.Net.Http.Headers @using System.Reflection +@using ArcaneLibs @using Spacebar.Models.AdminApi @using Spacebar.AdminApi.TestClient.Services @using ArcaneLibs.Blazor.Components diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor
index b2116ebd..e9a59f3b 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Pages/UsersDelete.razor
@@ -2,6 +2,7 @@ @using System.Net.Http.Headers @using System.Text.Json @using System.Text.Json.Nodes +@using ArcaneLibs @using ArcaneLibs.Extensions @using Spacebar.Models.AdminApi @using Spacebar.AdminApi.TestClient.Services diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs deleted file mode 100644
index 311eb457..00000000 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Services/StreamingHttpClient.cs +++ /dev/null
@@ -1,304 +0,0 @@ -#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances -// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging -using System.Data; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http.Headers; -using System.Reflection; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using ArcaneLibs; -using ArcaneLibs.Extensions; - -namespace Spacebar.AdminApi.TestClient.Services; - -#if SINGLE_HTTPCLIENT -// TODO: Add URI wrapper for -public class StreamingHttpClient { - private static readonly HttpClient Client; - - static StreamingHttpClient() { - try { - var handler = new SocketsHttpHandler { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - MaxConnectionsPerServer = 4096, - EnableMultipleHttp2Connections = true - }; - Client = new HttpClient(handler) { - DefaultRequestVersion = new Version(3, 0), - Timeout = TimeSpan.FromDays(1) - }; - } - catch (PlatformNotSupportedException e) { - Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!"); - Console.WriteLine("Original exception (safe to ignore!):"); - Console.WriteLine(e); - - Client = new HttpClient { - DefaultRequestVersion = new Version(3, 0) - }; - } - catch (Exception e) { - Console.WriteLine("Failed to create HttpClient:"); - Console.WriteLine(e); - throw; - } - } - -#if SYNC_HTTPCLIENT - internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1); -#endif - - public static bool LogRequests = true; - public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new(); - - public Uri? BaseAddress { get; set; } - - // default headers, not bound to client - public HttpRequestHeaders DefaultRequestHeaders { get; set; } = - typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, [], null)?.Invoke([]) as HttpRequestHeaders ?? - throw new InvalidOperationException("Failed to create HttpRequestHeaders"); - - private static JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) { - options ??= new JsonSerializerOptions(); - // options.Converters.Add(new JsonFloatStringConverter()); - // options.Converters.Add(new JsonDoubleStringConverter()); - // options.Converters.Add(new JsonDecimalStringConverter()); - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - return options; - } - - public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); - // if (!request.RequestUri.IsAbsoluteUri) - request.RequestUri = request.RequestUri.EnsureAbsolute(BaseAddress!); - var swWait = Stopwatch.StartNew(); -#if SYNC_HTTPCLIENT - await _rateLimitSemaphore.WaitAsync(cancellationToken); -#endif - - if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); - if (!request.RequestUri.IsAbsoluteUri) - request.RequestUri = new Uri(BaseAddress ?? throw new InvalidOperationException("Relative URI passed, but no BaseAddress is specified!"), request.RequestUri); - swWait.Stop(); - var swExec = Stopwatch.StartNew(); - - foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value); - foreach (var (key, value) in DefaultRequestHeaders) { - if (request.Headers.Contains(key)) continue; - request.Headers.Add(key, value); - } - - request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true); - - if (LogRequests) - Console.WriteLine("Sending " + request.Summarise(includeHeaders: true, includeQuery: true, includeContentIfText: false, hideHeaders: ["Accept"])); - - HttpResponseMessage? responseMessage; - try { - responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - } - catch (Exception e) { - if (e is TaskCanceledException or TimeoutException) { - if (request.Method == HttpMethod.Get && !cancellationToken.IsCancellationRequested) { - await Task.Delay(Random.Shared.Next(500, 2500), cancellationToken); - request.ResetSendStatus(); - return await SendAsync(request, cancellationToken); - } - } - else if (!e.ToString().StartsWith("TypeError: NetworkError")) - Console.WriteLine( - $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); - - throw; - } -#if SYNC_HTTPCLIENT - finally { - _rateLimitSemaphore.Release(); - } -#endif - - // Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.GetContentLength())}, WAIT={swWait.ElapsedMilliseconds}ms, EXEC={swExec.ElapsedMilliseconds}ms)"); - if (LogRequests) - Console.WriteLine("Received " + responseMessage.Summarise(includeHeaders: true, includeContentIfText: false, hideHeaders: [ - "Server", - "Date", - "Transfer-Encoding", - "Connection", - "Vary", - "Content-Length", - "Access-Control-Allow-Origin", - "Access-Control-Allow-Methods", - "Access-Control-Allow-Headers", - "Access-Control-Expose-Headers", - "Cache-Control", - "Cross-Origin-Resource-Policy", - "X-Content-Security-Policy", - "Referrer-Policy", - "X-Robots-Tag", - "Content-Security-Policy" - ])); - - return responseMessage; - } - - public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - var responseMessage = await SendUnhandledAsync(request, cancellationToken); - if (responseMessage.IsSuccessStatusCode) return responseMessage; - - //retry on gateway timeout - // if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) { - // request.ResetSendStatus(); - // return await SendAsync(request, cancellationToken); - // } - - //error handling - var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken); - if (content.Length == 0) - throw new DataException("Content was empty"); - // throw new MatrixException() { - // ErrorCode = "M_UNKNOWN", - // Error = "Unknown error, server returned no content" - // }; - - // if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content); - if (!content.TrimStart().StartsWith('{')) { - responseMessage.EnsureSuccessStatusCode(); - throw new InvalidDataException("Encountered invalid data:\n" + content); - } - //we have a matrix error - - throw new Exception("Unknown http exception"); - // MatrixException? ex; - // try { - // ex = JsonSerializer.Deserialize<MatrixException>(content); - // } - // catch (JsonException e) { - // throw new LibMatrixException() { - // ErrorCode = "M_INVALID_JSON", - // Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken) - // }; - // } - // - // Debug.Assert(ex != null, nameof(ex) + " != null"); - // ex.RawContent = content; - // // Console.WriteLine($"Failed to send request: {ex}"); - // if (ex.RetryAfterMs is null) throw ex!; - // //we have a ratelimit error - // await Task.Delay(ex.RetryAfterMs.Value, cancellationToken); - request.ResetSendStatus(); - return await SendAsync(request, cancellationToken); - } - - // GetAsync - public Task<HttpResponseMessage> GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) => - SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None); - - // GetFromJsonAsync - public async Task<T?> TryGetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - try { - return await GetFromJsonAsync<T>(requestUri, options, cancellationToken); - } - catch (JsonException e) { - Console.WriteLine($"Failed to deserialize response from {requestUri}: {e.Message}"); - return default; - } - catch (HttpRequestException e) { - Console.WriteLine($"Failed to get {requestUri}: {e.Message}"); - return default; - } - } - - public async Task<T> GetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { - options = GetJsonSerializerOptions(options); - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var response = await SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); - - return await JsonSerializer.DeserializeAsync<T>(responseStream, options, cancellationToken) ?? - throw new InvalidOperationException("Failed to deserialize response"); - } - - // GetStreamAsync - public async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) { - var request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - var response = await SendAsync(request, cancellationToken); - response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStreamAsync(cancellationToken); - } - - public async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) where T : notnull { - options = GetJsonSerializerOptions(options); - var request = new HttpRequestMessage(HttpMethod.Put, requestUri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), - Encoding.UTF8, "application/json"); - return await SendAsync(request, cancellationToken); - } - - public async Task<HttpResponseMessage> PostAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, - CancellationToken cancellationToken = default) where T : notnull { - options ??= new JsonSerializerOptions(); - // options.Converters.Add(new JsonFloatStringConverter()); - // options.Converters.Add(new JsonDoubleStringConverter()); - // options.Converters.Add(new JsonDecimalStringConverter()); - options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - var request = new HttpRequestMessage(HttpMethod.Post, requestUri); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), - Encoding.UTF8, "application/json"); - return await SendAsync(request, cancellationToken); - } - - public async IAsyncEnumerable<T?> GetAsyncEnumerableFromJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) { - options = GetJsonSerializerOptions(options); - var res = await GetAsync(requestUri); - options.PropertyNameCaseInsensitive = true; - var result = JsonSerializer.DeserializeAsyncEnumerable<T>(await res.Content.ReadAsStreamAsync(), options); - await foreach (var resp in result) yield return resp; - } - - public static async Task<bool> CheckSuccessStatus(string url) { - //cors causes failure, try to catch - try { - var resp = await Client.GetAsync(url); - return resp.IsSuccessStatusCode; - } - catch (Exception e) { - Console.WriteLine($"Failed to check success status: {e.Message}"); - return false; - } - } - - public async Task<HttpResponseMessage> PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) { - var request = new HttpRequestMessage(HttpMethod.Post, uri) { - Content = content - }; - return await SendAsync(request, cancellationToken); - } - - public async Task<HttpResponseMessage> DeleteAsync(string url) { - var request = new HttpRequestMessage(HttpMethod.Delete, url); - return await SendAsync(request); - } - - public async Task<HttpResponseMessage> DeleteAsJsonAsync<T>(string url, T payload) { - var request = new HttpRequestMessage(HttpMethod.Delete, url) { - Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") - }; - return await SendAsync(request); - } - - public async Task<HttpResponseMessage> PatchAsJsonAsync<T>(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 diff --git a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj
index 552dbd3e..d1656c63 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj +++ b/extra/admin-api/Utilities/Spacebar.AdminApi.TestClient/Spacebar.AdminApi.TestClient.csproj
@@ -18,8 +18,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251207-164820" /> - <PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.0-preview.20251207-164820" /> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260126-091403" /> + <PackageReference Include="ArcaneLibs.Blazor.Components" Version="1.0.1-preview.20260126-091403" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.2" PrivateAssets="all" /> diff --git a/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj b/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj
index 5daabdd0..546875d2 100644 --- a/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj +++ b/extra/admin-api/Utilities/Spacebar.AdminApiTest/Spacebar.AdminApiTest.csproj
@@ -10,7 +10,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.20251207-164820" /> + <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260126-091403" /> </ItemGroup> </Project> diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs new file mode 100644
index 00000000..c0b0bbc3 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs
@@ -0,0 +1,117 @@ +using System.Diagnostics; +using Microsoft.EntityFrameworkCore; +using Spacebar.Interop.Cdn.Abstractions; +using Spacebar.Models.Db.Contexts; + +namespace Spacebar.Cdn.Fsck; + +public class FsckService(ILogger<FsckService> logger, IServiceScopeFactory serviceScopeFactory, IFileSource fs) : IHostedService { + private SpacebarDbContext _db = null!; + + public async Task StartAsync(CancellationToken cancellationToken) { + var sw = Stopwatch.StartNew(); + await using var scope = serviceScopeFactory.CreateAsyncScope(); + _db = scope.ServiceProvider.GetRequiredService<SpacebarDbContext>(); + logger.LogInformation("Starting fsck on {source}...", $"{fs.GetType().FullName}({fs.BaseUrl})"); + await RunFsckAsync("User Avatars", "/avatars", EnumerateUserAvatarFilesAsync()); + await RunFsckAsync("User Banners", "/banners", EnumerateUserBannerPathsAsync()); + await RunFsckAsync("Guild Icons", "/icons", EnumerateGuildIconPathsAsync()); + await RunFsckAsync("Stickers", "/stickers", EnumerateStickerPathsAsync()); + await RunFsckAsync("Emojis", "/emojis", EnumerateEmojiPathsAsync()); + logger.LogInformation("Fsck complete in {time}.", sw.Elapsed); + } + + public async Task StopAsync(CancellationToken cancellationToken) { } + + private readonly Stopwatch _lastUpdateSw = Stopwatch.StartNew(); + private readonly SemaphoreSlim _fsckSemaphore = new SemaphoreSlim(32, 32); + + public struct FsckItem { + public string Path; + public string ItemId; + } + + private async Task RunFsckAsync(string name, string path, IQueryable<FsckItem> items) { + int i = 0, count = await items.CountAsync(); + List<Task> tasks = []; + + await foreach (var item in items.AsAsyncEnumerable()) { + tasks.Add(Task.Run(async () => { + await _fsckSemaphore.WaitAsync(); + if (_lastUpdateSw.ElapsedMilliseconds >= (1000 / 30) || i == 0) { + _lastUpdateSw.Restart(); + Console.Write($"{name} fsck: {i}/{count}: {item.Path,-64}\r"); + } + + i++; + if (!await fs.FileExists(item.Path)) + logger.LogWarning("{itemType} {itemId} is missing at {path}", name, item.ItemId, item.Path); + + _fsckSemaphore.Release(); + })); + } + + await Task.WhenAll(tasks); + logger.LogInformation("Validated {count} items for {path}.", i, path); + } + +#region User Assets + + public IQueryable<FsckItem> EnumerateUserAvatarFilesAsync() => + _db.Users + .Where(x => !string.IsNullOrWhiteSpace(x.Avatar)) + .OrderBy(x => x.Id) + .Select(x => new FsckItem { + Path = $"/avatars/{x.Id}/{x.Avatar}", + ItemId = x.Id + }); + + public IQueryable<FsckItem> EnumerateUserBannerPathsAsync() => + _db.Users + .Where(x => !string.IsNullOrWhiteSpace(x.Banner)) + .OrderBy(x => x.Id) + .Select(x => new FsckItem { + Path = $"/banners/{x.Id}/{x.Banner}", + ItemId = x.Id + }); + +#endregion + +#region Guild Assets + + public IQueryable<FsckItem> EnumerateGuildIconPathsAsync() => + _db.Guilds + .Where(x => !string.IsNullOrWhiteSpace(x.Icon)) + .OrderBy(x => x.Id) + .Select(x => new FsckItem { + Path = $"/icons/{x.Id}/{x.Icon}", + ItemId = x.Id + }); + + public IQueryable<FsckItem> EnumerateRoleIconPathsAsync() => + _db.Roles + .Where(x => !string.IsNullOrWhiteSpace(x.Icon)) + .OrderBy(x => x.Id) + .Select(x => new FsckItem { + Path = $"/role-icons/{x.Id}/{x.Icon}", + ItemId = x.Id + }); + + public IQueryable<FsckItem> EnumerateStickerPathsAsync() => + _db.Stickers + .OrderBy(x => x.Id) + .Select(x => new FsckItem { + Path = $"/stickers/{x.Id}.png", + ItemId = x.Id + }); + + public IQueryable<FsckItem> EnumerateEmojiPathsAsync() => + _db.Emojis + .OrderBy(x => x.Id) + .Select(x => new FsckItem { + Path = $"/emojis/{x.Id}", + ItemId = x.Id + }); + +#endregion +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs new file mode 100644
index 00000000..15822cc9 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs
@@ -0,0 +1,21 @@ +using ArcaneLibs; +using Microsoft.EntityFrameworkCore; +using Spacebar.Cdn.Fsck; +using Spacebar.Interop.Cdn.Abstractions; +using Spacebar.Models.Db.Contexts; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddSingleton<IFileSource>(new ProxyFileSource("http://cdn.old.server.spacebar.chat")); +builder.Services.AddSingleton<LruFileCache>(new LruFileCache(1*1024*1024*1024)); +builder.Services.AddHostedService<FsckService>(); + +builder.Services.AddDbContextPool<SpacebarDbContext>(options => { + options + .UseNpgsql(builder.Configuration.GetConnectionString("Spacebar")) + .EnableDetailedErrors(); +}); + +StreamingHttpClient.LogRequests = false; + +var host = builder.Build(); +host.Start(); \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Properties/launchSettings.json b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Properties/launchSettings.json new file mode 100644
index 00000000..d2903516 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Properties/launchSettings.json
@@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "Development": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Local": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Local" + } + } + } +} diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj new file mode 100644
index 00000000..e08d3ae8 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj
@@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk.Worker"> + + <PropertyGroup> + <TargetFramework>net10.0</TargetFramework> + <Nullable>enable</Nullable> + <ImplicitUsings>enable</ImplicitUsings> + <UserSecretsId>dotnet-Spacebar.Cdn.Fsck-a4aee86f-7e64-4c71-88d7-d2a76cc8b77e</UserSecretsId> + <NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2"/> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\Interop\Spacebar.Interop.Cdn.Abstractions\Spacebar.Interop.Cdn.Abstractions.csproj" /> + <ProjectReference Include="..\..\Models\Spacebar.Models.Db\Spacebar.Models.Db.csproj" /> + </ItemGroup> +</Project> diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.Development.json b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.Development.json new file mode 100644
index 00000000..b2dcdb67 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.Development.json
@@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.json b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.json new file mode 100644
index 00000000..b2dcdb67 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/appsettings.json
@@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json new file mode 100644
index 00000000..fe51488c --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json
@@ -0,0 +1 @@ +[] diff --git a/extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json b/extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json
index 3bf6a98c..269c9e39 100644 --- a/extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json +++ b/extra/admin-api/Utilities/Spacebar.CleanSettingsRows/deps.json
@@ -5,39 +5,14 @@ "hash": "sha256-EXvojddPu+9JKgOG9NSQgUTfWq1RpOYw7adxDPKDJ6o=" }, { - "pname": "Microsoft.Build", - "version": "17.7.2", - "hash": "sha256-k35nFdPxC8t0zAltVSmAJtsepp/ubNIjPOsJ6k8jSqM=" - }, - { "pname": "Microsoft.Build.Framework", - "version": "17.14.28", - "hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM=" + "version": "17.11.31", + "hash": "sha256-YS4oASrmC5dmZrx5JPS7SfKmUpIJErlUpVDsU3VrfFE=" }, { "pname": "Microsoft.Build.Framework", - "version": "17.7.2", - "hash": "sha256-fNWmVQYFTJDveAGmxEdNqJRAczV6+Ep8RA8clKBJFqw=" - }, - { - "pname": "Microsoft.Build.Tasks.Core", - "version": "17.14.28", - "hash": "sha256-M9zRXYijH2HtLlRXbrUK1a1LQ9zkT+DC9ZmMiiVZwv0=" - }, - { - "pname": "Microsoft.Build.Tasks.Core", - "version": "17.7.2", - "hash": "sha256-OrV/qWgZHzGlNUmaSfX5wDBcmg1aQeF3/OUHpSH+uZU=" - }, - { - "pname": "Microsoft.Build.Utilities.Core", - "version": "17.14.28", - "hash": "sha256-VFfO+UpyTpw2X/qiCCOCYzvMLuu7B+XVSSpJZQLkPzU=" - }, - { - "pname": "Microsoft.Build.Utilities.Core", - "version": "17.7.2", - "hash": "sha256-oatF0KfuP1nb4+OLNKg2/R/ZLO4EiACaO5leaxMEY4A=" + "version": "18.0.2", + "hash": "sha256-fO31KAdDs2J0RUYD1ov9UB3ucsbALan7K0YdWW+yg7A=" }, { "pname": "Microsoft.CodeAnalysis.Analyzers", @@ -46,28 +21,28 @@ }, { "pname": "Microsoft.CodeAnalysis.Common", - "version": "4.14.0", - "hash": "sha256-ne/zxH3GqoGB4OemnE8oJElG5mai+/67ASaKqwmL2BE=" + "version": "5.0.0", + "hash": "sha256-g4ALvBSNyHEmSb1l5TFtWW7zEkiRmhqLx4XWZu9sr2U=" }, { "pname": "Microsoft.CodeAnalysis.CSharp", - "version": "4.14.0", - "hash": "sha256-5Mzj3XkYYLkwDWh17r1NEXSbXwwWYQPiOmkSMlgo1JY=" + "version": "5.0.0", + "hash": "sha256-ctBCkQGFpH/xT5rRE3xibu9YxPD108RuC4a4Z25koG8=" }, { "pname": "Microsoft.CodeAnalysis.CSharp.Workspaces", - "version": "4.14.0", - "hash": "sha256-aNbV1a0yYBs0fpQawG6LXcbyoE8en+YFSpV5vcYE4J4=" + "version": "5.0.0", + "hash": "sha256-yWVcLt/f2CouOfFy966glGdtSFy+RcgrU1dd9UtlL/Q=" }, { "pname": "Microsoft.CodeAnalysis.Workspaces.Common", - "version": "4.14.0", - "hash": "sha256-0YfeaJe01WBUm9avy4a8FacQJXA1NkpnDpiXu4yz88I=" + "version": "5.0.0", + "hash": "sha256-Bir5e1gEhgQQ6upQmVKQHAKLRfenAu60DAzNupNnZsQ=" }, { "pname": "Microsoft.CodeAnalysis.Workspaces.MSBuild", - "version": "4.14.0", - "hash": "sha256-5SJfpRqzqCK0UbkmAaJpA/r1XJb0YAriMMeQHYC4d+o=" + "version": "5.0.0", + "hash": "sha256-+58+iqTayTiE0pDaog1U8mjaDA8bNNDLA8gjCQZZudo=" }, { "pname": "Microsoft.EntityFrameworkCore", @@ -91,8 +66,8 @@ }, { "pname": "Microsoft.EntityFrameworkCore.Design", - "version": "10.0.1", - "hash": "sha256-GGNZIGNEMhSGaMRFkRN4bOuCUBs5YVnX8klXarm319U=" + "version": "10.0.2", + "hash": "sha256-bTShsGux0y/49PIIMb/4ZX3x5+rPacvT5/NcooNCI1Y=" }, { "pname": "Microsoft.EntityFrameworkCore.Relational", @@ -101,8 +76,8 @@ }, { "pname": "Microsoft.EntityFrameworkCore.Relational", - "version": "10.0.1", - "hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY=" + "version": "10.0.2", + "hash": "sha256-Y4jPpoYhKizg5wF6QfkBX4sYlE2FU1bYhfoDN3xkhKM=" }, { "pname": "Microsoft.Extensions.Caching.Abstractions", @@ -111,16 +86,6 @@ }, { "pname": "Microsoft.Extensions.Caching.Memory", - "version": "10.0.0", - "hash": "sha256-AMgDSm1k6q0s17spGtyR5q8nAqUFDOxl/Fe38f9M+d4=" - }, - { - "pname": "Microsoft.Extensions.Caching.Memory", - "version": "10.0.1", - "hash": "sha256-Qb7xK6VEZDas0lJFaW1suKdFjtkSYwLHHxkQEfWIU2A=" - }, - { - "pname": "Microsoft.Extensions.Caching.Memory", "version": "10.0.2", "hash": "sha256-sRUF7DM0s1yzZnfjM/hF9A/IysE6Er23gZ6jST+RWh0=" }, @@ -131,16 +96,6 @@ }, { "pname": "Microsoft.Extensions.Configuration.Abstractions", - "version": "10.0.0", - "hash": "sha256-GcgrnTAieCV7AVT13zyOjfwwL86e99iiO/MiMOxPGG0=" - }, - { - "pname": "Microsoft.Extensions.Configuration.Abstractions", - "version": "10.0.1", - "hash": "sha256-s4PDp+vtzdxKIxnOT3+dDRoTDopyl8kqmmw4KDnkOtQ=" - }, - { - "pname": "Microsoft.Extensions.Configuration.Abstractions", "version": "10.0.2", "hash": "sha256-P+0kaDGO+xB9KxF9eWHDJ4hzi05sUGM/uMNEX5NdBTE=" }, @@ -191,8 +146,8 @@ }, { "pname": "Microsoft.Extensions.DependencyModel", - "version": "10.0.1", - "hash": "sha256-XIj2jEURe25YA4RhBSuCqQpic0YP+TZaO/dbBPCjad8=" + "version": "10.0.2", + "hash": "sha256-w/dGIjtZiGH+KW3969BPOdQpQEV+WB7RPTa2MK2DavE=" }, { "pname": "Microsoft.Extensions.Diagnostics", @@ -231,16 +186,6 @@ }, { "pname": "Microsoft.Extensions.Logging", - "version": "10.0.0", - "hash": "sha256-P+zPAadLL63k/GqK34/qChqQjY9aIRxZfxlB9lqsSrs=" - }, - { - "pname": "Microsoft.Extensions.Logging", - "version": "10.0.1", - "hash": "sha256-zuLP3SIpCToMOlIPOEv3Kq8y/minecd8k8GSkxFo13E=" - }, - { - "pname": "Microsoft.Extensions.Logging", "version": "10.0.2", "hash": "sha256-9+gfQwK32JMYscW1YvyCWEzc9mRZOjCACoD9U1vVaJI=" }, @@ -306,11 +251,6 @@ }, { "pname": "Microsoft.Extensions.Primitives", - "version": "10.0.1", - "hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc=" - }, - { - "pname": "Microsoft.Extensions.Primitives", "version": "10.0.2", "hash": "sha256-8Ccrjjv9cFVf9RyCc7GS/Byt8+DXdSNea0UX3A5BEdA=" }, @@ -320,14 +260,9 @@ "hash": "sha256-ZNLusK1CRuq5BZYZMDqaz04PIKScE2Z7sS2tehU7EJs=" }, { - "pname": "Microsoft.NET.StringTools", - "version": "17.14.28", - "hash": "sha256-UzREyvDxkiOQ4cEOQ5UCjkwXGrldIDCcbefECTPGjXI=" - }, - { - "pname": "Microsoft.NET.StringTools", - "version": "17.7.2", - "hash": "sha256-hQE07TCgcQuyu9ZHVq2gPDb0+xe8ECJUdrgh17bJP4o=" + "pname": "Microsoft.VisualStudio.SolutionPersistence", + "version": "1.0.52", + "hash": "sha256-KZGPtOXe6Hv8RrkcsgoLKTRyaCScIpQEa2NhNB3iOXw=" }, { "pname": "Mono.TextTemplating", @@ -355,16 +290,6 @@ "hash": "sha256-uPetUFZyHfxjScu5x4agjk9pIhbCkt5rG4Axj25npcQ=" }, { - "pname": "System.CodeDom", - "version": "7.0.0", - "hash": "sha256-7IPt39cY+0j0ZcRr/J45xPtEjnSXdUJ/5ai3ebaYQiE=" - }, - { - "pname": "System.CodeDom", - "version": "9.0.0", - "hash": "sha256-578lcBgswW0eM16r0EnJzfGodPx86RxxFoZHc2PSzsw=" - }, - { "pname": "System.Composition", "version": "9.0.0", "hash": "sha256-FehOkQ2u1p8mQ0/wn3cZ+24HjhTLdck8VZYWA1CcgbM=" @@ -395,83 +320,8 @@ "hash": "sha256-F5fpTUs3Rr7yP/NyIzr+Xn5NdTXXp8rrjBnF9UBBUog=" }, { - "pname": "System.Configuration.ConfigurationManager", - "version": "7.0.0", - "hash": "sha256-SgBexTTjRn23uuXvkzO0mz0qOfA23MiS4Wv+qepMLZE=" - }, - { - "pname": "System.Configuration.ConfigurationManager", - "version": "9.0.0", - "hash": "sha256-+pLnTC0YDP6Kjw5DVBiFrV/Q3x5is/+6N6vAtjvhVWk=" - }, - { "pname": "System.Diagnostics.EventLog", "version": "10.0.2", "hash": "sha256-rezZk0M4+MWRxwGSFzXfvekrhL8qLTp7Pc8YsSy/4nE=" - }, - { - "pname": "System.Diagnostics.EventLog", - "version": "9.0.0", - "hash": "sha256-tPvt6yoAp56sK/fe+/ei8M65eavY2UUhRnbrREj/Ems=" - }, - { - "pname": "System.Formats.Nrbf", - "version": "9.0.0", - "hash": "sha256-c4qf6CocQUZB0ySGQd8s15PXY7xfrjQqMGXxkwytKyw=" - }, - { - "pname": "System.Reflection.MetadataLoadContext", - "version": "7.0.0", - "hash": "sha256-VYl6SFD130K9Aw4eJH16ApJ9Sau4Xu0dcxEip2veuTI=" - }, - { - "pname": "System.Resources.Extensions", - "version": "9.0.0", - "hash": "sha256-y2gLEMuAy6QfEyNJxABC/ayMWGnwlpX735jsUQLktho=" - }, - { - "pname": "System.Security.Cryptography.Pkcs", - "version": "7.0.0", - "hash": "sha256-3J3vL9hcKSuZjT2GKappa2A9p2xJm1nH2asTNAl8ZCA=" - }, - { - "pname": "System.Security.Cryptography.Pkcs", - "version": "7.0.2", - "hash": "sha256-qS5Z/Yo8J+f3ExVX5Qkcpj1Z57oUZqz5rWa1h5bVpl8=" - }, - { - "pname": "System.Security.Cryptography.Pkcs", - "version": "9.0.0", - "hash": "sha256-AjG14mGeSc2Ka4QSelGBM1LrGBW3VJX60lnihKyJjGY=" - }, - { - "pname": "System.Security.Cryptography.ProtectedData", - "version": "9.0.0", - "hash": "sha256-gPgPU7k/InTqmXoRzQfUMEKL3QuTnOKowFqmXTnWaBQ=" - }, - { - "pname": "System.Security.Cryptography.Xml", - "version": "7.0.1", - "hash": "sha256-CH8+JVC8LyCSW75/6ZQ7ecMbSOAE1c16z4dG8JTp01w=" - }, - { - "pname": "System.Security.Cryptography.Xml", - "version": "9.0.0", - "hash": "sha256-SQJWwAFrJUddEU6JiZB52FM9tGjRlJAYH8oYVzG5IJU=" - }, - { - "pname": "System.Security.Permissions", - "version": "7.0.0", - "hash": "sha256-DOFoX+AKRmrkllykHheR8FfUXYx/Ph+I/HYuReQydXI=" - }, - { - "pname": "System.Security.Permissions", - "version": "9.0.0", - "hash": "sha256-BFrA9ottmQtLIAiKiGRbfSUpzNJwuaOCeFRDN4Z0ku0=" - }, - { - "pname": "System.Windows.Extensions", - "version": "9.0.0", - "hash": "sha256-RErD+Ju15qtnwdwB7E0SjjJGAnhXwJyC7UPcl24Z3Vs=" } ] diff --git a/extra/admin-api/nuget.config b/extra/admin-api/nuget.config
index aa87aca2..3cea9dd1 100644 --- a/extra/admin-api/nuget.config +++ b/extra/admin-api/nuget.config
@@ -2,8 +2,8 @@ <configuration> <packageSources> <!--To inherit the global NuGet package sources remove the <clear/> line below --> - <clear /> - <add key="nuget" value="https://api.nuget.org/v3/index.json" /> + <!--<clear />--> + <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="dotnet10-transport" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10-transport/nuget/v3/index.json" /> </packageSources> </configuration> diff --git a/extra/admin-api/outputs.nix b/extra/admin-api/outputs.nix
index 20ae962d..8392629a 100644 --- a/extra/admin-api/outputs.nix +++ b/extra/admin-api/outputs.nix
@@ -65,6 +65,11 @@ flake-utils.lib.eachSystem flake-utils.lib.allSystems ( in { # Interop + Spacebar-Interop-Cdn-Abstractions = makeNupkg { + name = "Spacebar.Interop.Cdn.Abstractions"; + projectFile = "Interop/Spacebar.Interop.Cdn.Abstractions/Spacebar.Interop.Cdn.Abstractions.csproj"; + nugetDeps = Interop/Spacebar.Interop.Cdn.Abstractions/deps.json; + }; Spacebar-Interop-Replication-Abstractions = makeNupkg { name = "Spacebar.Interop.Replication.Abstractions"; projectFile = "Interop/Spacebar.Interop.Replication.Abstractions/Spacebar.Interop.Replication.Abstractions.csproj"; @@ -105,6 +110,13 @@ flake-utils.lib.eachSystem flake-utils.lib.allSystems ( packNupkg = false; projectReferences = [ proj.Spacebar-Models-Db ]; }; + Spacebar-Cdn-Fsck = makeNupkg { + name = "Spacebar.Cdn.Fsck"; + projectFile = "Utilities/Spacebar.Cdn.Fsck/Spacebar.Cdn.Fsck.csproj"; + nugetDeps = Utilities/Spacebar.Cdn.Fsck/deps.json; + packNupkg = false; + projectReferences = [ proj.Spacebar-Models-Db proj.Spacebar-Interop-Cdn-Abstractions ]; + }; # Main projects Spacebar-AdminApi = makeNupkg { diff --git a/nix/modules/default/cdn-cs.nix b/nix/modules/default/cdn-cs.nix new file mode 100644
index 00000000..b123e245 --- /dev/null +++ b/nix/modules/default/cdn-cs.nix
@@ -0,0 +1,333 @@ +self: +{ + config, + lib, + pkgs, + spacebar, + ... +}: + +let + cfg = config.services.spacebarchat-server; + jsonFormat = pkgs.formats.json { }; + configFile = + let + endpointSettings = { + api = { + endpointPublic = "http${if cfg.apiEndpoint.useSsl then "s" else ""}://${cfg.apiEndpoint.host}:${toString cfg.apiEndpoint.publicPort}"; + }; + cdn = { + endpointPublic = "http${if cfg.cdnEndpoint.useSsl then "s" else ""}://${cfg.cdnEndpoint.host}:${toString cfg.cdnEndpoint.publicPort}"; + endpointPrivate = "http://127.0.0.1:${toString cfg.cdnEndpoint.localPort}"; + }; + gateway = { + endpointPublic = "ws${if cfg.gatewayEndpoint.useSsl then "s" else ""}://${cfg.gatewayEndpoint.host}:${toString cfg.gatewayEndpoint.publicPort}"; + }; + general = { + serverName = cfg.serverName; + }; + } + // ( + if cfg.enableAdmInApi then + { + adminApi = { + endpointPublic = "http${if cfg.adminApiEndpoint.useSsl then "s" else ""}://${cfg.adminApiEndpoint.host}:${toString cfg.adminApiEndpoint.publicPort}"; + }; + } + else + { } + ); + in + jsonFormat.generate "spacebarchat-server.json" (lib.recursiveUpdate endpointSettings cfg.settings); +in +{ + imports = [ + ./integration-nginx.nix + ./secrets.nix + ./users.nix + ]; + options.services.spacebarchat-server = + let + mkEndpointOptions = import ./options-subtypes/mkEndpointOptions.nix { inherit lib; }; + 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; + default = null; + description = '' + Path to a file containing a definition of the `DATABASE` environment variable database connection string. + Example content: `DATABASE=postgres://username:password@host-IP:port/databaseName`. + See https://docs.spacebar.chat/setup/server/database/. + ''; + }; + + serverName = lib.mkOption { + type = lib.types.str; + description = "The server name for this Spacebar instance (aka. common name, usually the domain where your well known is hosted)."; + }; + adminApiEndpoint = mkEndpointOptions "admin-api.sb.localhost" 3004; + apiEndpoint = mkEndpointOptions "api.sb.localhost" 3001; + gatewayEndpoint = mkEndpointOptions "gateway.sb.localhost" 3003; + cdnEndpoint = mkEndpointOptions "cdn.sb.localhost" 3003; + cdnPath = lib.mkOption { + type = lib.types.str; + default = "./files"; + description = "Path to store CDN files."; + }; + + extraEnvironment = lib.mkOption { + default = { }; + description = '' + Environment variables passed to spacebarchat-server. + See https://docs.spacebar.chat/setup/server/configuration/env for supported values. + ''; + type = lib.types.submodule { + freeformType = + with lib.types; + attrsOf (oneOf [ + str + bool + int + ]); + options = { + THREADS = lib.mkOption { + type = lib.types.ints.positive; + default = 1; + description = "Number of threads to run Spacebar on when using bundle. Make sure you've enabled RabbitMQ if using more than one."; + }; + }; + }; + }; + + settings = lib.mkOption { + type = jsonFormat.type; + default = { }; + description = '' + Configuration for spacebarchat-server. + See https://docs.spacebar.chat/setup/server/configuration for supported values. + ''; + }; + }; + + config = lib.mkIf cfg.enable ( + let + makeServerTsService = ( + conf: + lib.recursiveUpdate + (lib.recursiveUpdate { + documentation = [ "https://docs.spacebar.chat/" ]; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + environment = + { } + // (if cfg.cdnSignaturePath != null then { CDN_SIGNATURE_PATH = "%d/cdnSignature"; } else { }) + // (if cfg.legacyJwtSecretPath != null then { LEGACY_JWT_SECRET_PATH = "%d/legacyJwtSecret"; } else { }) + // (if cfg.mailjetApiKeyPath != null then { MAILJET_API_KEY_PATH = "%d/mailjetApiKey"; } else { }) + // (if cfg.mailjetApiSecretPath != null then { MAILJET_API_SECRET_PATH = "%d/mailjetApiSecret"; } else { }) + // (if cfg.smtpPasswordPath != null then { SMTP_PASSWORD_PATH = "%d/smtpPassword"; } else { }) + // (if cfg.gifApiKeyPath != null then { GIF_API_KEY_PATH = "%d/gifApiKey"; } else { }) + // (if cfg.rabbitmqHostPath != null then { RABBITMQ_HOST_PATH = "%d/rabbitmqHost"; } else { }) + // (if cfg.abuseIpDbApiKeyPath != null then { ABUSE_IP_DB_API_KEY_PATH = "%d/abuseIpDbApiKey"; } else { }) + // (if cfg.captchaSecretKeyPath != null then { CAPTCHA_SECRET_KEY_PATH = "%d/captchaSecretKey"; } else { }) + // (if cfg.captchaSiteKeyPath != null then { CAPTCHA_SITE_KEY_PATH = "%d/captchaSiteKey"; } else { }) + // (if cfg.ipdataApiKeyPath != null then { IPDATA_API_KEY_PATH = "%d/ipdataApiKey"; } else { }) + // (if cfg.requestSignaturePath != null then { REQUEST_SIGNATURE_PATH = "%d/requestSignature"; } else { }); + serviceConfig = { + LoadCredential = + [ ] + ++ (if cfg.cdnSignaturePath != null then [ "cdnSignature:${cfg.cdnSignaturePath}" ] else [ ]) + ++ (if cfg.legacyJwtSecretPath != null then [ "legacyJwtSecret:${cfg.legacyJwtSecretPath}" ] else [ ]) + ++ (if cfg.mailjetApiKeyPath != null then [ "mailjetApiKey:${cfg.mailjetApiKeyPath}" ] else [ ]) + ++ (if cfg.mailjetApiSecretPath != null then [ "mailjetApiSecret:${cfg.mailjetApiSecretPath}" ] else [ ]) + ++ (if cfg.smtpPasswordPath != null then [ "smtpPassword:${cfg.smtpPasswordPath}" ] else [ ]) + ++ (if cfg.gifApiKeyPath != null then [ "gifApiKey:${cfg.gifApiKeyPath}" ] else [ ]) + ++ (if cfg.rabbitmqHostPath != null then [ "rabbitmqHost:${cfg.rabbitmqHostPath}" ] else [ ]) + ++ (if cfg.abuseIpDbApiKeyPath != null then [ "abuseIpDbApiKey:${cfg.abuseIpDbApiKeyPath}" ] else [ ]) + ++ (if cfg.captchaSecretKeyPath != null then [ "captchaSecretKey:${cfg.captchaSecretKeyPath}" ] else [ ]) + ++ (if cfg.captchaSiteKeyPath != null then [ "captchaSiteKey:${cfg.captchaSiteKeyPath}" ] else [ ]) + ++ (if cfg.ipdataApiKeyPath != null then [ "ipdataApiKey:${cfg.ipdataApiKeyPath}" ] else [ ]) + ++ (if cfg.requestSignaturePath != null then [ "requestSignature:${cfg.requestSignaturePath}" ] else [ ]); + + User = "spacebarchat"; + Group = "spacebarchat"; + DynamicUser = false; + + LockPersonality = true; + NoNewPrivileges = true; + + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateUsers = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "@chown" # Required for copying files with FICLONE, apparently. + ]; + CapabilityBoundingSet = [ + "~CAP_SYS_ADMIN" + "~CAP_AUDIT_*" + "~CAP_NET_(BIND_SERVICE|BROADCAST|RAW)" + "~CAP_NET_ADMIN" # No use for this as we don't currently use iptables for enforcing instance bans + "~CAP_SYS_TIME" + "~CAP_KILL" + "~CAP_(DAC_*|FOWNER|IPC_OWNER)" + "~CAP_LINUX_IMMUTABLE" + "~CAP_IPC_LOCK" + "~CAP_BPF" + "~CAP_SYS_TTY_CONFIG" + "~CAP_SYS_BOOT" + "~CAP_SYS_CHROOT" + "~CAP_BLOCK_SUSPEND" + "~CAP_LEASE" + "~CAP_(CHOWN|FSETID|FSETFCAP)" # Check if we need CAP_CHOWN for `fchown()` (FICLONE)? + "~CAP_SET(UID|GID|PCAP)" + "~CAP_MAC_*" + "~CAP_SYS_PTRACE" + "~CAP_SYS_(NICE|RESOURCE)" + "~CAP_SYS_RAWIO" + "~CAP_SYSLOG" + ]; + RestrictSUIDSGID = true; + + WorkingDirectory = "/var/lib/spacebar"; + StateDirectory = "spacebar"; + StateDirectoryMode = "0750"; + RuntimeDirectory = "spacebar"; + RuntimeDirectoryMode = "0750"; + ReadWritePaths = [ cfg.cdnPath ]; + NoExecPaths = [ cfg.cdnPath ]; + + Restart = "on-failure"; + RestartSec = 10; + StartLimitBurst = 5; + UMask = "077"; + } + // lib.optionalAttrs (cfg.databaseFile != null) { EnvironmentFile = cfg.databaseFile; }; + } conf) + { + } + ); + in + { + assertions = [ + # { + # assertion = lib.all (map (key: !(key == "CONFIG_PATH" || key == "CONFIG_READONLY" || key == "PORT" || key == "STORAGE_LOCATION")) (lib.attrNames cfg.extraEnvironment)); + # message = "You cannot set CONFIG_PATH, CONFIG_READONLY, PORT or STORAGE_LOCATION in extraEnvironment, these are managed by the NixOS module."; + # } + ]; + + systemd.services.spacebar-apply-migrations = makeServerTsService { + description = "Spacebar Server - Apply DB migrations"; + # after = lib.optional config.services.postgresql.enable "postgresql.service"; + # requires = lib.optional config.services.postgresql.enable "postgresql.service"; + environment = builtins.mapAttrs (_: val: builtins.toString val) ( + cfg.extraEnvironment + // { + # things we force... + CONFIG_PATH = configFile; + CONFIG_READONLY = 1; + } + ); + serviceConfig = { + ExecStart = "${cfg.package}/bin/apply-migrations"; + Type = "oneshot"; + RemainAfterExit = true; + TimeoutStartSec = 15; + RestartSec = 1; + StartLimitBurst = 15; + }; + }; + + systemd.services.spacebar-api = makeServerTsService { + description = "Spacebar Server - API"; + after = [ "spacebar-apply-migrations.service" ]; + requires = [ "spacebar-apply-migrations.service" ]; + 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; + PORT = toString cfg.apiEndpoint.localPort; + STORAGE_LOCATION = cfg.cdnPath; + } + ); + serviceConfig = { + ExecStart = "${cfg.package}/bin/start-api"; + }; + }; + + systemd.services.spacebar-gateway = makeServerTsService { + description = "Spacebar Server - Gateway"; + after = [ "spacebar-apply-migrations.service" ]; + requires = [ "spacebar-apply-migrations.service" ]; + 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; + PORT = toString cfg.gatewayEndpoint.localPort; + STORAGE_LOCATION = cfg.cdnPath; + } + ); + serviceConfig = { + ExecStart = "${cfg.package}/bin/start-gateway"; + }; + }; + + systemd.services.spacebar-cdn = lib.mkIf (!cfg.useCdnCs) (makeServerTsService { + description = "Spacebar Server - CDN"; + after = [ "spacebar-apply-migrations.service" ]; + requires = [ "spacebar-apply-migrations.service" ]; + 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; + PORT = toString cfg.cdnEndpoint.localPort; + STORAGE_LOCATION = cfg.cdnPath; + } + ); + serviceConfig = { + ExecStart = "${cfg.package}/bin/start-cdn"; + }; + }); + } + ); +} diff --git a/nix/modules/default/default.nix b/nix/modules/default/default.nix
index 6bd1045a..db022e40 100644 --- a/nix/modules/default/default.nix +++ b/nix/modules/default/default.nix
@@ -10,26 +10,39 @@ self: let cfg = config.services.spacebarchat-server; jsonFormat = pkgs.formats.json { }; - configFile = jsonFormat.generate "spacebarchat-server.json" ( - lib.recursiveUpdate { - api = { - endpointPublic = "http${if cfg.apiEndpoint.useSsl then "s" else ""}://${cfg.apiEndpoint.host}:${toString cfg.apiEndpoint.publicPort}"; - }; - cdn = { - endpointPublic = "http${if cfg.cdnEndpoint.useSsl then "s" else ""}://${cfg.cdnEndpoint.host}:${toString cfg.cdnEndpoint.publicPort}"; - endpointPrivate = "http://127.0.0.1:${toString cfg.cdnEndpoint.localPort}"; - }; - gateway = { - endpointPublic = "ws${if cfg.gatewayEndpoint.useSsl then "s" else ""}://${cfg.gatewayEndpoint.host}:${toString cfg.gatewayEndpoint.publicPort}"; - }; - general = { - serverName = cfg.serverName; - }; - } cfg.settings - ); + configFile = + let + endpointSettings = { + api = { + endpointPublic = "http${if cfg.apiEndpoint.useSsl then "s" else ""}://${cfg.apiEndpoint.host}:${toString cfg.apiEndpoint.publicPort}"; + }; + cdn = { + endpointPublic = "http${if cfg.cdnEndpoint.useSsl then "s" else ""}://${cfg.cdnEndpoint.host}:${toString cfg.cdnEndpoint.publicPort}"; + endpointPrivate = "http://127.0.0.1:${toString cfg.cdnEndpoint.localPort}"; + }; + gateway = { + endpointPublic = "ws${if cfg.gatewayEndpoint.useSsl then "s" else ""}://${cfg.gatewayEndpoint.host}:${toString cfg.gatewayEndpoint.publicPort}"; + }; + general = { + serverName = cfg.serverName; + }; + } + // ( + if cfg.enableAdmInApi then + { + adminApi = { + endpointPublic = "http${if cfg.adminApiEndpoint.useSsl then "s" else ""}://${cfg.adminApiEndpoint.host}:${toString cfg.adminApiEndpoint.publicPort}"; + }; + } + else + { } + ); + in + jsonFormat.generate "spacebarchat-server.json" (lib.recursiveUpdate endpointSettings cfg.settings); in { imports = [ + ./cdn-cs.nix ./integration-nginx.nix ./secrets.nix ./users.nix @@ -41,6 +54,7 @@ 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; @@ -56,6 +70,7 @@ in type = lib.types.str; description = "The server name for this Spacebar instance (aka. common name, usually the domain where your well known is hosted)."; }; + adminApiEndpoint = mkEndpointOptions "admin-api.sb.localhost" 3004; apiEndpoint = mkEndpointOptions "api.sb.localhost" 3001; gatewayEndpoint = mkEndpointOptions "gateway.sb.localhost" 3003; cdnEndpoint = mkEndpointOptions "cdn.sb.localhost" 3003; @@ -223,8 +238,8 @@ in systemd.services.spacebar-apply-migrations = makeServerTsService { description = "Spacebar Server - Apply DB migrations"; -# after = lib.optional config.services.postgresql.enable "postgresql.service"; -# requires = lib.optional config.services.postgresql.enable "postgresql.service"; + # after = lib.optional config.services.postgresql.enable "postgresql.service"; + # requires = lib.optional config.services.postgresql.enable "postgresql.service"; environment = builtins.mapAttrs (_: val: builtins.toString val) ( cfg.extraEnvironment // { @@ -291,7 +306,7 @@ in }; }; - systemd.services.spacebar-cdn = makeServerTsService { + systemd.services.spacebar-cdn = lib.mkIf (!cfg.useCdnCs) (makeServerTsService { description = "Spacebar Server - CDN"; after = [ "spacebar-apply-migrations.service" ]; requires = [ "spacebar-apply-migrations.service" ]; @@ -313,7 +328,7 @@ in serviceConfig = { ExecStart = "${cfg.package}/bin/start-cdn"; }; - }; + }); } ); }