summary refs log tree commit diff
path: root/extra/admin-api/Spacebar.Offload/Controllers/Op8Controller.cs
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-03-19 08:38:20 +0100
committerRory& <root@rory.gay>2026-03-19 08:38:20 +0100
commitd0eac0b8e7738c645707b52e386ca1e2079b8da8 (patch)
treec8fc5b135ec94f6351e025ff7df472f83db4c6c5 /extra/admin-api/Spacebar.Offload/Controllers/Op8Controller.cs
parentCDN: use file contents for hashes, the fuck (diff)
downloadserver-ts-d0eac0b8e7738c645707b52e386ca1e2079b8da8.tar.xz
GatewayOffload: rename to Offload
Diffstat (limited to 'extra/admin-api/Spacebar.Offload/Controllers/Op8Controller.cs')
-rw-r--r--extra/admin-api/Spacebar.Offload/Controllers/Op8Controller.cs93
1 files changed, 93 insertions, 0 deletions
diff --git a/extra/admin-api/Spacebar.Offload/Controllers/Op8Controller.cs b/extra/admin-api/Spacebar.Offload/Controllers/Op8Controller.cs
new file mode 100644

index 00000000..ac68bb22 --- /dev/null +++ b/extra/admin-api/Spacebar.Offload/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/GuildMembers")] +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