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;
|