summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-03-13 06:10:31 +0100
committerRory& <root@rory.gay>2026-03-13 06:10:31 +0100
commitad4b34220027b91f259d4a214dba44a6f8633014 (patch)
tree5bb3f948a7573bb6486a2bf30c34eeb42f738d7e
parentgateway: util function to do gateway offload stuff (diff)
downloadserver-ts-ad4b34220027b91f259d4a214dba44a6f8633014.tar.xz
CS: make replication messages typed, some offload work
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ISpacebarReplication.cs3
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ReplicationMessage.cs12
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/RabbitMqSpacebarReplication.cs10
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Replication.UnixSocket/UnixSocketSpacebarReplication.cs4
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/BulkMessageDeleteResponse.cs14
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/ChannelStatusesRequest.cs64
-rw-r--r--extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs17
-rw-r--r--extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs11
-rw-r--r--extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/IpcTestController.cs3
-rw-r--r--extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs11
-rw-r--r--extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj2
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Controllers/ChannelStatusController.cs68
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Controllers/IdentifyController.cs9
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs2
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs3
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Controllers/Op8Controller.cs93
16 files changed, 291 insertions, 35 deletions
diff --git a/extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ISpacebarReplication.cs b/extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ISpacebarReplication.cs

index 9398eedf..c568c98d 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ISpacebarReplication.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ISpacebarReplication.cs
@@ -2,5 +2,6 @@ public interface ISpacebarReplication { public Task InitializeAsync(); - public Task SendAsync(ReplicationMessage message); + public Task SendAsync(ContentlessReplicationMessage message); + public Task SendAsync<TPayload>(ReplicationMessage<TPayload> message); } diff --git a/extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ReplicationMessage.cs b/extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ReplicationMessage.cs
index bc7dc41f..630ef47e 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ReplicationMessage.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Replication.Abstractions/ReplicationMessage.cs
@@ -2,7 +2,7 @@ using System.Text.Json.Serialization; namespace Spacebar.Interop.Replication.Abstractions; -public class ReplicationMessage { +public class ContentlessReplicationMessage { [JsonPropertyName("channel_id")] public string? ChannelId { get; set; } @@ -11,7 +11,7 @@ public class ReplicationMessage { [JsonPropertyName("user_id")] public string? UserId { get; set; } - + [JsonPropertyName("session_id")] public string? SessionId { get; set; } @@ -24,9 +24,11 @@ public class ReplicationMessage { [JsonPropertyName("origin")] public string? Origin { get; set; } - [JsonPropertyName("data")] - public object Payload { get; set; } = null!; - [JsonPropertyName("reconnect_delay")] public int? ReconnectDelay { get; set; } +} + +public class ReplicationMessage<TPayload> : ContentlessReplicationMessage { + [JsonPropertyName("data")] + public TPayload Payload { get; set; } = default!; } \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/RabbitMqSpacebarReplication.cs b/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/RabbitMqSpacebarReplication.cs
index f979d923..b818b13e 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/RabbitMqSpacebarReplication.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Replication.RabbitMq/RabbitMqSpacebarReplication.cs
@@ -22,16 +22,17 @@ public class RabbitMqSpacebarReplication : ISpacebarReplication { _mqChannel = await _mqConnection.CreateChannelAsync(); } - public async Task SendAsync(ReplicationMessage message) { + // HACK: body is required in rabbitmq... + private async Task SendAsyncInternal(ContentlessReplicationMessage message, object? body = null) { var exchangeId = message.GuildId ?? message.ChannelId ?? message.UserId ?? "global"; await _mqChannel.ExchangeDeclareAsync(exchange: exchangeId, type: ExchangeType.Fanout, durable: false); var props = new BasicProperties() { Type = message.Event }; var publishSuccess = false; - var body = message.Payload.ToJson().AsBytes().ToArray(); // TODO: byte array payloads etc someday? + var encodedBody = body.ToJson().AsBytes().ToArray(); // TODO: byte array payloads etc someday? do { try { - await _mqChannel.BasicPublishAsync(exchange: exchangeId, routingKey: "", mandatory: true, basicProperties: props, body: body); + await _mqChannel.BasicPublishAsync(exchange: exchangeId, routingKey: "", mandatory: true, basicProperties: props, body: encodedBody); publishSuccess = true; } catch (Exception e) { @@ -40,4 +41,7 @@ public class RabbitMqSpacebarReplication : ISpacebarReplication { } } while (!publishSuccess); } + + public async Task SendAsync(ContentlessReplicationMessage message) => await SendAsyncInternal(message); + public async Task SendAsync<TPayload>(ReplicationMessage<TPayload> message) => await SendAsyncInternal(message, message.Payload); } \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Replication.UnixSocket/UnixSocketSpacebarReplication.cs b/extra/admin-api/Interop/Spacebar.Interop.Replication.UnixSocket/UnixSocketSpacebarReplication.cs
index 2959c71b..1c23c1c4 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Replication.UnixSocket/UnixSocketSpacebarReplication.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Replication.UnixSocket/UnixSocketSpacebarReplication.cs
@@ -20,7 +20,7 @@ public class UnixSocketSpacebarReplication(UnixSocketConfiguration conf) : ISpac }; } - public async Task SendAsync(ReplicationMessage message) { + public async Task SendAsync(ContentlessReplicationMessage message) { // message format: [uint32be length][payload] var payload = JsonSerializer.SerializeToUtf8Bytes(message); byte[] formattedPayload = [..BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder(payload.Length)), ..payload]; @@ -30,6 +30,8 @@ public class UnixSocketSpacebarReplication(UnixSocketConfiguration conf) : ISpac skv.Value.SendAsync(formattedPayload); }); } + + async Task ISpacebarReplication.SendAsync<TPayload>(ReplicationMessage<TPayload> message) => await SendAsync(message); } public class UnixSocketConfiguration { diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/BulkMessageDeleteResponse.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/BulkMessageDeleteResponse.cs new file mode 100644
index 00000000..f4cb669a --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/BulkMessageDeleteResponse.cs
@@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace Spacebar.Models.Gateway; + +public class BulkMessageDeleteResponse { + [JsonPropertyName("guild_id")] + public string? GuildId { get; set; } + + [JsonPropertyName("channel_id")] + public required string ChannelId { get; set; } + + [JsonPropertyName("ids")] + public required List<string> MessageIds { get; set; } +} \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/ChannelStatusesRequest.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/ChannelStatusesRequest.cs new file mode 100644
index 00000000..94737ffe --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/ChannelStatusesRequest.cs
@@ -0,0 +1,64 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Spacebar.Models.Gateway; + +public class ChannelStatusesRequest { + [JsonPropertyName("guild_id")] + [JsonRequired] + public JsonValue GuildIdRawValue { get; set; } = null!; + + [JsonIgnore] + public string? GuildId { + get => GuildIdRawValue.GetValueKind() == JsonValueKind.String ? GuildIdRawValue.GetValue<string>() : null; + [MemberNotNull] set => GuildIdRawValue = JsonValue.Create(value!); + } + + [JsonIgnore] + public List<string>? GuildIds { + get => GuildIdRawValue.GetValueKind() == JsonValueKind.Array ? GuildIdRawValue.AsArray().Deserialize<List<string>>() : null; + [MemberNotNull] set => GuildIdRawValue = JsonValue.Create(value!)!; + } +} + +public class ChannelInfoRequest : ChannelStatusesRequest { + [JsonPropertyName("fields")] + public required List<string> Fields { get; set; } +} + +public class ChannelStatus { + [JsonPropertyName("id")] + public string ChannelId { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } +} + +public class ChannelStatusesResponse { + [JsonPropertyName("guild_id")] + public string GuildId { get; set; } + + [JsonPropertyName("channels")] + public List<ChannelStatus> Channels { get; set; } +} + +public class ChannelInfo { + [JsonPropertyName("id")] + public required string ChannelId { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("voice_start_time")] + public DateTimeOffset? VoiceStartTime { get; set; } +} + +public class ChannelInfoResponse { + [JsonPropertyName("guild_id")] + public string GuildId { get; set; } + + [JsonPropertyName("channels")] + public List<ChannelInfo> Channels { get; set; } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs
index 45a9e437..cccc4760 100644 --- a/extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/ChannelController.cs
@@ -6,6 +6,7 @@ using Spacebar.Models.AdminApi; using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Models.Db.Contexts; using Spacebar.Models.Db.Models; +using Spacebar.Models.Gateway; namespace Spacebar.AdminApi.Controllers; @@ -18,15 +19,15 @@ public class ChannelController( SpacebarAspNetAuthenticationService auth, ISpacebarReplication replication ) : ControllerBase { - [HttpDelete("{id}")] public async Task DeleteById(string id) { (await auth.GetCurrentUserAsync(Request)).GetRights().AssertHasAllRights(SpacebarRights.Rights.OPERATOR); - replication.SendAsync(new() { + // TODO: proper type + await replication.SendAsync<Channel>(new() { Origin = "AdminApi/DeleteChannelById", ChannelId = id, Event = "CHANNEL_DELETE", - Payload = await db.Channels.SingleAsync (x=>x.Id == id) + Payload = await db.Channels.SingleAsync(x => x.Id == id) }); await db.Channels.Where(x => x.Id == id).ExecuteDeleteAsync(); @@ -58,13 +59,13 @@ public class ChannelController( break; } - await replication.SendAsync(new() { + await replication.SendAsync<BulkMessageDeleteResponse>(new() { ChannelId = channelId, Event = "MESSAGE_BULK_DELETE", - Payload = new { - ids = messageIds, - channel_id = channelId, - guild_id = guildId, + Payload = new BulkMessageDeleteResponse() { + GuildId = guildId, + ChannelId = channelId, + MessageIds = messageIds, }, Origin = "Admin API (GuildController.DeleteUser)", }); diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs
index 62df5a38..38d8b3c2 100644 --- a/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/GuildController.cs
@@ -6,6 +6,7 @@ using Spacebar.Models.AdminApi; using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Models.Db.Contexts; using Spacebar.Models.Db.Models; +using Spacebar.Models.Gateway; namespace Spacebar.AdminApi.Controllers; @@ -218,13 +219,13 @@ public class GuildController( break; } - await replication.SendAsync(new() { + await replication.SendAsync<BulkMessageDeleteResponse>(new() { ChannelId = channelId, Event = "MESSAGE_BULK_DELETE", - Payload = new { - ids = messageIds, - channel_id = channelId, - guild_id = guildId, + Payload = new() { + GuildId = guildId, + ChannelId = channelId, + MessageIds = messageIds, }, Origin = "Admin API (GuildController.DeleteUser)", }); diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/IpcTestController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/IpcTestController.cs
index 228606f7..6881e1e3 100644 --- a/extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/IpcTestController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/TestControllers/IpcTestController.cs
@@ -40,7 +40,8 @@ public class IpcTestController( while (true) { var clr = re.Next(); color = clr.r << 16 | clr.g << 8 | clr.b; - await replication.SendAsync(new() { + // TODO: create type + await replication.SendAsync<object>(new() { Event = "GUILD_ROLE_UPDATE", GuildId = guildId, Origin = "Admin API (GET /users/test)", diff --git a/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs b/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs
index 8eacb0c3..f1f861d5 100644 --- a/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs +++ b/extra/admin-api/Spacebar.AdminApi/Controllers/UserController.cs
@@ -6,6 +6,7 @@ using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Interop.Replication.Abstractions; using Spacebar.Models.AdminApi; using Spacebar.Models.Db.Contexts; +using Spacebar.Models.Gateway; namespace Spacebar.AdminApi.Controllers; @@ -218,13 +219,13 @@ public class UserController( break; } - await replication.SendAsync(new() { + await replication.SendAsync<BulkMessageDeleteResponse>(new() { Event = "MESSAGE_BULK_DELETE", ChannelId = channelId, - Payload = new { - channel_id = channelId, - guild_id = guildId, - ids = messageIds, + Payload = new() { + GuildId = guildId, + ChannelId = channelId, + MessageIds = messageIds, }, Origin = "AdminApi/DeleteMessagesForChannel" }); diff --git a/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj
index c1b5f633..9adf4d0d 100644 --- a/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj +++ b/extra/admin-api/Spacebar.AdminApi/Spacebar.AdminApi.csproj
@@ -24,6 +24,8 @@ <PackageReference Include="Spacebar.Models.Config" Version="*-preview*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/> <ProjectReference Include="..\Models\Spacebar.Models.Db\Spacebar.Models.Db.csproj" Condition="'$(ContinuousIntegrationBuild)'!='true'"/> <PackageReference Include="Spacebar.Models.Db" Version="*-preview*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/> + <ProjectReference Include="..\Models\Spacebar.Models.Gateway\Spacebar.Models.Gateway.csproj" Condition="'$(ContinuousIntegrationBuild)'!='true'"/> + <PackageReference Include="Spacebar.Models.Gateway" Version="*-preview*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/> </ItemGroup> <ItemGroup> diff --git a/extra/admin-api/Spacebar.GatewayOffload/Controllers/ChannelStatusController.cs b/extra/admin-api/Spacebar.GatewayOffload/Controllers/ChannelStatusController.cs new file mode 100644
index 00000000..767e838b --- /dev/null +++ b/extra/admin-api/Spacebar.GatewayOffload/Controllers/ChannelStatusController.cs
@@ -0,0 +1,68 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.Interop.Authentication.AspNetCore; +using Spacebar.Interop.Replication.Abstractions; +using Spacebar.Models.Db.Contexts; +using Spacebar.Models.Gateway; + +namespace Spacebar.GatewayOffload.Controllers; + +[ApiController] +[Route("/_spacebar/offload/gateway")] +public class ChannelStatusController(ILogger<ChannelStatusController> logger, SpacebarAspNetAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) + : ControllerBase { + [HttpPost("channelStatuses")] + public async IAsyncEnumerable<ReplicationMessage<ChannelStatusesResponse>> GetChannelStatuses([FromBody] ChannelStatusesRequest req) { + await foreach (var res in GetChannelInfos(new() { + Fields = ["status"], + GuildIdRawValue = req.GuildIdRawValue, + })) { + yield return new() { + Payload = new() { + GuildId = res.Payload.GuildId, + Channels = res.Payload.Channels.Select(c => new ChannelStatus { + ChannelId = c.ChannelId, + Status = c.Status!, + }).ToList(), + } + }; + } + } + + [HttpPost("channelInfo")] + public async IAsyncEnumerable<ReplicationMessage<ChannelInfoResponse>> GetChannelInfos([FromBody] ChannelInfoRequest req) { + var user = await authService.GetCurrentUserAsync(Request); + string[] statusOptions = [ + "Vibing ✨", + "Hanging out: 12%...", + "Communicating...", + // idk, i cant come up with more stuff, maybe suggestions welcome, or actually storing some data? + ]; + + foreach (var guildId in req.GuildIds ?? [req.GuildId!]) { + var channels = (await db.Channels.Include(x => x.VoiceStates).Where(x => x.Type == 2 && x.GuildId == guildId && x.VoiceStates.Count > 0) + .Select(x => x.Id) + .ToListAsync()) + .Select(x => new { + id = x, + status = statusOptions[new Random().Next(statusOptions.Length)], // TODO: We don't currently store channel statuses, so make some stuff up + voiceStartTime = DateTime.Now.Subtract(TimeSpan.FromMinutes(new Random().Next(1, 120))), // TODO: We also don't store voice start times, so make some stuff up + }).ToList(); + + yield return new() { + Payload = new() { + GuildId = guildId, + Channels = channels.Select(c => new ChannelInfo { + ChannelId = c.id, + Status = req.Fields.Contains("status") ? c.status : null, + VoiceStartTime = req.Fields.Contains("voice_start_time") ? c.voiceStartTime : null, + }).ToList(), + }, + }; + } + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.GatewayOffload/Controllers/IdentifyController.cs b/extra/admin-api/Spacebar.GatewayOffload/Controllers/IdentifyController.cs
index d49790a9..02bad394 100644 --- a/extra/admin-api/Spacebar.GatewayOffload/Controllers/IdentifyController.cs +++ b/extra/admin-api/Spacebar.GatewayOffload/Controllers/IdentifyController.cs
@@ -11,7 +11,7 @@ namespace Spacebar.GatewayOffload.Controllers; [Route("/_spacebar/offload/gateway/Identify")] public class IdentifyController(ILogger<IdentifyController> logger, SpacebarAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase { [HttpPost("")] - public async IAsyncEnumerable<ReplicationMessage> DoIdentify(IdentifyRequest payload) { + public async IAsyncEnumerable<ContentlessReplicationMessage> DoIdentify(IdentifyRequest payload) { var user = await TraceResult.TraceAsync("getAuthUser", () => authService.GetCurrentUserAsync(payload.Token)); var session = await TraceResult.TraceAsync("getAuthSession", () => authService.GetCurrentSessionAsync(payload.Token)); @@ -37,12 +37,13 @@ public class IdentifyController(ILogger<IdentifyController> logger, SpacebarAuth } } - yield return new() { - Payload = new ReadyResponse { }, + yield return new ReplicationMessage<ReadyResponse>() { + Payload = new() { }, }; } - private ReplicationMessage Close(CloseCode closeCode) => new() { + // TODO: type? also, implement this in gateway lol + private ReplicationMessage<object?> Close(CloseCode closeCode) => new() { Origin = "IdentifyController", Event = "SB_GW_CLOSE", Payload = new { diff --git a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs
index db45a895..884a9de8 100644 --- a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs +++ b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs
@@ -20,7 +20,7 @@ namespace Spacebar.GatewayOffload.Controllers; public class Op12Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase { [HttpPost("")] - public async IAsyncEnumerable<ReplicationMessage> DoGuildSync(List<string> guildIds) + public async IAsyncEnumerable<ReplicationMessage<GuildSyncResponse>> DoGuildSync(List<string> guildIds) { var user = await authService.GetCurrentUserAsync(Request); guildIds = (await db.Members.AsNoTracking().Where(x => x.Id == user.Id).Select(x => x.GuildId).ToListAsync()) diff --git a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs
index 9f32eef2..57f1390c 100644 --- a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs +++ b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs
@@ -14,7 +14,8 @@ namespace Spacebar.GatewayOffload.Controllers; [Route("/_spacebar/offload/gateway/LazyRequest")] public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase { [HttpPost] - public async IAsyncEnumerable<ReplicationMessage> DoLazyRequest([FromBody] LazyRequest payload) { + // TODO: actually return something? + public async IAsyncEnumerable<ContentlessReplicationMessage> DoLazyRequest([FromBody] LazyRequest payload) { var user = await TraceResult.TraceAsync("getAuthUser", () => authService.GetCurrentUserAsync(Request)); var session = await TraceResult.TraceAsync("getAuthSession", () => authService.GetCurrentSessionAsync(Request)); diff --git a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op8Controller.cs b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op8Controller.cs new file mode 100644
index 00000000..dbcd4452 --- /dev/null +++ b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op8Controller.cs
@@ -0,0 +1,93 @@ +using System.Collections.Frozen; +using System.Linq.Expressions; +using System.Text.Json; +using System.Text.Json.Nodes; +using ArcaneLibs.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.DataMappings.Generic; +using Spacebar.Interop.Authentication.AspNetCore; +using Spacebar.Interop.Replication.Abstractions; +using Spacebar.Models.Db.Contexts; +using Spacebar.Models.Db.Models; +using Spacebar.Models.Gateway; +using Spacebar.Models.Generic; + +namespace Spacebar.GatewayOffload.Controllers; + +[ApiController] +[Route("/_spacebar/offload/gateway/GuildSync")] +public class Op8Controller(ILogger<Op8Controller> logger, SpacebarAspNetAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase +{ + [HttpPost("")] + public async IAsyncEnumerable<ReplicationMessage<GuildSyncResponse>> DoGuildSync(List<string> guildIds) + { + var user = await authService.GetCurrentUserAsync(Request); + guildIds = (await db.Members.AsNoTracking().Where(x => x.Id == user.Id).Select(x => x.GuildId).ToListAsync()) + .Intersect(guildIds) + .OrderByDescending(gi => db.Members.Count(m => m.GuildId == gi)) + .ToList(); + + var syncs = guildIds.Select(GetGuildSyncAsync).ToList().ToAsyncResultEnumerable(); + await foreach (var res in syncs) + { + yield return new() + { + Origin = "OFFLOAD_GUILD_SYNC", + UserId = user.Id, + Event = "GUILD_SYNC", + CreatedAt = DateTime.Now, + Payload = res + }; + } + } + + // TODO: figure out how to abstract this to a function without EFCore complaining about not being translatable... + private static Expression<Func<Session, bool>> IsOnline = (Session session) => session.Status != "offline" && session.Status != "invisible" && session.Status != "unknown"; + + private async Task<GuildSyncResponse> GetGuildSyncAsync(string guildId) + { + await using var sc = sp.CreateAsyncScope(); + var _db = sc.ServiceProvider.GetRequiredService<SpacebarDbContext>(); + var memberCount = await _db.Members.AsNoTracking().Where(x => x.GuildId == guildId).CountAsync(); + + var offlineTreshold = DateTime.Now.Subtract(TimeSpan.FromDays(14)); + var isLargeGuild = memberCount > 10000; + + var members = await _db.Members.AsNoTracking().Where(x => x.GuildId == guildId) + .Include(x => x.IdNavigation) + .ThenInclude(x => x.Sessions.Where(s => + !s.IsAdminSession && ( + // see TODO on IsOnline - somehow need to replicate `IsOnline(s)` + s.Status != "offline" && s.Status != "invisible" && s.Status != "unknown" + ) && (!isLargeGuild || s.LastSeen >= offlineTreshold))) + .Where(x => x.IdNavigation.Sessions.Count > 0) // ignore members without sessions + .ToListAsync(); + + var mappedPartialUsers = members.Select(x => x.IdNavigation).ToFrozenDictionary(x => x.Id, x => x.ToPartialUser()); + var mappedMembers = members.ToFrozenDictionary(m => m.Id, m => m.ToPublicMember(mappedPartialUsers[m.Id])); + + var presences = members.Select(x => x.IdNavigation).Where(x => x.Sessions.Count > 0).ToFrozenDictionary(x => x.Id, x => + { + var sortedSessions = x.Sessions.OrderByDescending(s => s.LastSeen).ToList(); + return new Presence() + { + GuildId = guildId, + User = mappedPartialUsers[x.Id], + Activities = x.Sessions.Where(s => s.Status is not ("offline" or "invisible" or "unknown")) + .SelectMany(s => JsonSerializer.Deserialize<JsonObject[]>(s.Activities) ?? []).ToList(), + Status = sortedSessions.FirstOrDefault(s => !string.IsNullOrWhiteSpace(s.Status))?.Status ?? "offline", + ClientStatus = JsonSerializer.Deserialize<Presence.ClientStatuses>(sortedSessions.First(s => !string.IsNullOrWhiteSpace(s.ClientStatus)).ClientStatus) ?? + new() + }; + }).Where(x => x.Value.Activities.Count > 0).ToFrozenDictionary(); + + var r = new GuildSyncResponse() + { + GuildId = guildId, + Members = mappedMembers.Values.ToList(), + Presences = presences.Values.ToList() + }; + return r; + } +} \ No newline at end of file