mirror of
https://github.com/spacebarchat/server.git
synced 2026-03-30 13:55:39 +00:00
C# dependency updates, CDN-CS work
This commit is contained in:
20
.idea/workspace.xml
generated
20
.idea/workspace.xml
generated
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
6
extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml
generated
Normal file
6
extra/admin-api/.idea/.idea.SpacebarAdminAPI/.idea/sqldialects.xml
generated
Normal file
@@ -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>
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.11.31",
|
||||
"hash": "sha256-YS4oASrmC5dmZrx5JPS7SfKmUpIJErlUpVDsU3VrfFE="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Build.Framework",
|
||||
"version": "17.14.28",
|
||||
"hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM="
|
||||
},
|
||||
{
|
||||
"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,29 +61,19 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"version": "10.0.0",
|
||||
"hash": "sha256-vOP2CE5YA551BlpbOuIy6RuAiAEPEpCVS1cEE33/zN4="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.EntityFrameworkCore.Relational",
|
||||
"version": "10.0.1",
|
||||
"hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY="
|
||||
"version": "10.0.2",
|
||||
"hash": "sha256-Y4jPpoYhKizg5wF6QfkBX4sYlE2FU1bYhfoDN3xkhKM="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Caching.Abstractions",
|
||||
"version": "10.0.2",
|
||||
"hash": "sha256-nKmQuZTt1g5/8gBajo7wdCV64kdCucdiQR8JTt7ZZb0="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Caching.Memory",
|
||||
"version": "10.0.1",
|
||||
"hash": "sha256-Qb7xK6VEZDas0lJFaW1suKdFjtkSYwLHHxkQEfWIU2A="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Caching.Memory",
|
||||
"version": "10.0.2",
|
||||
@@ -116,8 +81,8 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -179,11 +139,6 @@
|
||||
"version": "9.0.0",
|
||||
"hash": "sha256-DT5euAQY/ItB5LPI8WIp6Dnd0lSvBRP35vFkOXC68ck="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Primitives",
|
||||
"version": "10.0.1",
|
||||
"hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Primitives",
|
||||
"version": "10.0.2",
|
||||
@@ -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",
|
||||
@@ -229,16 +179,6 @@
|
||||
"version": "6.0.0",
|
||||
"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",
|
||||
@@ -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="
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Spacebar.AdminApi.TestClient.Services.Services;
|
||||
|
||||
public class CdnFileStorageRebuildService() {
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
117
extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs
Normal file
117
extra/admin-api/Utilities/Spacebar.Cdn.Fsck/FsckService.cs
Normal file
@@ -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
|
||||
}
|
||||
21
extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs
Normal file
21
extra/admin-api/Utilities/Spacebar.Cdn.Fsck/Program.cs
Normal file
@@ -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();
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json
Normal file
1
extra/admin-api/Utilities/Spacebar.Cdn.Fsck/deps.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -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.11.31",
|
||||
"hash": "sha256-YS4oASrmC5dmZrx5JPS7SfKmUpIJErlUpVDsU3VrfFE="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Build.Framework",
|
||||
"version": "17.14.28",
|
||||
"hash": "sha256-7RzEyIipumafwLW1xN1q23114NafG6PT0+RADElNsiM="
|
||||
},
|
||||
{
|
||||
"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,24 +76,14 @@
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.EntityFrameworkCore.Relational",
|
||||
"version": "10.0.1",
|
||||
"hash": "sha256-zLgxr/iW9HP8Fip1IDgr7X0Ar8OWKDvVmoEt65gG6VY="
|
||||
"version": "10.0.2",
|
||||
"hash": "sha256-Y4jPpoYhKizg5wF6QfkBX4sYlE2FU1bYhfoDN3xkhKM="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Caching.Abstractions",
|
||||
"version": "10.0.2",
|
||||
"hash": "sha256-nKmQuZTt1g5/8gBajo7wdCV64kdCucdiQR8JTt7ZZb0="
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -129,16 +94,6 @@
|
||||
"version": "10.0.2",
|
||||
"hash": "sha256-dBJAKDyp/sm+ZSMQfH0+4OH8Jnv1s20aHlWS6HNnH+c="
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -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",
|
||||
@@ -229,16 +184,6 @@
|
||||
"version": "10.0.2",
|
||||
"hash": "sha256-mkeKUXepn4bfEdZFXdURmNEFdGiHQdpcxnm6joG+pUA="
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -304,11 +249,6 @@
|
||||
"version": "10.0.2",
|
||||
"hash": "sha256-WJahsWyT5wYdLPEJufHKpb3l/dl7D2iw2SnMK0Jr53U="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Primitives",
|
||||
"version": "10.0.1",
|
||||
"hash": "sha256-EXmukq09erT4s+miQpBSYy3IY4HxxKlwEPL43/KoyEc="
|
||||
},
|
||||
{
|
||||
"pname": "Microsoft.Extensions.Primitives",
|
||||
"version": "10.0.2",
|
||||
@@ -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",
|
||||
@@ -354,16 +289,6 @@
|
||||
"version": "6.0.0",
|
||||
"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",
|
||||
@@ -394,84 +319,9 @@
|
||||
"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": "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="
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
333
nix/modules/default/cdn-cs.nix
Normal file
333
nix/modules/default/cdn-cs.nix
Normal file
@@ -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";
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user