diff --git a/extra/admin-api/Interop/Spacebar.Interop.Authentication/deps.json b/extra/admin-api/Interop/Spacebar.Interop.Authentication/deps.json
index 85c76a7b..89c9ffe6 100644
--- a/extra/admin-api/Interop/Spacebar.Interop.Authentication/deps.json
+++ b/extra/admin-api/Interop/Spacebar.Interop.Authentication/deps.json
@@ -1,13 +1,13 @@
[
{
"pname": "ArcaneLibs",
- "version": "1.0.1-preview.20260420-212318",
- "hash": "sha256-y+gU8S3jbcFgIwwc0vkkEPMfHzGZ+55//9uvsKOEGXY="
+ "version": "1.0.1-preview.20260619-035441",
+ "hash": "sha256-jNEGd8Ccgkk4A8ZMaJO4QDExmsf9q7TeuhiNESos3Q4="
},
{
"pname": "BCrypt.Net-Next",
- "version": "4.1.0",
- "hash": "sha256-Efjrsw4FXgVKA2vQV6ztc40pLsonnV3JxwxW7eOyfDk="
+ "version": "4.2.0",
+ "hash": "sha256-dqDg0iQQUjncCN+bIASlsrxt4oEfyg/JkPu34wSRpc4="
},
{
"pname": "Microsoft.EntityFrameworkCore",
@@ -141,13 +141,13 @@
},
{
"pname": "Npgsql",
- "version": "10.0.2",
- "hash": "sha256-hW03ZWoW7y16vvScwsjZNyqFybGy+s6hQYQXebo78yQ="
+ "version": "10.0.3",
+ "hash": "sha256-ddCXCSOoyfy7035OvnL+4LEDYqHjZyPoZ3ffG2coMW0="
},
{
"pname": "Npgsql.EntityFrameworkCore.PostgreSQL",
- "version": "10.0.1",
- "hash": "sha256-G5WmWoc02gHTsdBLXESFQ5eMV+liwiO8YjzFKg4NDEk="
+ "version": "10.0.2",
+ "hash": "sha256-JlJXttrCmPFUGVznTGl7hsfn9/BzKt91y/uLErDqQfY="
},
{
"pname": "System.IdentityModel.Tokens.Jwt",
diff --git a/extra/admin-api/Models/Spacebar.Models.Api/TypingResponse.cs b/extra/admin-api/Models/Spacebar.Models.Api/TypingResponse.cs
index e69de29b..99a1d115 100644
--- a/extra/admin-api/Models/Spacebar.Models.Api/TypingResponse.cs
+++ b/extra/admin-api/Models/Spacebar.Models.Api/TypingResponse.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Serialization;
+
+namespace Spacebar.Models.Api;
+
+public class TypingResponse {
+ [JsonPropertyName("message_send_cooldown_ms")]
+ public int? MessageSendCooldownMilliseconds { get; set; }
+
+ [JsonPropertyName("thread_create_cooldown_ms")]
+ public int? ThreadCreateCooldownMilliseconds { get; set; }
+
+ [JsonIgnore]
+ public TimeSpan? MessageSendCooldown {
+ get => MessageSendCooldownMilliseconds is null ? null : TimeSpan.FromMilliseconds((long)MessageSendCooldownMilliseconds);
+ set => MessageSendCooldownMilliseconds = (int?)value?.TotalMilliseconds;
+ }
+
+ [JsonIgnore]
+ public TimeSpan? ThreadCreateCooldown {
+ get => ThreadCreateCooldownMilliseconds is null ? null : TimeSpan.FromMilliseconds((long)ThreadCreateCooldownMilliseconds);
+ set => ThreadCreateCooldownMilliseconds = (int?)value?.TotalMilliseconds;
+ }
+}
\ No newline at end of file
diff --git a/extra/admin-api/Spacebar.Cdn.Worker/DiscordImageResizeService.cs b/extra/admin-api/Spacebar.Cdn.Worker/DiscordImageResizeService.cs
index 6509a0b6..5a70bfd6 100644
--- a/extra/admin-api/Spacebar.Cdn.Worker/DiscordImageResizeService.cs
+++ b/extra/admin-api/Spacebar.Cdn.Worker/DiscordImageResizeService.cs
@@ -14,9 +14,10 @@ public class DiscordImageResizeParams {
public bool SpacebarAllowUpscale { get; set; } = false;
public bool SpacebarOptimiseGif { get; set; } = true;
+ public string Format { get; set; } = "webp";
public string ToSerializedName() {
- return $"{(Animated ? "a_" : "")}{Size}px_{Quality.ToString()}_u.{SpacebarAllowUpscale}_o.{SpacebarOptimiseGif}";
+ return $"{(Animated ? "a_" : "")}{Size}px_{Quality.ToString()}_u.{SpacebarAllowUpscale}_o.{SpacebarOptimiseGif}.{Format}";
}
}
@@ -33,6 +34,7 @@ public static class HttpRequestExtensions {
Animated = request.Query.ContainsKey("animated") && bool.TryParse(request.Query["animated"], out bool an) && an,
SpacebarAllowUpscale = request.Query.ContainsKey("allowUpscale") && bool.TryParse(request.Query["allowUpscale"], out bool au) && au,
SpacebarOptimiseGif = request.Query.ContainsKey("optimiseGif") && bool.TryParse(request.Query["optimiseGif"], out bool og) && og,
+ Format = request.Query.ContainsKey("format") ? request.Query["format"]! : "webp",
};
}
}
@@ -72,7 +74,7 @@ public class DiscordImageResizeService {
if (!resizeParams.Animated) {
var oldImg = img;
- img = new MagickImageCollection([oldImg.First()]);
+ img = new MagickImageCollection([oldImg.First().Clone()]);
oldImg.Dispose();
}
diff --git a/extra/admin-api/Spacebar.Cdn.Worker/Program.cs b/extra/admin-api/Spacebar.Cdn.Worker/Program.cs
index 289b869b..60d2a447 100644
--- a/extra/admin-api/Spacebar.Cdn.Worker/Program.cs
+++ b/extra/admin-api/Spacebar.Cdn.Worker/Program.cs
@@ -96,7 +96,7 @@ app.MapGet("/scale/{*path}", async (HttpContext ctx, IFileSource ifs, DiscordIma
await mig.ReadAsync(f.Stream, ctx.RequestAborted);
var res = await dirs.Apply(mig, ctx.Request.GetResizeParams());
await ctx.Response.StartAsync();
- await res.WriteAsync(ctx.Response.Body);
+ await res.WriteAsync(ctx.Response.Body, Mimes.GetFormatForExtension(ctx.Request.GetResizeParams().Format));
await ctx.Response.CompleteAsync();
});
diff --git a/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj b/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj
index b521b61c..508e160d 100644
--- a/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj
+++ b/extra/admin-api/Tests/Spacebar.Tests/Spacebar.Tests.csproj
@@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
+ <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260619-035441" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/ChannelTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/ChannelTests.cs
index 0bae6d34..080a991e 100644
--- a/extra/admin-api/Tests/Spacebar.Tests/Tests/ChannelTests.cs
+++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/ChannelTests.cs
@@ -1,4 +1,5 @@
using System.Net.Http.Json;
+using System.Text.Json.Nodes;
using Spacebar.Models.Generic;
using Spacebar.Sdk.Core;
using Spacebar.Tests.Abstractions;
@@ -53,4 +54,34 @@ public class ChannelTests(ITestOutputHelper testOutputHelper, TestFixture fixtur
public async Task SendTyping() {
await Assert.HttpSuccess(await Client.ApiHttpClient.PostAsync($"channels/{Channel!.Id}/typing", null, TestContext.Current.CancellationToken));
}
+
+ [Fact]
+ public async Task UpdateChannel() {
+ var cg = Client.GetGuild(Guild.Id);
+ var nc = await cg.CreateChannelAsync(new() {
+ Name = "asdf",
+ Type = 0
+ });
+
+ var res = await Assert.HttpSuccess(await Client.ApiHttpClient.PatchAsJsonAsync($"channels/{nc.Id}", new JsonObject() {
+ { "name", "hellorld" }
+ }, cancellationToken: TestContext.Current.CancellationToken));
+
+ var resC = await res.Content.ReadFromJsonAsync<Channel>(cancellationToken: TestContext.Current.CancellationToken);
+ Assert.Equal("hellorld", resC!.Name);
+
+ var c = await Client.GetChannel(nc.Id).GetInfoAsync();
+ Assert.Equal("hellorld", c.Name);
+ }
+
+ [Fact]
+ public async Task DeleteChannel() {
+ var cg = Client.GetGuild(Guild.Id);
+ var c = await cg.CreateChannelAsync(new() {
+ Name = "asdf",
+ Type = 0
+ });
+
+ await Assert.HttpSuccess(await Client.ApiHttpClient.DeleteAsync($"channels/{c.Id}", TestContext.Current.CancellationToken));
+ }
}
\ No newline at end of file
diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/DMTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/DMTests.cs
new file mode 100644
index 00000000..76f147d1
--- /dev/null
+++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/DMTests.cs
@@ -0,0 +1,84 @@
+using System.Net.Http.Json;
+using System.Text.Json.Nodes;
+using Spacebar.Models.Generic;
+using Spacebar.Sdk.Core;
+using Spacebar.Tests.Abstractions;
+using Spacebar.Tests.Extensions;
+using Spacebar.Tests.Fixtures;
+using Xunit.Microsoft.DependencyInjection.Abstracts;
+
+namespace Spacebar.Tests.Tests;
+
+public class DmTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : TestBed<TestFixture>(testOutputHelper, fixture), IAsyncLifetime {
+ private readonly Config _config = fixture.GetRequiredService<Config>(testOutputHelper);
+ private readonly SpacebarClientWellKnownResolverService _wellKnownResolver = fixture.GetRequiredService<SpacebarClientWellKnownResolverService>(testOutputHelper);
+ private readonly SpacebarClientProviderService _clientProvider = fixture.GetRequiredService<SpacebarClientProviderService>(testOutputHelper);
+ private readonly UserAbstraction _userAbstraction = fixture.GetRequiredService<UserAbstraction>(testOutputHelper);
+
+ private static AuthenticatedSpacebarClient Client { get; set; } = null!;
+ private static AuthenticatedSpacebarClient? Client2 { get; set; } = null!;
+ private static Channel? Channel { get; set; }
+
+ public async ValueTask InitializeAsync() {
+ testOutputHelper.WriteLine("Running InitializeAsync");
+ // All these tests can share a single client
+ Client = await _userAbstraction.GetSharedUser();
+ Client2 ??= await _userAbstraction.GetFreshUser();
+ // Channel ??= await Client.GetGuild(Guild.Id).CreateChannelAsync(new() { Name = "meow", Type = 0 });
+ }
+
+ // [Fact]
+ // public async Task CreateChannel() {
+ // Assert.Equal("Test guild", Guild!.Name);
+ // var channel = await Client!.GetGuild(Guild.Id).CreateChannelAsync(new() {
+ // Name = "test",
+ // Type = 0 // TODO: this should be the default
+ // });
+ //
+ // Assert.Equal("test", channel.Name);
+ // }
+ //
+ // [Fact]
+ // public async Task GetChannel() {
+ // var res = await Assert.HttpSuccess(await Client.ApiHttpClient.GetAsync("channels/" + Channel.Id, TestContext.Current.CancellationToken));
+ //
+ // var channelResp = await res.Content.ReadFromJsonAsync<Channel>(cancellationToken: TestContext.Current.CancellationToken);
+ // Assert.Equal(Channel.Name, channelResp!.Name);
+ // Assert.Equal(Channel.Id, channelResp!.Id);
+ // }
+ //
+ // [Fact]
+ // public async Task SendTyping() {
+ // await Assert.HttpSuccess(await Client.ApiHttpClient.PostAsync($"channels/{Channel!.Id}/typing", null, TestContext.Current.CancellationToken));
+ // }
+ //
+ // [Fact]
+ // public async Task UpdateChannel() {
+ // var cg = Client.GetGuild(Guild.Id);
+ // var nc = await cg.CreateChannelAsync(new() {
+ // Name = "asdf",
+ // Type = 0
+ // });
+ //
+ // var res = await Assert.HttpSuccess(await Client.ApiHttpClient.PatchAsJsonAsync($"channels/{nc.Id}", new JsonObject() {
+ // { "name", "hellorld" }
+ // }, cancellationToken: TestContext.Current.CancellationToken));
+ //
+ // var resC = await res.Content.ReadFromJsonAsync<Channel>(cancellationToken: TestContext.Current.CancellationToken);
+ // Assert.Equal("hellorld", resC!.Name);
+ //
+ // var c = await Client.GetChannel(nc.Id).GetInfoAsync();
+ // Assert.Equal("hellorld", c.Name);
+ // }
+ //
+ // [Fact]
+ // public async Task DeleteChannel() {
+ // var cg = Client.GetGuild(Guild.Id);
+ // var c = await cg.CreateChannelAsync(new() {
+ // Name = "asdf",
+ // Type = 0
+ // });
+ //
+ // await Assert.HttpSuccess(await Client.ApiHttpClient.DeleteAsync($"channels/{c.Id}", TestContext.Current.CancellationToken));
+ // }
+}
\ No newline at end of file
diff --git a/extra/admin-api/Tests/Spacebar.Tests/Tests/GuildTests.cs b/extra/admin-api/Tests/Spacebar.Tests/Tests/GuildTests.cs
index 7d8fae17..9447ad96 100644
--- a/extra/admin-api/Tests/Spacebar.Tests/Tests/GuildTests.cs
+++ b/extra/admin-api/Tests/Spacebar.Tests/Tests/GuildTests.cs
@@ -1,4 +1,6 @@
-using ArcaneLibs.Extensions;
+using System.Net.Http.Json;
+using System.Text.Json.Nodes;
+using ArcaneLibs.Extensions;
using Spacebar.Models.Generic;
using Spacebar.Sdk.Core;
using Spacebar.Tests.Abstractions;
@@ -56,7 +58,29 @@ public class GuildTests(ITestOutputHelper testOutputHelper, TestFixture fixture)
[Fact]
public async Task SetChannelOrder() {
var cg = Client.GetGuild(Guild.Id);
+
+ await Task.WhenAll(Enumerable.Range(1, 10).Select(x => cg.CreateChannelAsync(new() {
+ Name = Guid.NewGuid().ToString(),
+ Type = 0
+ })));
+
var guildChannels = await cg.GetChannelsAsync();
- // testOutputHelper.WriteLine(guildChannels.Select(x=>(x.Id, x.Name)).ToJson(includeFields: true));
+ testOutputHelper.WriteLine(guildChannels.Select(x=>(x.Id, x.Name, x.Position)).ToJson(includeFields: true));
+
+ var payload = new JsonArray();
+
+ var i = 0;
+ foreach (var c in guildChannels.OrderBy(x=>x.Name)) {
+ payload.Add(new JsonObject() {
+ {"id", c.Id.ToString()},
+ {"position", i++}
+ });
+ }
+
+ await Assert.HttpSuccess(await Client.ApiHttpClient.PatchAsJsonAsync($"guilds/{cg.Id}/channels", payload, cancellationToken: TestContext.Current.CancellationToken));
+
+ var newGuildChannels = await cg.GetChannelsAsync();
+ testOutputHelper.WriteLine(newGuildChannels.Select(x=>(x.Id, x.Name, x.Position)).ToJson(includeFields: true));
+ Assert.Equal(guildChannels.OrderBy(x=>x.Name), newGuildChannels, (a, b) => a.Id == b.Id);
}
}
\ No newline at end of file
diff --git a/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs b/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs
index 2b4a4ba4..2058e21f 100644
--- a/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs
+++ b/extra/admin-api/Utilities/Spacebar.Sdk/Core/SpacebarClient.cs
@@ -114,6 +114,20 @@ public class SpacebarClientChannel(AuthenticatedSpacebarClient client, long chan
if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync<JsonObject>())!);
return (await resp.Content.ReadFromJsonAsync<List<Webhook>>())!;
}
+
+ public async Task<TypingResponse> SendTypingAsync() {
+ var resp = await client.ApiHttpClient.GetAsync($"channels/{channelId}/webhooks");
+ // TODO: abstract out
+ if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync<JsonObject>())!);
+ return (await resp.Content.ReadFromJsonAsync<TypingResponse>())!;
+ }
+
+ public async Task<Channel> GetInfoAsync() {
+ var resp = await client.ApiHttpClient.GetAsync($"channels/{channelId}");
+ // TODO: abstract out
+ if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync<JsonObject>())!);
+ return (await resp.Content.ReadFromJsonAsync<Channel>())!;
+ }
}
public class SpacebarClientGuild(AuthenticatedSpacebarClient client, long guildId) {
diff --git a/extra/admin-api/Utilities/Spacebar.Sdk/Spacebar.Sdk.csproj b/extra/admin-api/Utilities/Spacebar.Sdk/Spacebar.Sdk.csproj
index fb9f9d9e..4cc3e6d2 100644
--- a/extra/admin-api/Utilities/Spacebar.Sdk/Spacebar.Sdk.csproj
+++ b/extra/admin-api/Utilities/Spacebar.Sdk/Spacebar.Sdk.csproj
@@ -12,4 +12,9 @@
<ProjectReference Include="..\..\Models\Spacebar.Models.Gateway\Spacebar.Models.Gateway.csproj"/>
<ProjectReference Include="..\..\Models\Spacebar.Models.Generic\Spacebar.Models.Generic.csproj"/>
</ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.20260619-035441" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.9" />
+ </ItemGroup>
</Project>
diff --git a/scripts/validateImports.cs b/scripts/validateImports.cs
index ac301923..9008ecdb 100755
--- a/scripts/validateImports.cs
+++ b/scripts/validateImports.cs
@@ -22,24 +22,42 @@ void LogError(string fileName, ImportReference import)
foreach (var f in ReadDirRecursive("./src"))
{
- if (f.EndsWith(".test.ts"))
- {
- }
+ if (f.EndsWith(".test.ts")) continue; //ignore for now
else
await foreach (var import in GetImports(f))
{
// These files are completely idempotent and safe to import
- if (import.ImportSource.EndsWith("util/util/ProcessLifeCycle") && import.ImportEntities.SequenceEqual(["ProcessLifecycle"])) continue;
+ if (import.ImportSource.EndsWith("util/util/ProcessLifeCycle") &&
+ import.ImportEntities.SequenceEqual(["ProcessLifecycle"])) continue;
+
+ // Technically valid but what the heck
+ if (import.ImportSource.StartsWith('#')) LogError(f, import);
// file level
- if (f == "./src/schemas/Validator.ts")
+ else if (new FileInfo(f).Name == "start.ts")
+ {
+ if (import is
+ {
+ ImportSource: "module-alias" or "dotenv" or "./Server" // good imports
+ or "node:cluster" or "node:os" or "node:fs" // do we want to keep these around?
+ }) continue;
+ LogError(f, import);
+ }
+ else if (f != "./src/bundle/Server.ts" && import is
+ {
+ ImportSource: "@spacebar/api"
+ or "@spacebar/cdn"
+ // or "@spacebar/gateway"
+ // or "@spacebar/webrtc"
+ }) LogError(f, import);
+ else if (f == "./src/schemas/Validator.ts")
{
if (import is not { ImportSource: "ajv" or "ajv-formats" or "node:fs" or "node:path" })
LogError(f, import);
}
else if (f == "./src/database/Database.ts")
{
- if (import is not { ImportSource: "picocolors" or "dotenv" or "node:fs" or "node:path" })
+ if (import is not { ImportSource: "picocolors" or "dotenv" or "typeorm" or "node:fs" or "node:path" })
LogError(f, import);
}
@@ -64,12 +82,15 @@ foreach (var f in ReadDirRecursive("./src"))
{
LogError(f, import);
}
- else
+ else if (f.StartsWith("./src/api"))
{
+ if (import.ImportSource == "express" &&
+ ((string[])["Request", "Response", "Router"]).ContainsAll(import.ImportEntities)) continue;
+ LogError(f, import);
}
}
- if (importErrors.ContainsKey(f)) Console.Write("\n");
+ if (importErrors.ContainsKey(f)) Console.Write("\n\n");
}
if (importErrors.Any())
@@ -83,7 +104,7 @@ Console.WriteLine();
IEnumerable<string> ReadDirRecursive(string path)
{
- foreach (var f in Directory.GetFiles(path).OrderBy(x=>x.TrimEnd(".ts").ToString()))
+ foreach (var f in Directory.GetFiles(path).OrderBy(x => x.TrimEnd(".ts").ToString()))
yield return f;
foreach (var d in Directory.GetDirectories(path).Order())
@@ -118,6 +139,7 @@ async IAsyncEnumerable<ImportReference> GetImports(string path)
if (p.StartsWith("lambert-server")) return ImportSourceTypeValue.VendoredModule;
if (p.StartsWith("../")) return ImportSourceTypeValue.Path;
if (p.StartsWith("./")) return ImportSourceTypeValue.LocalPath;
+ if (p.StartsWith('#')) return ImportSourceTypeValue.Hashtag;
return ImportSourceTypeValue.Npm;
}
@@ -204,4 +226,7 @@ enum ImportSourceTypeValue
[Color(255, 255, 0)] [FriendlyName(Name = "Local file path")]
LocalPath,
+
+ [FriendlyName(Name = "Hashtag import")] [Color(255, 82, 82)]
+ Hashtag
}
\ No newline at end of file
|