summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-02-11 10:37:09 +0100
committerRory& <root@rory.gay>2026-02-11 10:39:16 +0100
commitc11629268b5033b9a2ceabf05ed395cef1f68c5d (patch)
tree8de9b116aedf8cd8316610c4b5564ff06f5309fc
parentfeat: Add limit query parameter when fetching reactions (diff)
downloadserver-ts-c11629268b5033b9a2ceabf05ed395cef1f68c5d.tar.xz
More C# progress
-rw-r--r--extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Channel.cs13
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Authentication.AspNetCore/SpacebarAspNetAuthenticationService.cs5
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationConfiguration.cs1
-rw-r--r--extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs11
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/CloseCode.cs19
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs74
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/GuildSyncResponse.cs2
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs9
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/QoSPayload.cs14
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/ReadyResponse.cs3
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/SbWebsocketMeta.cs52
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs17
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Generic/Member.cs6
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Generic/PresenceResponse.cs (renamed from extra/admin-api/Models/Spacebar.Models.Gateway/PresenceResponse.cs)12
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Generic/Trace.cs67
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Controllers/IdentifyController.cs47
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs24
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs43
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Extensions/Db/UserExtensions.cs9
-rw-r--r--extra/admin-api/Spacebar.GatewayOffload/Program.cs7
20 files changed, 400 insertions, 35 deletions
diff --git a/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Channel.cs b/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Channel.cs
new file mode 100644

index 00000000..b4331203 --- /dev/null +++ b/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Channel.cs
@@ -0,0 +1,13 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Spacebar.Models.Generic; + +namespace Spacebar.DataMappings.Generic; + +public static class Channel { + extension(Models.Db.Models.Channel channel) { + [JsonIgnore] + public IEnumerable<ChannelPermissionOverwrite>? MappedPermissionOverwrites => + channel.PermissionOverwrites is null ? [] : JsonSerializer.Deserialize<List<ChannelPermissionOverwrite>>(channel.PermissionOverwrites); + } +} \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Authentication.AspNetCore/SpacebarAspNetAuthenticationService.cs b/extra/admin-api/Interop/Spacebar.Interop.Authentication.AspNetCore/SpacebarAspNetAuthenticationService.cs
index 07abb9fe..5bb42a21 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Authentication.AspNetCore/SpacebarAspNetAuthenticationService.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Authentication.AspNetCore/SpacebarAspNetAuthenticationService.cs
@@ -23,4 +23,9 @@ public class SpacebarAspNetAuthenticationService(SpacebarAuthenticationService a var token = GetTokenAsync(request); return await authService.GetCurrentUserAsync(token); } + + public Task<Session> GetCurrentSessionAsync(HttpRequest request) { + var token = GetTokenAsync(request); + return authService.GetCurrentSessionAsync(token); + } } \ No newline at end of file diff --git a/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationConfiguration.cs b/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationConfiguration.cs
index f1186ce0..8fb81b28 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationConfiguration.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationConfiguration.cs
@@ -11,6 +11,7 @@ public class SpacebarAuthenticationConfiguration { public required string PublicKeyPath { get; set; } public string? OverrideUid { get; set; } + public string? OverrideDid { get; set; } public bool DisableAuthentication { get; set; } = false; public bool Enforce2FA { get; set; } = true; public TimeSpan AuthCacheExpiry { get; set; } = TimeSpan.FromSeconds(30); diff --git a/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs b/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs
index 5f9ff500..1094a714 100644 --- a/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs +++ b/extra/admin-api/Interop/Spacebar.Interop.Authentication/SpacebarAuthenticationService.cs
@@ -10,6 +10,7 @@ namespace Spacebar.Interop.Authentication; public class SpacebarAuthenticationService(ILogger<SpacebarAuthenticationService> logger, SpacebarDbContext db, SpacebarAuthenticationConfiguration config) { private static readonly ExpiringSemaphoreCache<User> UserCache = new(); + private static readonly ExpiringSemaphoreCache<Session> SessionCache = new(); public async Task<TokenValidationResult?> ValidateTokenAsync(string token) { var handler = new JwtSecurityTokenHandler(); @@ -47,13 +48,13 @@ public class SpacebarAuthenticationService(ILogger<SpacebarAuthenticationService config.AuthCacheExpiry); } - public async Task<User> GetCurrentSessionAsync(string token) { + public async Task<Session> GetCurrentSessionAsync(string token) { var res = await ValidateTokenAsync(token); - return await UserCache.GetOrAdd(token, + return await SessionCache.GetOrAdd(token, async () => { - var uid = config.OverrideUid ?? res?.ClaimsIdentity.Claims.First(x => x.Type == "id").Value; - if (string.IsNullOrWhiteSpace(uid)) throw new InvalidOperationException("No user ID specified, is the access token valid?"); - return await db.Users.FindAsync(uid) ?? throw new InvalidOperationException(); + var did = config.OverrideDid ?? res?.ClaimsIdentity.Claims.First(x => x.Type == "did").Value; + if (string.IsNullOrWhiteSpace(did)) throw new InvalidOperationException("No device ID specified, is the access token valid?"); + return await db.Sessions.FindAsync(did) ?? throw new InvalidOperationException(); }, config.AuthCacheExpiry); } diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/CloseCode.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/CloseCode.cs new file mode 100644
index 00000000..a6d9c5f9 --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/CloseCode.cs
@@ -0,0 +1,19 @@ +namespace Spacebar.Models.Gateway; + +public enum CloseCode { + UnknownError = 4000, + UnknownOpcode = 4001, + DecodeError = 4002, + NotAuthenticated = 4003, + AuthenticationFailed = 4004, + AlreadyAuthenticated = 4005, + InvalidSession = 4006, + InvalidSeq = 4007, + RateLimited = 4008, + SessionTimedOut = 4009, + InvalidShard = 4010, + ShardingRequired = 4011, + InvalidApiVersion = 4012, + InvalidIntent = 4013, + DisallowedIntent = 4014, +} \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs new file mode 100644
index 00000000..3d81c360 --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs
@@ -0,0 +1,74 @@ +using System.Text.Json.Serialization; +using Spacebar.Models.Generic; + +public class GuildMemberListUpdate { + [JsonPropertyName("id")] + public string ListId { get; set; } = null!; + + [JsonPropertyName("guild_id")] + public string GuildId { get; set; } = null!; + + [JsonPropertyName("online_count")] + public int OnlineCount { get; set; } + + [JsonPropertyName("member_count")] + public int MemberCount { get; set; } + + [JsonPropertyName("ops")] + public List<BaseGuildMemberListUpdateOperation> Operations { get; set; } = null!; + + [JsonPropertyName("groups")] + public List<GuildMemberListGroupCount> Groups { get; set; } = null!; +} + +// i cba to write a dictionary converter for this... +public class GuildMemberListGroupCount { + /// <summary> + /// Role ID, "online" or "offline" + /// </summary> + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("count")] + public int Count { get; set; } + + public static implicit operator KeyValuePair<string, int>(GuildMemberListGroupCount groupCount) => new(groupCount.Id, groupCount.Count); + public static implicit operator GuildMemberListGroupCount(KeyValuePair<string, int> kvp) => new() { Id = kvp.Key, Count = kvp.Value }; +} + +public enum GuildMemberListUpdateOperationType { + [JsonStringEnumMemberName("sync")] Sync = 0, + [JsonStringEnumMemberName("insert")] Insert = 1, + [JsonStringEnumMemberName("update")] Update = 2, + [JsonStringEnumMemberName("delete")] Delete = 3, + + [JsonStringEnumMemberName("invalidate")] + Invalidate = 4 +} + +#region Operations + +public class BaseGuildMemberListUpdateOperation { + [JsonPropertyName("op")] + public GuildMemberListUpdateOperationType Operation { get; set; } +} + +public class GuildMemberListSyncOperation : BaseGuildMemberListUpdateOperation { + [JsonPropertyName("range")] + public int[] Range { get; set; } = null!; + + [JsonPropertyName("items")] + public List<GuildMemberListSyncItem> Items { get; set; } = null!; +} + +public class GuildMemberListSyncItem { + public class GuildMemberListMemberSyncItem : GuildMemberListSyncItem { + [JsonPropertyName("member")] + public Member Member { get; set; } = null!; + } +} + +#endregion + +// TODO: diff algo +// TODO: snapshots \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/GuildSyncResponse.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/GuildSyncResponse.cs
index 566e6c83..fcd38c10 100644 --- a/extra/admin-api/Models/Spacebar.Models.Gateway/GuildSyncResponse.cs +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/GuildSyncResponse.cs
@@ -8,7 +8,7 @@ public class GuildSyncResponse { public string GuildId { get; set; } [JsonPropertyName("presences")] - public List<PresenceResponse> Presences { get; set; } + public List<Presence> Presences { get; set; } [JsonPropertyName("members")] public List<Member> Members { get; set; } diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs
index a956655e..ee39e292 100644 --- a/extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/IdentifyRequest.cs
@@ -3,8 +3,7 @@ using System.Text.Json.Serialization; namespace Spacebar.Models.Gateway; -public class IdentifyRequest -{ +public class IdentifyRequest { [JsonPropertyName("token")] public string Token { get; set; } @@ -34,8 +33,7 @@ public class IdentifyRequest } [Flags] -public enum GatewayIntentFlags -{ +public enum GatewayIntentFlags : ulong { Guilds = 1, GuildMembers = 1 << 1, GuildModeration = 1 << 2, @@ -68,8 +66,7 @@ public enum GatewayIntentFlags } [Flags] -public enum GatewayCapabilityFlags -{ +public enum GatewayCapabilityFlags { LazyUserNotes = 1, NoAffineUserIds = 1 << 1, VersionedReadStates = 1 << 2, diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/QoSPayload.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/QoSPayload.cs new file mode 100644
index 00000000..efb8dff9 --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/QoSPayload.cs
@@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace Spacebar.Models.Gateway; + +public class QoSPayload { + [JsonPropertyName("ver")] + public int Version { get; set; } + + [JsonPropertyName("active")] + public bool Active { get; set; } + + [JsonPropertyName("reasons")] + public List<string> Reasons { get; set; } = []; +} \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/ReadyResponse.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/ReadyResponse.cs new file mode 100644
index 00000000..5573cdb5 --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/ReadyResponse.cs
@@ -0,0 +1,3 @@ +namespace Spacebar.Models.Gateway; + +public class ReadyResponse { } \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/SbWebsocketMeta.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/SbWebsocketMeta.cs new file mode 100644
index 00000000..6281426f --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/SbWebsocketMeta.cs
@@ -0,0 +1,52 @@ +using System.Text.Json.Serialization; + +namespace Spacebar.Models.Gateway; + +// TODO: move to interop +public class SbWebsocketMeta +{ + [JsonPropertyName("user_id")] + public string UserId { get; set; } = string.Empty; + + [JsonPropertyName("session_id")] + public string SessionId { get; set; } = string.Empty; + + [JsonPropertyName("accessToken")] + public string AccessToken { get; set; } = string.Empty; + + [JsonPropertyName("encoding")] + public string Encoding { get; set; } = "json"; + + [JsonPropertyName("compress")] + public string? Compress { get; set; } + + [JsonPropertyName("ipAddress")] + public string? IpAddress { get; set; } + + [JsonPropertyName("userAgent")] + public string? UserAgent { get; set; } + + [JsonPropertyName("fingerprint")] + public string? Fingerprint { get; set; } + + [JsonPropertyName("shard_count")] + public int? ShardCount { get; set; } + + [JsonPropertyName("shard_id")] + public int? ShardId { get; set; } + + [JsonPropertyName("intents")] + public GatewayIntentFlags Intents { get; set; } = default!; + + [JsonPropertyName("sequence")] + public long Sequence { get; set; } + + [JsonPropertyName("capabilities")] + public GatewayCapabilityFlags? Capabilities { get; set; } + + [JsonPropertyName("large_threshold")] + public int LargeThreshold { get; set; } + + [JsonPropertyName("qos")] + public QoSPayload? Qos { get; set; } +} \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs b/extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs new file mode 100644
index 00000000..857d2e78 --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs
@@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace Spacebar.Models.Generic; + +public class ChannelPermissionOverwrite { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("type")] + public int Type { get; set; } + + [JsonPropertyName("allow"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public ulong Allow { get; set; } + + [JsonPropertyName("deny"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public ulong Deny { get; set; } +} \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Generic/Member.cs b/extra/admin-api/Models/Spacebar.Models.Generic/Member.cs
index 23b84921..cb47dc66 100644 --- a/extra/admin-api/Models/Spacebar.Models.Generic/Member.cs +++ b/extra/admin-api/Models/Spacebar.Models.Generic/Member.cs
@@ -28,4 +28,10 @@ public class Member { [JsonPropertyName("bio")] public string? Bio { get; set; } +} + +// Unsure if this is used anywhere outside of op14...? +public class MemberWithPresence : Member { + [JsonPropertyName("presence")] + public Presence? Presence { get; set; } } \ No newline at end of file diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/PresenceResponse.cs b/extra/admin-api/Models/Spacebar.Models.Generic/PresenceResponse.cs
index 53d4cb24..97dcf21c 100644 --- a/extra/admin-api/Models/Spacebar.Models.Gateway/PresenceResponse.cs +++ b/extra/admin-api/Models/Spacebar.Models.Generic/PresenceResponse.cs
@@ -3,9 +3,9 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Spacebar.Models.Generic; -namespace Spacebar.Models.Gateway; +namespace Spacebar.Models.Generic; -public class PresenceResponse { +public class Presence { [JsonPropertyName("user")] public required PartialUser User { get; set; } @@ -29,6 +29,14 @@ public class PresenceResponse { [JsonPropertyName("has_played_game")] public bool? HasPlayedGame { get; set; } + // Unsure if this is used outside of op14 + [JsonPropertyName("game")] + public JsonObject? Game { get; set; } + + // Unsure if used outside of op14 + [JsonPropertyName("processed_at_timestamp")] + public ulong? ProcessedAtTimestamp { get; set; } + [SuppressMessage("ReSharper", "UnusedMember.Local")] public class ClientStatuses { [JsonPropertyName("desktop"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/extra/admin-api/Models/Spacebar.Models.Generic/Trace.cs b/extra/admin-api/Models/Spacebar.Models.Generic/Trace.cs new file mode 100644
index 00000000..e6722797 --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Generic/Trace.cs
@@ -0,0 +1,67 @@ +using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Spacebar.Models.Generic; + +public class Trace { + [JsonPropertyName("micros")] + public long Micros { get; set; } + + [JsonIgnore] + public string? Name { get; set; } + + [JsonIgnore] + public List<Trace>? Calls { get; set; } + + [JsonPropertyName("calls")] + // zipped array of [string, Trace] + public JsonArray? ZippedCalls { + get { + if (Calls == null) return null; + var arr = new JsonArray(); + foreach (var t in Calls) { + arr.Add(t.Name); + arr.Add(t); + } + + return arr; + } + } + + public JsonArray AsRoot() { + return new() { + Name, + this + }; + } +} + +public static class TraceResult { + public static async Task<TraceResult<T>> TraceAsync<T>(string name, Func<Task<T>> func) { + var sw = Stopwatch.StartNew(); + var result = await func(); + sw.Stop(); + return new TraceResult<T> { + Name = name, + Micros = sw.Elapsed.Microseconds, + Result = result + }; + } + + public static async Task<TraceResult<T>> Trace<T>(string name, Func<T> func) { + var sw = Stopwatch.StartNew(); + var result = func(); + sw.Stop(); + return new TraceResult<T> { + Name = name, + Micros = sw.Elapsed.Microseconds, + Result = result + }; + } +} + +public class TraceResult<T> : Trace { + [JsonIgnore] + public T Result { get; set; } +} \ 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 6ce9f9fb..d49790a9 100644 --- a/extra/admin-api/Spacebar.GatewayOffload/Controllers/IdentifyController.cs +++ b/extra/admin-api/Spacebar.GatewayOffload/Controllers/IdentifyController.cs
@@ -1,13 +1,52 @@ using Microsoft.AspNetCore.Mvc; using Spacebar.Interop.Authentication; -using Spacebar.Interop.Authentication.AspNetCore; +using Spacebar.Interop.Replication.Abstractions; using Spacebar.Models.Db.Contexts; +using Spacebar.Models.Gateway; +using Spacebar.Models.Generic; namespace Spacebar.GatewayOffload.Controllers; [ApiController] [Route("/_spacebar/offload/gateway/Identify")] -public class IdentifyController(ILogger<IdentifyController> logger, SpacebarAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase -{ - +public class IdentifyController(ILogger<IdentifyController> logger, SpacebarAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase { + [HttpPost("")] + public async IAsyncEnumerable<ReplicationMessage> DoIdentify(IdentifyRequest payload) { + var user = await TraceResult.TraceAsync("getAuthUser", () => authService.GetCurrentUserAsync(payload.Token)); + var session = await TraceResult.TraceAsync("getAuthSession", () => authService.GetCurrentSessionAsync(payload.Token)); + + var socketMeta = new SbWebsocketMeta() { + // Auth data + AccessToken = payload.Token, + UserId = user.Result.Id, + SessionId = session.Result.SessionId, + // Client capabilities + Capabilities = payload.Capabilities ??= 0, + LargeThreshold = payload.LargeTreshold ??= user.Result.Bot ? 20 : 250, + Intents = payload.Intents ??= (GatewayIntentFlags)0b_110_11111111_11111111_11111111_11111111, + // Sharding info + ShardId = payload.Shard?[0], + ShardCount = payload.Shard?[1], + }; + + if (socketMeta is { ShardId: not null, ShardCount: not null }) { + if (socketMeta.ShardId < 0 || socketMeta.ShardCount <= 0 || socketMeta.ShardId >= socketMeta.ShardCount) { + logger.LogWarning("Invalid sharding from {userId}: {shardId}/{shardCount}", user.Result.Id, socketMeta.ShardId, socketMeta.ShardCount); + yield return this.Close(CloseCode.InvalidShard); + yield break; + } + } + + yield return new() { + Payload = new ReadyResponse { }, + }; + } + + private ReplicationMessage Close(CloseCode closeCode) => new() { + Origin = "IdentifyController", + Event = "SB_GW_CLOSE", + Payload = new { + code = closeCode, + } + }; } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs
index a2008345..db45a895 100644 --- a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs +++ b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op12Controller.cs
@@ -1,3 +1,4 @@ +using System.Collections.Frozen; using System.Linq.Expressions; using System.Text.Json; using System.Text.Json.Nodes; @@ -10,6 +11,7 @@ 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; @@ -21,7 +23,7 @@ public class Op12Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen public async IAsyncEnumerable<ReplicationMessage> DoGuildSync(List<string> guildIds) { var user = await authService.GetCurrentUserAsync(Request); - guildIds = (await db.Members.Where(x => x.Id == user.Id).Select(x => x.GuildId).ToListAsync()) + 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(); @@ -41,44 +43,44 @@ public class Op12Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen } // TODO: figure out how to abstract this to a function without EFCore complaining about not being translatable... - //private static Func<Session, bool> IsOnline = (Session session) => session.Status != "offline" && session.Status != "invisible" && session.Status != "unknown"; + 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.Where(x => x.GuildId == guildId).CountAsync(); + 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.Where(x => x.GuildId == guildId) + 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 + // 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).ToDictionary(x => x.Id, x => x.ToPartialUser()); - var mappedMembers = members.ToDictionary(m => m.Id, m => m.ToPublicMember(mappedPartialUsers[m.Id])); + 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).ToDictionary(x => x.Id, x => + 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 PresenceResponse() + 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<PresenceResponse.ClientStatuses>(sortedSessions.First(s => !string.IsNullOrWhiteSpace(s.ClientStatus)).ClientStatus) ?? + ClientStatus = JsonSerializer.Deserialize<Presence.ClientStatuses>(sortedSessions.First(s => !string.IsNullOrWhiteSpace(s.ClientStatus)).ClientStatus) ?? new() }; - }).Where(x => x.Value.Activities.Count > 0).ToDictionary(); + }).Where(x => x.Value.Activities.Count > 0).ToFrozenDictionary(); var r = new GuildSyncResponse() { diff --git a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs
index 84f03b89..9f32eef2 100644 --- a/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs +++ b/extra/admin-api/Spacebar.GatewayOffload/Controllers/Op14Controller.cs
@@ -1,13 +1,50 @@ +using System.Text.Json; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Spacebar.GatewayOffload.Extensions.Db; +using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Interop.Replication.Abstractions; +using Spacebar.Models.Db.Contexts; +using Spacebar.Models.Gateway; +using Spacebar.Models.Generic; namespace Spacebar.GatewayOffload.Controllers; [ApiController] [Route("/_spacebar/offload/gateway/LazyRequest")] -public class Op14Controller : ControllerBase { +public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase { [HttpPost] - public async IAsyncEnumerable<ReplicationMessage> DoLazyRequest() { - yield break; + public async IAsyncEnumerable<ReplicationMessage> DoLazyRequest([FromBody] LazyRequest payload) { + var user = await TraceResult.TraceAsync("getAuthUser", () => authService.GetCurrentUserAsync(Request)); + var session = await TraceResult.TraceAsync("getAuthSession", () => authService.GetCurrentSessionAsync(Request)); + + if (!await db.Members.AsNoTracking().AnyAsync(m => m.GuildId == payload.GuildId && m.Id == user.Result.Id)) { + logger.LogWarning("User {user} requested lazy member list for guild {guildId}, but is not a member", user.Result.Id, payload.GuildId); + yield break; + } + + if (payload.Channels.Count == 0) { + logger.LogWarning("User {user} requested lazy member list for guild {guildId}, but is not a member", user.Result.Tag, payload.GuildId); + yield break; + } + + // Fetch hoisted roles for the guild to define groups + var hoistedRoles = await db.Roles + .AsNoTracking() + .Where(r => r.GuildId == payload.GuildId && r.Hoist) + .OrderByDescending(r => r.Position) + .Select(r => new { r.Id }) + .ToListAsync(); + } + + private async Task<string?> GetMemberListIdAsync(SpacebarDbContext db, string guildId, string channelId) { + var channel = await db.Channels.AsNoTracking().FirstOrDefaultAsync(c => c.Id == channelId && c.GuildId == guildId); + if (channel == null) return null; + + if (string.IsNullOrWhiteSpace(channel.PermissionOverwrites) || channel.PermissionOverwrites == "[]") { + return "everyone"; + } + + return null; // TODO } } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.GatewayOffload/Extensions/Db/UserExtensions.cs b/extra/admin-api/Spacebar.GatewayOffload/Extensions/Db/UserExtensions.cs new file mode 100644
index 00000000..27b4fb16 --- /dev/null +++ b/extra/admin-api/Spacebar.GatewayOffload/Extensions/Db/UserExtensions.cs
@@ -0,0 +1,9 @@ +using Spacebar.Models.Db.Models; + +namespace Spacebar.GatewayOffload.Extensions.Db; + +public static class UserExtensions { + extension(User user) { + public string Tag => $"{user.Username}#{user.Discriminator}"; + } +} \ No newline at end of file diff --git a/extra/admin-api/Spacebar.GatewayOffload/Program.cs b/extra/admin-api/Spacebar.GatewayOffload/Program.cs
index 5cfe28d4..947ca35f 100644 --- a/extra/admin-api/Spacebar.GatewayOffload/Program.cs +++ b/extra/admin-api/Spacebar.GatewayOffload/Program.cs
@@ -1,8 +1,11 @@ +using System.Text.Json; using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; using Microsoft.EntityFrameworkCore; using Spacebar.Interop.Authentication; using Spacebar.Interop.Authentication.AspNetCore; using Spacebar.Models.Db.Contexts; +using Spacebar.Models.Generic; var builder = WebApplication.CreateBuilder(args); if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("APPSETTINGS_PATH"))) @@ -18,9 +21,7 @@ builder.Services.AddControllers(options => { options.JsonSerializerOptions.WriteIndented = true; options.JsonSerializerOptions.MaxDepth = 100; // options.JsonSerializerOptions.DefaultBufferSize = ; -}).AddMvcOptions(o=> { - o.SuppressOutputFormatterBuffering = true; -}); +}).AddMvcOptions(o => { o.SuppressOutputFormatterBuffering = true; }); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi builder.Services.AddOpenApi();