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 @@
"Node.js.Server.ts.executor": "Debug",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
+ "RunOnceActivity.typescript.service.memoryLimit.init": "true",
"git-widget-placeholder": "master",
"javascript.nodejs.core.library.configured.version": "24.11.1",
- "javascript.nodejs.core.library.typings.version": "24.10.4",
- "last_opened_file_path": "/home/Rory/git/spacebar/server-master/src/util/migration/postgres",
+ "javascript.nodejs.core.library.typings.version": "24.10.9",
+ "last_opened_file_path": "/home/Rory/git/spacebar/server-master/nix/modules/default",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.standard": "true",
"node.js.selected.package.eslint": "(autodetect)",
@@ -80,7 +86,6 @@
"npm.build:src.executor": "Run",
"npm.start.executor": "Debug",
"prettierjs.PrettierConfiguration.Package": "/home/Rory/git/spacebar/server-master/node_modules/prettier",
- "settings.editor.selected.configurable": "com.github.copilot.settings.customInstructions.CopilotInstructionsConfigurable",
"ts.external.directory.path": "/home/Rory/git/spacebar/server-master/node_modules/typescript/lib"
},
"keyToStringList": {
@@ -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";
};
- };
+ });
}
);
}
|