summary refs log tree commit diff
path: root/extra/admin-api
diff options
context:
space:
mode:
Diffstat (limited to 'extra/admin-api')
-rw-r--r--extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Member.cs5
-rw-r--r--extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/User.cs6
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs72
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/LazyRequest.cs4
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Generic/Member.cs4
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Generic/User.cs47
-rw-r--r--extra/admin-api/Spacebar.Offload/Controllers/Op14Controller.cs116
-rw-r--r--extra/admin-api/Spacebar.Offload/Extensions/MurmurHash3.cs47
8 files changed, 232 insertions, 69 deletions
diff --git a/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Member.cs b/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Member.cs

index 2a61b75a..1693edd3 100644 --- a/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Member.cs +++ b/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/Member.cs
@@ -1,3 +1,4 @@ +using System.Text.Json; using Spacebar.Models.Generic; namespace Spacebar.DataMappings.Generic; @@ -13,10 +14,10 @@ public static class MemberExtensions Avatar = string.IsNullOrWhiteSpace(member.Avatar) ? null : member.Avatar, Banner = string.IsNullOrWhiteSpace(member.Banner) ? null : member.Banner, Collectibles = member.Collectibles, - DisplayNameStyles = member.DisplayNameStyles, + DisplayNameStyles = JsonSerializer.Deserialize<DisplayNameStyle>(member.DisplayNameStyles ?? "null"), Bio = string.IsNullOrWhiteSpace(member.Bio) ? null : member.Bio, Nick = string.IsNullOrWhiteSpace(member.Nick) ? null : member.Nick, - Roles = member.Roles.Select(x=>x.Id).ToList() + Roles = member.Roles.Select(x => x.Id).ToList() }; } } \ No newline at end of file diff --git a/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/User.cs b/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/User.cs
index 607af941..95bce884 100644 --- a/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/User.cs +++ b/extra/admin-api/DataMappings/Spacebar.DataMappings.Generic/User.cs
@@ -1,3 +1,4 @@ +using System.Text.Json; using Spacebar.Models.Generic; namespace Spacebar.DataMappings.Generic; @@ -6,7 +7,8 @@ public static class User { public static PartialUser ToPartialUser(this Models.Db.Models.User user) { - return new PartialUser() { + return new PartialUser() + { Id = user.Id, Discriminator = user.Discriminator, Username = user.Username, @@ -16,7 +18,7 @@ public static class User Banner = user.Banner, Bot = user.Bot, Collectibles = user.Collectibles, - DisplayNameStyles = user.DisplayNameStyles, + DisplayNameStyles = JsonSerializer.Deserialize<DisplayNameStyle>(user.DisplayNameStyles ?? "null"), // GlobalName = x.GlobalName, PrimaryGuild = user.PrimaryGuild, PublicFlags = user.PublicFlags, diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs
index 45d28467..d1692cc7 100644 --- a/extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/GuildMemberListUpdate.cs
@@ -3,7 +3,8 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Spacebar.Models.Generic; -public class GuildMemberListUpdate { +public class GuildMemberListUpdate +{ public const string EventId = "GUILD_MEMBER_LIST_UPDATE"; [JsonPropertyName("id")] @@ -22,11 +23,12 @@ public class GuildMemberListUpdate { public List<GuildMemberListUpdateOperation> Operations { get; set; } = null!; [JsonPropertyName("groups")] - public List<GuildMemberListSyncItem.RoleEntry> Groups { get; set; } = null!; + public List<GuildMemberListSyncItem.RoleEntry.Content> Groups { get; set; } = null!; } // i cba to write a dictionary converter for this... -public class GuildMemberListGroupCount { +public class GuildMemberListGroupCount +{ /// <summary> /// Role ID, "online" or "offline" /// </summary> @@ -40,24 +42,28 @@ public class GuildMemberListGroupCount { 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, +[JsonConverter(typeof(JsonStringEnumConverter<GuildMemberListUpdateOperationType>))] +public enum GuildMemberListUpdateOperationType +{ + [JsonStringEnumMemberName("SYNC")] Sync = 0, + [JsonStringEnumMemberName("INSERT")] Insert = 1, + [JsonStringEnumMemberName("UPDATE")] Update = 2, + [JsonStringEnumMemberName("DELETE")] Delete = 3, - [JsonStringEnumMemberName("invalidate")] + [JsonStringEnumMemberName("INVALIDATE")] Invalidate = 4 } #region Operations [JsonConverter(typeof(GuildMemberListUpdateOperationJsonConverter))] -public class GuildMemberListUpdateOperation { +public class GuildMemberListUpdateOperation +{ [JsonPropertyName("op")] public GuildMemberListUpdateOperationType Operation { get; set; } - - public class SyncOperation : GuildMemberListUpdateOperation { + + public class SyncOperation : GuildMemberListUpdateOperation + { [JsonPropertyName("range")] public int[] Range { get; set; } = null!; @@ -67,25 +73,36 @@ public class GuildMemberListUpdateOperation { } [JsonConverter(typeof(GuildMemberListSyncItemJsonConverter))] -public class GuildMemberListSyncItem { - public class RoleEntry : GuildMemberListSyncItem { - [JsonPropertyName("id")] - public string Id { get; set; } +public class GuildMemberListSyncItem +{ + public class RoleEntry : GuildMemberListSyncItem + { + [JsonPropertyName("group")] + public Content Group { get; set; } + + public class Content + { + [JsonPropertyName("id")] + public string Id { get; set; } - [JsonPropertyName("count")] - public long Count { get; set; } + [JsonPropertyName("count")] + public long Count { get; set; } + } } - public class MemberEntry : GuildMemberListSyncItem { + public class MemberEntry : GuildMemberListSyncItem + { [JsonPropertyName("member")] public Member Member { get; set; } = null!; } } -public class GuildMemberListSyncItemJsonConverter : JsonConverter<GuildMemberListSyncItem> { - public override GuildMemberListSyncItem? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { +public class GuildMemberListSyncItemJsonConverter : JsonConverter<GuildMemberListSyncItem> +{ + public override GuildMemberListSyncItem? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { var n = JsonSerializer.Deserialize<JsonObject>(ref reader, options); - if (n.ContainsKey("id") && n.ContainsKey("count")) return n.Deserialize<GuildMemberListSyncItem.RoleEntry>(); + if (n.ContainsKey("group")) return n.Deserialize<GuildMemberListSyncItem.RoleEntry>(); if (n.ContainsKey("member")) return n.Deserialize<GuildMemberListSyncItem>(); throw new InvalidOperationException("Could not determine sync item type for keys " + string.Join(", ", n.Select(x => x.Key))); } @@ -94,11 +111,14 @@ public class GuildMemberListSyncItemJsonConverter : JsonConverter<GuildMemberLis JsonSerializer.Serialize(writer, value, value.GetType(), options); } -public class GuildMemberListUpdateOperationJsonConverter : JsonConverter<GuildMemberListUpdateOperation> { - public override GuildMemberListUpdateOperation? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { +public class GuildMemberListUpdateOperationJsonConverter : JsonConverter<GuildMemberListUpdateOperation> +{ + public override GuildMemberListUpdateOperation? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { var n = JsonSerializer.Deserialize<JsonObject>(ref reader, options); - return n!["op"]!.GetValue<string>() switch { - "sync" => n.Deserialize<GuildMemberListUpdateOperation.SyncOperation>(), + return n!["op"]!.GetValue<string>() switch + { + "SYNC" => n.Deserialize<GuildMemberListUpdateOperation.SyncOperation>(), _ => throw new InvalidCastException("Unknown operation: " + n["op"]!.GetValue<string>()) }; } diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/LazyRequest.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/LazyRequest.cs
index c144eb72..ee7895af 100644 --- a/extra/admin-api/Models/Spacebar.Models.Gateway/LazyRequest.cs +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/LazyRequest.cs
@@ -12,8 +12,8 @@ public class LazyRequest [JsonPropertyName("channels")] public Dictionary<string, List<List<int>>> Channels { get; set; } - [JsonPropertyName("members")] - public bool Members { get; set; } + [JsonPropertyName("members"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public List<long>? Members { get; set; } [JsonPropertyName("threads")] public bool Threads { get; set; } diff --git a/extra/admin-api/Models/Spacebar.Models.Generic/Member.cs b/extra/admin-api/Models/Spacebar.Models.Generic/Member.cs
index fa217a37..b11a041b 100644 --- a/extra/admin-api/Models/Spacebar.Models.Generic/Member.cs +++ b/extra/admin-api/Models/Spacebar.Models.Generic/Member.cs
@@ -23,8 +23,8 @@ public class Member { [JsonPropertyName("collectibles")] public object? Collectibles { get; set; } - [JsonPropertyName("display_name_styles")] - public object? DisplayNameStyles { get; set; } + [JsonPropertyName("display_name_styles"), JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public DisplayNameStyle? DisplayNameStyles { get; set; } [JsonPropertyName("banner")] public string? Banner { get; set; } diff --git a/extra/admin-api/Models/Spacebar.Models.Generic/User.cs b/extra/admin-api/Models/Spacebar.Models.Generic/User.cs
index 428794af..ad0028d8 100644 --- a/extra/admin-api/Models/Spacebar.Models.Generic/User.cs +++ b/extra/admin-api/Models/Spacebar.Models.Generic/User.cs
@@ -1,10 +1,12 @@ +using System.ComponentModel.DataAnnotations; using System.Diagnostics; using System.Text.Json.Serialization; namespace Spacebar.Models.Generic; [DebuggerDisplay("{Id} ({Username}#{Discriminator})")] -public class PartialUser { +public class PartialUser +{ [JsonPropertyName("id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] public required long Id { get; set; } @@ -26,8 +28,8 @@ public class PartialUser { [JsonPropertyName("collectibles")] public object? Collectibles { get; set; } - [JsonPropertyName("display_name_styles")] - public object? DisplayNameStyles { get; set; } + [JsonPropertyName("display_name_styles"), JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public DisplayNameStyle? DisplayNameStyles { get; set; } [JsonPropertyName("primary_guild")] public object? PrimaryGuild { get; set; } @@ -46,4 +48,43 @@ public class PartialUser { [JsonPropertyName("public_flags")] public ulong? PublicFlags { get; set; } +} + +public class DisplayNameStyle +{ + [JsonPropertyName("font_id")] + public Font FontId { get; set; } = Font.Default; + + [JsonPropertyName("effect_id")] + public Effect EffectId { get; set; } = Effect.Solid; + + [JsonPropertyName("colors"), MaxLength(2)] + public List<int> Colors { get; set; } = []; + + // https://docs.discord.food/resources/user#display-name-font - unsure if these are just really close or directly used by discord while being renamed... + public enum Font + { + Bangers = 1, + BioRhyme = 2, + CherryBomb = 3, + Chicle = 4, + Compagnon = 5, + MuseoModerno = 6, + NeoCastel = 7, + Pixelify = 8, + Ribes = 9, + Sinistre = 10, + Default = 11, + ZillaSlab = 12 + } + + public enum Effect + { + Solid = 1, + Gradient = 2, + Neon = 3, + Toon = 4, + Pop = 5, + Glow = 6 + } } \ No newline at end of file diff --git a/extra/admin-api/Spacebar.Offload/Controllers/Op14Controller.cs b/extra/admin-api/Spacebar.Offload/Controllers/Op14Controller.cs
index f3445554..79369de1 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 00000000..dfa9f301 --- /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