diff --git a/extra/admin-api/Spacebar.Offload/Controllers/Op14Controller.cs b/extra/admin-api/Spacebar.Offload/Controllers/Op14Controller.cs
index f34455542..79369de1e 100644
--- a/extra/admin-api/Spacebar.Offload/Controllers/Op14Controller.cs
+++ b/extra/admin-api/Spacebar.Offload/Controllers/Op14Controller.cs
@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using ArcaneLibs.Extensions;
@@ -12,44 +13,56 @@ using Spacebar.Interop.Replication.Abstractions;
using Spacebar.Models.Db.Contexts;
using Spacebar.Models.Gateway;
using Spacebar.Models.Generic;
+using Spacebar.Models.Generic.Constants;
+using Spacebar.Offload.Extensions;
namespace Spacebar.GatewayOffload.Controllers;
[ApiController]
[Route("/_spacebar/offload/gateway/LazyRequest")]
-public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase {
+public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthenticationService authService, SpacebarDbContext db, IServiceProvider sp) : ControllerBase
+{
[HttpPost]
// TODO: actually return something?
- public async IAsyncEnumerable<ContentlessReplicationMessage> DoLazyRequest([FromBody] LazyRequest payload) {
+ 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));
- if (!await db.Members.AsNoTracking().AnyAsync(m => m.GuildId == payload.GuildId && m.Id == user.Result.Id)) {
+ 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);
+ if (payload.Channels.Count == 0)
+ {
+ logger.LogWarning("User {user} requested lazy member list for guild {guildId}, but did not request a channel range", user.Result.Tag, payload.GuildId);
yield break;
}
var memberList = await GetGuildMemberListAsync(db, payload.GuildId);
- yield return new ReplicationMessage<GuildMemberListUpdate>() {
+ yield return new ReplicationMessage<GuildMemberListUpdate>()
+ {
UserId = user.Result.Id,
Event = GuildMemberListUpdate.EventId,
Origin = "Offload/LazyRequest",
CreatedAt = DateTime.UtcNow,
- Payload = new GuildMemberListUpdate() {
+ Payload = new GuildMemberListUpdate()
+ {
GuildId = payload.GuildId,
- ListId = payload.GuildId.ToString(),
- OnlineCount = memberList.TakeWhile(x => x is not RoleEntry { Id: "offline" }).Count(),
+ // this doesnt appear to work currently, skip it
+ ListId = "everyone", // await GetMemberListIdAsync(db, guildId: payload.GuildId, channelId: long.Parse(payload.Channels.Keys.First())),
+ OnlineCount = memberList.TakeWhile(x => x is not RoleEntry { Group.Id: "offline" }).Count(),
MemberCount = await db.Members.CountAsync(x => x.GuildId == payload.GuildId),
- Operations = [
- new GuildMemberListUpdateOperation.SyncOperation() {
+ Operations =
+ [
+ new GuildMemberListUpdateOperation.SyncOperation()
+ {
+ Operation = GuildMemberListUpdateOperationType.Sync,
Items = memberList.Select<IMemberListEntry, GuildMemberListSyncItem>(item => item is RoleEntry re
- ? new GuildMemberListSyncItem.RoleEntry() { Id = re.Id, Count = re.Count }
+ ? new GuildMemberListSyncItem.RoleEntry() { Group = new() { Id = re.Group.Id, Count = re.Group.Count } }
: item is MemberEntry me
? new GuildMemberListSyncItem.MemberEntry() { Member = me.Member }
: throw new InvalidCastException("List item was neither RoleEntry nor MemberEntry???"))
@@ -57,7 +70,8 @@ public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen
Range = [0, memberList.Count]
}
],
- Groups = memberList.OfType<RoleEntry>().Select(re => new GuildMemberListSyncItem.RoleEntry() { Id = re.Id, Count = re.Count }).ToList()
+ Groups = memberList.OfType<RoleEntry>().Select(re => new GuildMemberListSyncItem.RoleEntry.Content() { Id = re.Group.Id, Count = re.Group.Count })
+ .ToList()
}
// TODO: send presence updates
// TODO: handle subscriptions
@@ -66,18 +80,33 @@ public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen
};
}
- private async Task<string?> GetMemberListIdAsync(SpacebarDbContext db, long guildId, long channelId) {
+ private async Task<string?> GetMemberListIdAsync(SpacebarDbContext db, long guildId, long 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 == "[]") {
+
+ if (string.IsNullOrWhiteSpace(channel.PermissionOverwrites) || channel.PermissionOverwrites == "[]")
+ {
return "everyone";
}
- return null; // TODO
+ List<string> perms = [];
+ foreach (var overwrite in channel.MappedPermissionOverwrites)
+ {
+ if (((Permissions) overwrite.Allow).HasFlag(Permissions.ViewChannel)) perms.Add($"allow:{overwrite}");
+ else if (((Permissions) overwrite.Deny).HasFlag(Permissions.ViewChannel)) perms.Add($"deny:{overwrite}");
+ }
+
+ perms.Sort();
+
+ ReadOnlySpan<byte> hashData = Encoding.UTF8.GetBytes(string.Join(",", perms));
+ var hashResult = MurmurHash3.Hash32(ref hashData);
+ return hashResult.ToString();
}
- private async Task<List<IMemberListEntry>> GetGuildMemberListAsync(SpacebarDbContext db, long guildId) {
+ private async Task<List<IMemberListEntry>> GetGuildMemberListAsync(SpacebarDbContext db, long guildId)
+ {
var memberList = new List<IMemberListEntry>();
// Fetch hoisted roles for the guild to define groups
@@ -90,10 +119,13 @@ public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen
logger.LogDebug("Got hoisted roles: {roleIds}", hoistedRoles.Select(x => x.Id).ToList());
List<long> handledRoles = [];
- foreach (var roleObj in hoistedRoles) {
+ foreach (var roleObj in hoistedRoles)
+ {
var role = roleObj.Id;
var members = await db.Members.AsNoTracking()
.Include(x => x.IdNavigation)
+ .ThenInclude(x => x.Sessions.Where(s => s.Status != "offline" && s.Status != "invisible" && s.Status != "unknown"))
+ .Include(x => x.Roles)
.Where(x =>
x.GuildId == guildId
&& x.Roles.Any(r => r.Id == role)
@@ -107,8 +139,11 @@ public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen
members.Count, role, roleObj.Name, string.Join("\n - ", members.Take(10).Select(x => $"{x.Id} {x.Nick ?? x.IdNavigation.Tag}"))
);
- memberList.Add(new RoleEntry() { Id = role.ToString(), Count = members.Count });
- memberList.AddRange(members.Select(m => (IMemberListEntry)new MemberEntry() { Member = m.ToPublicMember() }));
+ if (members.Count > 0)
+ {
+ memberList.Add(new RoleEntry() { Group = new() { Id = role.ToString(), Count = members.Count } });
+ memberList.AddRange(members.Select(m => (IMemberListEntry)new MemberEntry() { Member = m.ToPublicMember() }));
+ }
handledRoles.Add(role);
}
@@ -129,12 +164,14 @@ public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen
onlineMembers.Count, "online", "online", string.Join("\n - ", onlineMembers.Take(10).Select(x => $"{x.Id} {x.Nick ?? x.IdNavigation.Tag}"))
);
- if (onlineMembers.Count > 0) {
- memberList.Add(new RoleEntry() { Id = "online", Count = onlineMembers.Count });
+ if (onlineMembers.Count > 0)
+ {
+ memberList.Add(new RoleEntry() { Group = new() { Id = "online", Count = onlineMembers.Count } });
memberList.AddRange(onlineMembers.Select(m => (IMemberListEntry)new MemberEntry() { Member = m.ToPublicMember() }));
}
- if (memberList.Count < 2000) {
+ if (memberList.Count < 2000)
+ {
logger.LogInformation("Less than 2000 members, including offline members...");
var offlineMembers = await db.Members.AsNoTracking()
.Include(x => x.IdNavigation)
@@ -151,8 +188,12 @@ public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen
offlineMembers.Count, "offline", "offline", string.Join("\n - ", offlineMembers.Take(10).Select(x => $"{x.Id} {x.Nick ?? x.IdNavigation.Tag}"))
);
- if (offlineMembers.Count > 0) {
- memberList.Add(new RoleEntry() { Id = "offline", Count = offlineMembers.Count });
+ if (offlineMembers.Count > 0)
+ {
+ memberList.Add(new RoleEntry()
+ {
+ Group = new() { Id = "offline", Count = offlineMembers.Count }
+ });
memberList.AddRange(offlineMembers.Select(m => (IMemberListEntry)new MemberEntry() { Member = m.ToPublicMember() }));
}
}
@@ -162,17 +203,28 @@ public class Op14Controller(ILogger<Op12Controller> logger, SpacebarAspNetAuthen
}
}
-internal interface IMemberListEntry { }
+// TODO: either remove these classes by using the schema ones, or compress these down?
+internal interface IMemberListEntry
+{
+}
+
+internal struct RoleEntry : IMemberListEntry
+{
+ [JsonPropertyName("group")]
+ public Content Group { get; set; }
-internal struct RoleEntry : IMemberListEntry {
- [JsonPropertyName("id")]
- public string Id { get; set; }
+ internal struct Content
+ {
+ [JsonPropertyName("id")]
+ public string Id { get; set; }
- [JsonPropertyName("count")]
- public int Count { get; set; }
+ [JsonPropertyName("count")]
+ public int Count { get; set; }
+ }
}
-internal struct MemberEntry : IMemberListEntry {
+internal struct MemberEntry : IMemberListEntry
+{
[JsonPropertyName("member")]
public Member Member { get; set; }
}
\ No newline at end of file
diff --git a/extra/admin-api/Spacebar.Offload/Extensions/MurmurHash3.cs b/extra/admin-api/Spacebar.Offload/Extensions/MurmurHash3.cs
new file mode 100644
index 000000000..dfa9f3018
--- /dev/null
+++ b/extra/admin-api/Spacebar.Offload/Extensions/MurmurHash3.cs
@@ -0,0 +1,47 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using static System.Numerics.BitOperations;
+
+namespace Spacebar.Offload.Extensions;
+
+// https://github.com/JeremyEspresso/MurmurHash/blob/master/src/MurmurHash/MurmurHash3.cs
+// Changes: Default seed to 0
+public static class MurmurHash3
+{
+ /// <summary>
+ /// Hashes the <paramref name="bytes"/> into a MurmurHash3 as a <see cref="uint"/>.
+ /// </summary>
+ /// <param name="bytes">The span.</param>
+ /// <param name="seed">The seed for this algorithm.</param>
+ /// <returns>The MurmurHash3 as a <see cref="uint"/></returns>
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static uint Hash32(ref ReadOnlySpan<byte> bytes, uint seed = 0)
+ {
+ ref byte bp = ref MemoryMarshal.GetReference(bytes);
+ ref uint endPoint = ref Unsafe.Add(ref Unsafe.As<byte, uint>(ref bp), bytes.Length >> 2);
+ if (bytes.Length >= 4)
+ {
+ do
+ {
+ seed = RotateLeft(seed ^ RotateLeft(Unsafe.ReadUnaligned<uint>(ref bp) * 3432918353U, 15) * 461845907U, 13) * 5 - 430675100;
+ bp = ref Unsafe.Add(ref bp, 4);
+ } while (Unsafe.IsAddressLessThan(ref Unsafe.As<byte, uint>(ref bp), ref endPoint));
+ }
+
+ var remainder = bytes.Length & 3;
+ if (remainder > 0)
+ {
+ uint num = 0;
+ if (remainder > 2) num ^= Unsafe.Add(ref endPoint, 2) << 16;
+ if (remainder > 1) num ^= Unsafe.Add(ref endPoint, 1) << 8;
+ num ^= endPoint;
+
+ seed ^= RotateLeft(num * 3432918353U, 15) * 461845907U;
+ }
+
+ seed ^= (uint)bytes.Length;
+ seed = (uint)((seed ^ (seed >> 16)) * -2048144789);
+ seed = (uint)((seed ^ (seed >> 13)) * -1028477387);
+ return seed ^ seed >> 16;
+ }
+}
\ No newline at end of file
|