From 8f76cc5edf8588c8bec36daf0bbdc7d699d38800 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 17 May 2026 03:14:34 +0200 Subject: [PATCH] client: basic message parsing, gateway listening --- .../Spacebar.Models.Gateway/GatewayPayload.cs | 2 +- .../Models/Spacebar.Models.Generic/Channel.cs | 46 +++++ .../Models/Spacebar.Models.Generic/Message.cs | 133 +++++++++++++++ .../Components/ChannelMessageList.razor | 158 ++++++++++++++++++ .../Components/ChannelMessageList.razor.cs | 30 ++++ .../Components/ChannelMessageList.razor.css | 7 + .../Components/ClientManager.razor | 64 +++++-- .../Core/MarkdownEnumerator.cs | 36 ++++ .../Spacebar.Client/Core/SpacebarClient.cs | 47 +++++- .../Spacebar.Client/Layout/NavMenu.razor | 26 ++- .../Spacebar.Client/Layout/NavMenu.razor.css | 25 ++- .../Spacebar.Client/Pages/Channels/@me.razor | 8 +- .../Pages/Channels/GuildChannel.razor | 72 ++++++++ .../Pages/Channels/GuildChannel.razor.css | 0 .../Pages/Discovery/GuildDiscovery.razor | 6 + .../Spacebar.Client/Spacebar.Client.csproj | 6 +- .../WebCore/Client/ClientStateContainer.cs | 8 + .../Spacebar.Client/wwwroot/css/app.css | 19 +++ 18 files changed, 664 insertions(+), 29 deletions(-) create mode 100644 extra/admin-api/Models/Spacebar.Models.Generic/Message.cs create mode 100644 extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor create mode 100644 extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.cs create mode 100644 extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.css create mode 100644 extra/admin-api/Utilities/Spacebar.Client/Core/MarkdownEnumerator.cs create mode 100644 extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor create mode 100644 extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor.css create mode 100644 extra/admin-api/Utilities/Spacebar.Client/Pages/Discovery/GuildDiscovery.razor create mode 100644 extra/admin-api/Utilities/Spacebar.Client/WebCore/Client/ClientStateContainer.cs diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs index 1a5d2c864..a9d560a4c 100644 --- a/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs +++ b/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs @@ -35,7 +35,7 @@ public class GatewayPayload { } } -public enum GatewayOpcode { +public enum GatewayOpcode : byte { S2CDispatch, Heartbeat, C2SIdentify, diff --git a/extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs b/extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs index 01a91c649..93a7592ec 100644 --- a/extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs +++ b/extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs @@ -3,7 +3,53 @@ using System.Text.Json.Serialization; namespace Spacebar.Models.Generic; public class Channel { + [JsonPropertyName("id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public required long Id { get; set; } + [JsonPropertyName("created_at")] + public DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("default_thread_rate_limit_per_user")] + public int DefaultThreadRateLimitPerUser { get; set; } + + [JsonPropertyName("flags")] + public int Flags { get; set; } //TODO: enum + + [JsonPropertyName("guild_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? GuildId { get; set; } + + [JsonPropertyName("icon")] + public string? Icon { get; set; } + + [JsonPropertyName("last_message_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? LastMessageId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("nsfw")] + public bool Nsfw { get; set; } + + [JsonPropertyName("parent_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? ParentId { get; set; } + + [JsonPropertyName("permission_overwrites")] + public List PermissionOverwrites { get; set; } + + [JsonPropertyName("position")] + public int Position { get; set; } + + [JsonPropertyName("status")] + public string? Status { get; set; } + + [JsonPropertyName("topic")] + public string? Topic { get; set; } + + [JsonPropertyName("type")] + public int Type { get; set; } //TODO enum + + [JsonPropertyName("video_quality_mode")] + public object? VideoQualityMode { get; set; } } public class ChannelPermissionOverwrite { diff --git a/extra/admin-api/Models/Spacebar.Models.Generic/Message.cs b/extra/admin-api/Models/Spacebar.Models.Generic/Message.cs new file mode 100644 index 000000000..4898daafd --- /dev/null +++ b/extra/admin-api/Models/Spacebar.Models.Generic/Message.cs @@ -0,0 +1,133 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Spacebar.Models.Generic; + +public class Message { + [JsonPropertyName("id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public required long Id { get; set; } + + [JsonPropertyName("channel_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? ChannelId { get; set; } + + [JsonPropertyName("guild_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? GuildId { get; set; } + + [JsonPropertyName("webhook_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? WebhookId { get; set; } + + [JsonPropertyName("application_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? ApplicationId { get; set; } + + [JsonPropertyName("content")] + public string? Content { get; set; } + + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } + + [JsonPropertyName("edited_timestamp")] + public DateTime? EditedTimestamp { get; set; } + + [JsonPropertyName("tts")] + public bool? Tts { get; set; } + + [JsonPropertyName("mention_everyone")] + public bool? MentionEveryone { get; set; } + + [JsonPropertyName("mentions")] + public List? Mentions { get; set; } + + [JsonPropertyName("embeds")] + public JsonObject[] Embeds { get; set; } + + [JsonPropertyName("reactions")] + public JsonObject[] Reactions { get; set; } = null!; + + [JsonPropertyName("nonce")] + public string? Nonce { get; set; } + + [JsonPropertyName("type")] + public int Type { get; set; } + + [JsonPropertyName("activity")] + public string? Activity { get; set; } + + [JsonPropertyName("message_reference")] + public MessageReference? MessageReference { get; set; } + + [JsonPropertyName("referenced_message")] + public Message? ReferencedMessage { get; set; } + + [JsonPropertyName("interaction")] + public string? Interaction { get; set; } + + [JsonPropertyName("components")] + public JsonObject[] Components { get; set; } + + [JsonPropertyName("flags")] + public int Flags { get; set; } + + [JsonPropertyName("poll")] + public string? Poll { get; set; } + + [JsonPropertyName("username")] + public string? Username { get; set; } + + [JsonPropertyName("avatar")] + public string? Avatar { get; set; } + + [JsonPropertyName("pinned_at")] + public DateTime? PinnedAt { get; set; } + + [JsonPropertyName("interaction_metadata")] + public JsonObject? InteractionMetadata { get; set; } + + [JsonPropertyName("message_snapshots")] + public JsonObject[]? MessageSnapshots { get; set; } + + [JsonPropertyName("thread_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? ThreadId { get; set; } + + [JsonPropertyName("author")] + public PartialUser Author { get; set; } + + [JsonPropertyName("attachments")] + public List Attachments { get; set; } +} + +public class MessageReference { + [JsonPropertyName("guild_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long? GuildId { get; set; } + + [JsonPropertyName("channel_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long ChannelId { get; set; } + + [JsonPropertyName("message_id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long MessageId { get; set; } +} + +public class MessageAttachment { + [JsonPropertyName("id"), JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] + public long Id { get; set; } + + [JsonPropertyName("filename")] + public string Filename { get; set; } + + [JsonPropertyName("size")] + public int Size { get; set; } + + [JsonPropertyName("height")] + public int? Height { get; set; } + [JsonPropertyName("width")] + public int? Width { get; set; } + + [JsonPropertyName("content_type")] + public string ContentType { get; set; } + + // channel id? message id? are these leaked properties? + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("proxy_url")] + public string ProxyUrl { get; set; } +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor b/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor new file mode 100644 index 000000000..6ac95c629 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor @@ -0,0 +1,158 @@ +@using System.Collections.ObjectModel +@using System.Text.Json +@using System.Text.RegularExpressions +@using ArcaneLibs.Blazor.Components.Services +@using ArcaneLibs.Extensions +@using Spacebar.Client.Core +@using Spacebar.Models.Generic +@inject JsConsoleService jsConsole + +@foreach (var message in Messages) { + if (message.Type == 0 || message.Type == 19) { + if (message.Type == 19) { + ╭⎯⎯ @message.ReferencedMessage?.Author.Username @string.Join("", message.ReferencedMessage?.Content?.Split("\n")[0].Take(100) ?? []) +
+ } + + $"role_{x}")))">@message.Author.Username +
+ @message.Content +
+
+ @GetMessageContent(message) +
+
+
+ @GetMessageContentEnumerated(message) +
+
+ @if (message.Attachments.Any()) { + @foreach (var att in message.Attachments) { + @if (att.ContentType.StartsWith("image/")) { + Attachment image + } + else { + @att.ToJson() + } + +
+ } + } + +
+ } + else { + + Unknown message type @message.Type +
+ View raw message data + @message.ToJson(indent: true) +
+
+
+ } +} + +@code { + + [Parameter] + public required ObservableCollection Messages { get; set; } + + public List GetMemberRoles(long guildId, long memberId) { + // App.ClientManager.ClientState.Guilds[guildId]. + return []; + } + + private static string[] _partColors = [ + "#FFFF0033", + "#FF00FF33", + "#00FFFF33", + "#FF000033", + "#00FF0033", + "#0000FF33" + ]; + + private static bool _shouldRenderMarkdownZones = true; + private RenderFragment GetMessageContent(Message msg) => builder => { + var fullContent = msg.Content; + int i = 0, line = 0; + Regex[][] groupedRegexes = [[MarkdownBoldRegex, MarkdownCodeblockRegex], [MarkdownCodeRegex, MarkdownItalicRegex]]; + Regex[] regexes = groupedRegexes.SelectMany(x => x).ToArray(); + + var lines = fullContent.Split('\n'); + foreach (var lineContent in lines) { + var content = lineContent; + var elemType = "span"; + var shouldBr = true; + if (content.StartsWith("-#")) { + elemType = "sub"; + content = content[2..].TrimStart(); + } + else if (content.StartsWith("#")) { + var hdrLevel = content.TakeWhile(x => x == '#').Count(); + content = content[hdrLevel..]; + shouldBr = false; + elemType = "h" + hdrLevel; + } + else if (content.StartsWith("*")) { + + } + + var indicies = regexes.Select(r => new { + regex = r, + regexStr = r.ToString(), + matchIdx = r.Match(content).Index, + matchContent = r.Match(content).Value + }).Where(x => x.matchIdx != 0).ToList(); + + if (indicies.Any()) { + jsConsole.Info("Found indices: ", JsonSerializer.SerializeToElement(indicies, new JsonSerializerOptions() { + IncludeFields = true + })); + + builder.OpenElement(i++, elemType); + { + if (_shouldRenderMarkdownZones) builder.AddAttribute(i++, "style", $"background-color: {_partColors[i % _partColors.Length]}"); + builder.AddContent(i++, content![..indicies.Min(x => x.matchIdx)]); + content = content![..indicies.Min(x => x.matchIdx)]; + } + builder.CloseComponent(); + } + else { + builder.OpenElement(i++, elemType); + { + if (_shouldRenderMarkdownZones && elemType != "span") builder.AddAttribute(i++, "style", $"background-color: {_partColors[i % _partColors.Length]}"); + builder.AddContent(i++, content!); + } + builder.CloseComponent(); + // continue; + } + + if (line++ <= lines.Length && shouldBr) { + builder.AddMarkupContent(i++, "
"); + } + } + }; + + private RenderFragment GetMessageContentEnumerated(Message msg) => builder => { + int i = 0; + builder.OpenElement(i++, "div"); + builder.AddAttribute(i++, "id", "msg"+msg.Id); + foreach (var comp in new MarkdownEnumerator().EnumerateMarkdownComponents(msg.Content)) { + if (comp is ContainerMarkdownNode) { + + } + else { + builder.OpenElement(i++, "span"); + builder.AddAttribute(i++, "class", "mdErrorBlinkBg"); + // jsConsole.Info("frames:", builder.GetFrames().Array[0].); + // builder.AddAttribute(i++, ); + builder.AddContent(i++, $"Unknown component type: {comp.GetType().FullName}"); + builder.CloseElement(); + } + } + + builder.CloseElement(); + }; + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.cs b/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.cs new file mode 100644 index 000000000..c1121f9b7 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.cs @@ -0,0 +1,30 @@ +using System.Text.RegularExpressions; + +namespace Spacebar.Client.Components; + +public partial class ChannelMessageList { + [GeneratedRegex(@"\*\*(.*)\*\*")] + private static partial Regex MarkdownBoldRegex { get; } + + [GeneratedRegex(@"\*(.*)\*")] + private static partial Regex MarkdownItalicRegex { get; } + + [GeneratedRegex(@"```((?.*)\n)(?.*)```")] + private static partial Regex MarkdownCodeblockRegex { get; } + + [GeneratedRegex(@"``?(.*)`?`")] + private static partial Regex MarkdownCodeRegex { get; } + + [GeneratedRegex(@"<#(\d*)>")] + private static partial Regex MarkdownChannelMentionRegex { get; } + + [GeneratedRegex(@"<@(\d*)>")] + private static partial Regex MarkdownUserMentionRegex { get; } + + [GeneratedRegex(@"<@&(\d*)>")] + private static partial Regex MarkdownRoleMentionRegex { get; } + + [GeneratedRegex(@"<:(?[a-zA-Z0-9]*?):(?\d*>)")] + private static partial Regex MarkdownEmojiMentionRegex { get; } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.css b/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.css new file mode 100644 index 000000000..a36877011 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.css @@ -0,0 +1,7 @@ +.attachmentImage { + max-height: 300px; + max-width: 300px; + object-position: center; + object-fit: cover; +} + diff --git a/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor b/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor index 71e82698d..353d86370 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor +++ b/extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor @@ -2,52 +2,95 @@ @using ArcaneLibs.Extensions @using Spacebar.Client.Core @using Spacebar.Client.WebCore +@using Spacebar.Client.WebCore.Client @using Spacebar.Models.Gateway @inject SessionStore sessionStore @inject SpacebarClientProviderService clientProvider @inject JsConsoleService jsConsole - + @ChildContent @code { private DebugBanner _dbgBanner = null!; - private AuthenticatedSpacebarClient? _client { get; set; } + private bool _readyReceived = false; + + public ClientManager() { + ClientAvailable = Task.Run(async () => { + while (Client is null) await Task.Delay(50); + ClientAvailable = null; + }); + ClientReady = Task.Run(async () => { + while (!_readyReceived) await Task.Delay(50); + ClientAvailable = null; + }); + } + + public AuthenticatedSpacebarClient? Client { get; set; } + + public ClientStateContainer ClientState { get; set; } = new(); [Parameter] public required RenderFragment ChildContent { get; set; } + public Task? ClientAvailable { get; set; } + public Task? ClientReady { get; set; } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; await _dbgBanner.SetStatus("Preparing for launch!"); await Task.Delay(125); var session = await sessionStore.GetCurrentSessionAsync(); if (session != null) { - _client = await clientProvider.GetAuthenticatedClientAsync(session.ServerName, session.AccessToken); + Client = await clientProvider.GetAuthenticatedClientAsync(session.ServerName, session.AccessToken); await _dbgBanner.SetStatus($"Got authenticated client for {session.ProfileCache.Username}#{session.ProfileCache.Discriminator} on {session.ServerName}! Connecting to gateway..."); - _client.Gateway.IdentifyData.ClientProperties = new IdentifyClientProperties() { + Client.Gateway.IdentifyData.ClientProperties = new IdentifyClientProperties() { HasClientMods = false, ApplicationArchitecture = "wasm" }.ToJsonNode().AsObject(); StateHasChanged(); - await _client.Gateway.Connect(); - _ = _client.Gateway.Start().ContinueWith(ct => { + await Client.Gateway.Connect(); + _ = Client.Gateway.Start().ContinueWith(ct => { jsConsole.Warn("[ClientManager] Heartbeat loop exited!"); if (ct.IsFaulted) { jsConsole.Error("Unhandled exception during gateway connection:", ct.Exception.ToString()); throw ct.Exception; } }); - _client.Gateway.OnceGatewayMessage.Add(async msg => { + Client.Gateway.OnceGatewayMessage.Add(async msg => { if (msg is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY" }) { - await _dbgBanner.SetStatus($"Got READY from gateway"); - await _dbgBanner.SetStatus(null, 1750); + await _dbgBanner.SetStatus($"Got READY from gateway, deserializing..."); var content = msg.GetData(); - await jsConsole.Info("Parsed ready payload:", content); + await _dbgBanner.SetStatus($"Deserialized READY from gateway, handling..."); + // ClientState.Guilds.AddRange(content.Guilds.ToDictionary(x=>x.Id, x=>x)); + foreach (var guild in content.Guilds) { + ClientState.Guilds.Add(guild.Id, guild); + await _dbgBanner.SetStatus($"Deserialized READY from gateway, handling... guilds ({ClientState.Guilds.Count})"); + await Task.Delay(1); + } + + foreach (var guild in content.Relationships) { + // ClientState.Relationships.Add(guild.Id, guild); + await _dbgBanner.SetStatus($"Deserialized READY from gateway, handling... guilds ({ClientState.Guilds.Count}), relationships (0)"); + await Task.Delay(1); + } + + await jsConsole.Info("Parsed ready payload:", new { original = msg.EventData, parsed = content }); + await _dbgBanner.SetStatus($"Done handling ready!"); + _readyReceived = true; + _ = _dbgBanner.SetStatus(null, 1750); + return false; + } + + if (msg is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "READY_SUPPLEMENTAL" }) { + await _dbgBanner.SetStatus("Received READY_SUPPLEMENTAL..."); + await jsConsole.Info("Parsed ready_supplemental payload", new { original = msg.EventData }); + _ =_dbgBanner.SetStatus(null, 1750); return true; } + return false; }); } @@ -55,7 +98,6 @@ await _dbgBanner.SetStatus("No session marked as current... :("); await _dbgBanner.SetStatus(null, 1750); } - } } \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Core/MarkdownEnumerator.cs b/extra/admin-api/Utilities/Spacebar.Client/Core/MarkdownEnumerator.cs new file mode 100644 index 000000000..600681356 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Core/MarkdownEnumerator.cs @@ -0,0 +1,36 @@ +namespace Spacebar.Client.Core; + +public class MarkdownEnumerator { + public IEnumerable EnumerateMarkdownComponents(string text) { + if (text.StartsWith("-#")) { + var line = text.Split('\n')[0]; + text = text.Replace(line + "\n", ""); + yield return new ContainerMarkdownNode() { + ComponentType = "sub", + Contents = new MarkdownEnumerator().EnumerateMarkdownComponents(line[2..].TrimStart()).ToList() + }; + } + else if (text.StartsWith("#")) { + var hdrLevel = text.TakeWhile(x => x == '#').Count(); + var line = text.Split('\n')[0]; + text = text.Replace(line + "\n", ""); + yield return new ContainerMarkdownNode() { + ComponentType = "h" + hdrLevel, + Contents = new MarkdownEnumerator().EnumerateMarkdownComponents(line[hdrLevel..].TrimStart()).ToList() + }; + } + yield return new InnerTextMarkdownNode(text); + } +} + +public class BaseMarkdownNode { +} + +public class ContainerMarkdownNode : BaseMarkdownNode { + public string ComponentType { get; set; } + public List Contents { get; set; } +} + +public class InnerTextMarkdownNode(string Text) : BaseMarkdownNode{ + public string Text { get; set; } +} diff --git a/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClient.cs b/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClient.cs index f8d903096..cd624e5d9 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClient.cs +++ b/extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClient.cs @@ -32,10 +32,12 @@ public class AuthenticatedSpacebarClient { }; ApiHttpClient.DefaultRequestHeaders.Authorization = new("Bearer", token); Gateway = new(sp.GetRequiredService>(), wellKnown, token); + ClientWellKnown = wellKnown; } public HttpClient ApiHttpClient { get; set; } public AuthenticatedSpacebarGatewayClient Gateway { get; set; } + public SpacebarClientWellKnown ClientWellKnown { get; set; } // TODO: write a proper full user model... public async Task GetCurrentUser() { @@ -48,6 +50,28 @@ public class AuthenticatedSpacebarClient { ~AuthenticatedSpacebarClient() { ApiHttpClient.Dispose(); } + + public SpacebarClientChannel GetChannel(long channelId) { + return new(this, channelId); + } +} + +public class SpacebarClientChannel(AuthenticatedSpacebarClient client, long channelId) { + public long Id => channelId; + + public async Task> GetMessagesAsync(long? around = null, long? before = null, long? after = null, int limit = 50) { + var uri = $"channels/{channelId}/messages?limit={limit}"; + if (around.HasValue) uri += $"&around={around.Value}"; + if (before.HasValue) uri += $"&before={before.Value}"; + if (after.HasValue) uri += $"&after={after.Value}"; + + var resp = await client.ApiHttpClient.GetAsync(uri); + // TODO: abstract out + if (!resp.IsSuccessStatusCode) throw SpacebarApiException.FromJson((await resp.Content.ReadFromJsonAsync())!); + var data = await resp.Content.ReadFromJsonAsync>(); + Console.WriteLine(data.ToJson(indent: false, ignoreNull: true)); + return data.Select(x => x.Deserialize()).ToList(); + } } public class AuthenticatedSpacebarGatewayClient(ILogger logger, SpacebarClientWellKnown wellKnown, string token) { @@ -87,11 +111,23 @@ public class AuthenticatedSpacebarGatewayClient(ILogger x(msg)).ToArray()); + await Task.WhenAll(OnGatewayMessage.Select(async x => { + try { + await x(msg); + } + catch (Exception e) { + logger.LogError("OnGatewayMessage callback failed: {e}", e); + } + }).ToArray()); foreach (var t in OnceGatewayMessage.Select((Func> Callback, Task WasHandled) (cb) => (cb, cb(msg))).ToList()) { - var handled = await t.WasHandled; - if (handled) { - OnceGatewayMessage.Remove(t.Callback); + try { + var handled = await t.WasHandled; + if (handled) { + OnceGatewayMessage.Remove(t.Callback); + } + } + catch (Exception e) { + logger.LogError("OnceGatewayMessage callback failed: {e}", e); } } } @@ -134,13 +170,14 @@ public class AuthenticatedSpacebarGatewayClient(ILogger(fullMsg); trace.Add(($"LJS({fullMsg.Length})", sw.GetElapsedAndRestart())); + Console.WriteLine($"Received gateway message #{d.Sequence}: {(byte)d.Opcode}/{d.Opcode.ToString().Replace("S2C", "")} {d.DispatchEventType}"); yield return d ?? throw new InvalidDataException("Gateway message deserialisation returned null?"); trace.Add(($"YLD", sw.GetElapsedAndRestart())); diff --git a/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor b/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor index ccc73abe4..e6c229628 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor +++ b/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor @@ -14,14 +14,22 @@ Home + @foreach (var guild in App.ClientManager.ClientState.Guilds) { + + } - @@ -36,4 +44,8 @@ collapseNavMenu = !collapseNavMenu; } + protected override async Task OnInitializedAsync() { + App.ClientManager.ClientState.Guilds.CollectionChanged += (_,_) => this.StateHasChanged(); + } + } \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor.css b/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor.css index 617b89cc8..3c280aa14 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor.css +++ b/extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor.css @@ -1,3 +1,26 @@ +.navGuildIcon { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: cover; + object-position: center; + image-rendering: high-quality; +} + +a:hover > .navGuildIcon { + transition: 0.15s; + border-radius: 25%; +} + +.nav-item > a.nav-link a { + text-overflow: fade; + text-wrap: nowrap; + padding-left: 0; + line-height: 1rem; +} + +/* default CSS */ + .navbar-toggler { background-color: rgba(255, 255, 255, 0.1); } @@ -52,7 +75,7 @@ height: 3rem; display: flex; align-items: center; - line-height: 3rem; + line-height: 1rem; } .nav-item ::deep a.active { diff --git a/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/@me.razor b/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/@me.razor index 28709841d..3a47c7bf3 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/@me.razor +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/@me.razor @@ -2,13 +2,16 @@ @attribute [Route(PageUri)] @using ArcaneLibs.Blazor.Components.Services @using Spacebar.Client.Core -@using Spacebar.Models.Gateway @inject JsConsoleService jsConsole

@@me

@Client?.ApiHttpClient.BaseAddress

@Client?.Gateway.RawClientWebSocket.State

+@foreach (var guild in App.ClientManager.ClientState.Guilds) { + @guild.Value.Name
+} + @code { private const string PageUri = "/channels/@me"; @@ -21,7 +24,8 @@ protected override async Task OnParametersSetAsync() { if (Client == null) return; Client.Gateway.OnGatewayMessage.Add(async msg => { - await jsConsole.Log("Received gateway message: ", msg); + // await jsConsole.Log("Received gateway message: ", msg); + // StateHasChanged(); }); } diff --git a/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor b/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor new file mode 100644 index 000000000..67c637ccf --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor @@ -0,0 +1,72 @@ +@page "/channels/{GuildId:long}/{ChannelId:long}" +@page "/channels/{GuildId:long}/{ChannelId:long}/{MessageId:long}" +@using System.Collections.ObjectModel +@using ArcaneLibs.Blazor.Components.Services +@using ArcaneLibs.Extensions +@using Spacebar.Client.Components +@using Spacebar.Models.Gateway +@using Spacebar.Models.Generic +@inject JsConsoleService jsConsole + + + + +@code { + + private ObservableCollection _messages { get; set; } = []; + private string _guildCss = ""; + + [Parameter] + public long GuildId { get; set; } + + [Parameter] + public long ChannelId { get; set; } + + [Parameter] + public long? MessageId { get; set; } + + protected override async Task OnInitializedAsync() { + var cid = ChannelId; + Console.WriteLine($"OIA GuildChannel(g={GuildId},c={ChannelId},m={MessageId})"); + await (App.ClientManager.ClientAvailable ?? Task.CompletedTask); + if (MessageId is null) { + App.ClientManager.Client?.Gateway.OnceGatewayMessage.Add(async evt => { + if (evt is { Opcode: GatewayOpcode.S2CDispatch, DispatchEventType: "MESSAGE_CREATE" }) { + if (evt.EventData["channel_id"].GetValue() != ChannelId.ToString()) return false; + await jsConsole.Info("Current channel message:", evt); + var msg = evt.GetData(); + await jsConsole.Info("... typed:", msg); + _messages.Add(msg); + StateHasChanged(); + } + + return cid != ChannelId; + }); + await (App.ClientManager.ClientReady ?? Task.CompletedTask); + _messages = new ObservableCollection(Enumerable.Reverse(await App.ClientManager.Client.GetChannel(ChannelId).GetMessagesAsync( + before: App.ClientManager.ClientState.Guilds[GuildId].Channels.First(x => x.Id == ChannelId).LastMessageId + 1, + limit: 100 + ))); + + // build role color css... + _guildCss = ""; + foreach (var role in App.ClientManager.ClientState.Guilds[GuildId].Roles) { + _guildCss += $$""" + .role_{{role.Id}} { + color: #{{role.Colors.PrimaryColor.ToString("X6")}}; + } + + """; + } + } + } + + public override async Task SetParametersAsync(ParameterView parameters) { + bool reset = false; + if (parameters.GetValueOrDefault("ChannelId") != ChannelId) reset = true; + + await base.SetParametersAsync(parameters); + if (reset) await OnInitializedAsync(); + } + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor.css b/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor.css new file mode 100644 index 000000000..e69de29bb diff --git a/extra/admin-api/Utilities/Spacebar.Client/Pages/Discovery/GuildDiscovery.razor b/extra/admin-api/Utilities/Spacebar.Client/Pages/Discovery/GuildDiscovery.razor new file mode 100644 index 000000000..306cc8cff --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/Discovery/GuildDiscovery.razor @@ -0,0 +1,6 @@ +@page "/discovery/guilds" +

GuildDiscovery

+ +@code { + +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj b/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj index aae396d10..28bb8024a 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj +++ b/extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj @@ -14,9 +14,11 @@ false false false - false + false true + full + @@ -24,7 +26,7 @@ - + diff --git a/extra/admin-api/Utilities/Spacebar.Client/WebCore/Client/ClientStateContainer.cs b/extra/admin-api/Utilities/Spacebar.Client/WebCore/Client/ClientStateContainer.cs new file mode 100644 index 000000000..46d82b943 --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/WebCore/Client/ClientStateContainer.cs @@ -0,0 +1,8 @@ +using ArcaneLibs.Collections; +using Spacebar.Models.Generic; + +namespace Spacebar.Client.WebCore.Client; + +public class ClientStateContainer { + public ObservableDictionary Guilds { get; set; } = []; +} \ No newline at end of file diff --git a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/app.css b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/app.css index 7c6e626e9..0373382ef 100644 --- a/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/app.css +++ b/extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/app.css @@ -24,6 +24,25 @@ pre, .code { height: 0; } +.mdErrorBlinkBg { + animation: ease-in-out 1s mdErrorBlinkBgAnim; + border: 1px #ff00ff; + margin-right: 1em; + border-radius: 3px; +} + +@keyframes mdErrorBlinkBgAnim { + 0%{ + background-color: #ffff0088; + } + 50% { + background-color: #ffff0000; + } + 100% { + background-color: #ffff0088; + } +} + #app > div > main > div { background-color: #333; border-bottom: none;