summary refs log tree commit diff
diff options
context:
space:
mode:
authorRory& <root@rory.gay>2026-05-17 03:14:34 +0200
committerRory& <root@rory.gay>2026-05-17 03:14:34 +0200
commit8f76cc5edf8588c8bec36daf0bbdc7d699d38800 (patch)
treed84535fa9d66a674ed652d0b7d5e6cb4865ba7ac
parentAdd WIP client (diff)
downloadserver-ts-8f76cc5edf8588c8bec36daf0bbdc7d699d38800.tar.xz
client: basic message parsing, gateway listening
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs2
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Generic/Channel.cs46
-rw-r--r--extra/admin-api/Models/Spacebar.Models.Generic/Message.cs133
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor158
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.cs30
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Components/ChannelMessageList.razor.css7
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Components/ClientManager.razor64
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Core/MarkdownEnumerator.cs36
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Core/SpacebarClient.cs47
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor26
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Layout/NavMenu.razor.css25
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/@me.razor8
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor72
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor.css0
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Pages/Discovery/GuildDiscovery.razor6
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/Spacebar.Client.csproj6
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/WebCore/Client/ClientStateContainer.cs8
-rw-r--r--extra/admin-api/Utilities/Spacebar.Client/wwwroot/css/app.css19
18 files changed, 664 insertions, 29 deletions
diff --git a/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs b/extra/admin-api/Models/Spacebar.Models.Gateway/GatewayPayload.cs

index 1a5d2c86..a9d560a4 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 01a91c64..93a7592e 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<ChannelPermissionOverwrite> 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 00000000..4898daaf --- /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<PartialUser>? 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<MessageAttachment> 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 00000000..6ac95c62 --- /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) { + <span>╭⎯⎯ <b>@message.ReferencedMessage?.Author.Username</b> @string.Join("", message.ReferencedMessage?.Content?.Split("\n")[0].Take(100) ?? [])</span> + <br/> + } + + <b class="@(string.Join(" ", GetMemberRoles(message.GuildId.Value, message.Author.Id).Select(x => $"role_{x}")))">@message.Author.Username</b> + <br/> + <span>@message.Content</span> + <br/> + <div style="background-color: #FFFF0033;"> + @GetMessageContent(message) + </div> + <br/> + <div style="background-color: #FF00FF33;"> + @GetMessageContentEnumerated(message) + </div> + <br/> + @if (message.Attachments.Any()) { + @foreach (var att in message.Attachments) { + @if (att.ContentType.StartsWith("image/")) { + <img src="@att.ProxyUrl" class="attachmentImage" alt="Attachment image"/> + } + else { + <span class="code">@att.ToJson()</span> + } + + <br/> + } + } + + <br/> + } + else { + <span class="code" style="background-color: #772222"> + Unknown message type @message.Type + <details> + <summary>View raw message data</summary> + @message.ToJson(indent: true) + </details> + </span> + <br/> + } +} + +@code { + + [Parameter] + public required ObservableCollection<Message> Messages { get; set; } + + public List<string> 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++, "<br/>"); + } + } + }; + + 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 00000000..c1121f9b --- /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(@"```((?<lang>.*)\n)(?<content>.*)```")] + 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(@"<:(?<name>[a-zA-Z0-9]*?):(?<emojiId>\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 00000000..a3687701 --- /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 71e82698..353d8637 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 <DebugBanner Name="@GetType().Name" @ref="_dbgBanner"/> -<CascadingValue TValue="AuthenticatedSpacebarClient" Value="@_client"> +<CascadingValue TValue="AuthenticatedSpacebarClient" Value="@Client"> @ChildContent </CascadingValue> @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<ReadyResponse>(); - 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 00000000..60068135 --- /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<BaseMarkdownNode> 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<BaseMarkdownNode> 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 f8d90309..cd624e5d 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<ILogger<AuthenticatedSpacebarGatewayClient>>(), 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<PartialUser> 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<List<Message>> 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<JsonObject>())!); + var data = await resp.Content.ReadFromJsonAsync<List<JsonObject>>(); + Console.WriteLine(data.ToJson(indent: false, ignoreNull: true)); + return data.Select(x => x.Deserialize<Message>()).ToList(); + } } public class AuthenticatedSpacebarGatewayClient(ILogger<AuthenticatedSpacebarGatewayClient> logger, SpacebarClientWellKnown wellKnown, string token) { @@ -87,11 +111,23 @@ public class AuthenticatedSpacebarGatewayClient(ILogger<AuthenticatedSpacebarGat logger.LogInformation("Got heartbeat ACK from server!"); } - await Task.WhenAll(OnGatewayMessage.Select(x => 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<GatewayPayload, Task<bool>> Callback, Task<bool> 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<AuthenticatedSpacebarGat idx++; if (msg.EndOfMessage) { - Console.WriteLine("Got message, deserialising..."); + Console.WriteLine($"Got message, deserialising {messageParts.Count} bytes..."); var fullMsg = messageParts.ToArray(); trace.Add(($"LD({messageParts.Count})", sw.GetElapsedAndRestart())); var d = JsonSerializer.Deserialize<GatewayPayload>(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 ccc73abe..e6c22962 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 @@ <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home </NavLink> </div> + @foreach (var guild in App.ClientManager.ClientState.Guilds) { + <div class="nav-item px-3"> + <NavLink class="nav-link" href="@($"/channels/{guild.Key}/{guild.Value.Channels.First(x => x.Type == 0).Id}")"> + @{ + var guildIconUrl = string.IsNullOrWhiteSpace(guild.Value.Icon) + ? "/img/icon_white.png" + : new Uri($"{App.ClientManager.Client?.ClientWellKnown.Cdn.BaseUrl}/icons/{guild.Key}/{guild.Value.Icon}").AbsoluteUri; + } + <img class="navGuildIcon" src="@guildIconUrl" alt="" aria-hidden="true"> + @guild.Value.Name + </NavLink> + </div> + } <div class="nav-item px-3"> - <NavLink class="nav-link" href="counter"> - <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter - </NavLink> - </div> - <div class="nav-item px-3"> - <NavLink class="nav-link" href="weather"> - <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather + <NavLink class="nav-link" href="/discovery/guilds"> + <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Discover... </NavLink> </div> </nav> @@ -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 617b89cc..3c280aa1 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 28709841..3a47c7bf 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 <h3>@@me</h3> <p>@Client?.ApiHttpClient.BaseAddress</p> <p>@Client?.Gateway.RawClientWebSocket.State</p> +@foreach (var guild in App.ClientManager.ClientState.Guilds) { + <span class="code">@guild.Value.Name</span><br/> +} + @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 00000000..67c637cc --- /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 + +<style>@_guildCss</style> +<ChannelMessageList Messages="@_messages"></ChannelMessageList> + +@code { + + private ObservableCollection<Message> _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<string>() != ChannelId.ToString()) return false; + await jsConsole.Info("Current channel message:", evt); + var msg = evt.GetData<Message>(); + await jsConsole.Info("... typed:", msg); + _messages.Add(msg); + StateHasChanged(); + } + + return cid != ChannelId; + }); + await (App.ClientManager.ClientReady ?? Task.CompletedTask); + _messages = new ObservableCollection<Message>(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<long>("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 00000000..e69de29b --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/Channels/GuildChannel.razor.css
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 00000000..306cc8cf --- /dev/null +++ b/extra/admin-api/Utilities/Spacebar.Client/Pages/Discovery/GuildDiscovery.razor
@@ -0,0 +1,6 @@ +@page "/discovery/guilds" +<h3>GuildDiscovery</h3> + +@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 aae396d1..28bb8024 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 @@ <BlazorEnableCompression>false</BlazorEnableCompression> <CompressionEnabled>false</CompressionEnabled> <BlazorCacheBootResources>false</BlazorCacheBootResources> - <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport> + <!--<BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>--> <WasmEnableHotReload>false</WasmEnableHotReload> <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + </PropertyGroup> <ItemGroup> @@ -24,7 +26,7 @@ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" PrivateAssets="all"/> </ItemGroup> - + <ItemGroup> <ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js"/> </ItemGroup> 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 00000000..46d82b94 --- /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<long, Guild> 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 7c6e626e..0373382e 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;