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();
|