From ff223bda2e331d2b1764acf74aabafaec394d4a8 Mon Sep 17 00:00:00 2001 From: Infi Date: Fri, 30 Aug 2024 13:31:58 +0200 Subject: [PATCH] Move message processing to javascript, future proofing it for other frameworks --- ChatTwo/Http/MessageProtocol/DataStructure.cs | 17 ++++- ChatTwo/Http/Processing.cs | 46 ++++------- ChatTwo/Http/static/start.js | 76 ++++++++++++++++++- 3 files changed, 102 insertions(+), 37 deletions(-) diff --git a/ChatTwo/Http/MessageProtocol/DataStructure.cs b/ChatTwo/Http/MessageProtocol/DataStructure.cs index a7f067c..4c03ac2 100644 --- a/ChatTwo/Http/MessageProtocol/DataStructure.cs +++ b/ChatTwo/Http/MessageProtocol/DataStructure.cs @@ -3,9 +3,9 @@ namespace ChatTwo.Http.MessageProtocol; #region Outgoing SSE -public struct SwitchChannel(string name) +public struct SwitchChannel(MessageTemplate[] channelName) { - [JsonProperty("channel")] public string Name = name; + [JsonProperty("channelName")] public MessageTemplate[] ChannelName = channelName; } public struct ChannelList(Dictionary channels) @@ -21,7 +21,18 @@ public struct Messages(MessageResponse[] set) public struct MessageResponse() { [JsonProperty("timestamp")] public string Timestamp = ""; - [JsonProperty("messageHTML")] public string Message = ""; + [JsonProperty("templates")] public MessageTemplate[] Templates; +} + +public struct MessageTemplate() +{ + [JsonProperty("payload")] public required string Payload; + + [JsonProperty("content")] public string Content = ""; + [JsonProperty("id")] public uint Id; + [JsonProperty("color")] public uint Color; + + public static MessageTemplate Empty => new() {Payload = "empty"}; } #endregion diff --git a/ChatTwo/Http/Processing.cs b/ChatTwo/Http/Processing.cs index 030b920..96ec3be 100644 --- a/ChatTwo/Http/Processing.cs +++ b/ChatTwo/Http/Processing.cs @@ -1,5 +1,4 @@ using System.Globalization; -using System.Net; using ChatTwo.Code; using ChatTwo.Http.MessageProtocol; using ChatTwo.Util; @@ -16,9 +15,9 @@ public class Processing Plugin = plugin; } - internal string ReadChannelName(Chunk[] channelName) + internal MessageTemplate[] ReadChannelName(Chunk[] channelName) { - return string.Join("", channelName.Select(chunk => ProcessChunk(chunk, noColor: true))); + return channelName.Select(ProcessChunk).ToArray(); } internal async Task ReadMessageList() @@ -34,12 +33,9 @@ public class Processing Timestamp = message.Date.ToLocalTime().ToString("t", !Plugin.Config.Use24HourClock ? null : CultureInfo.CreateSpecificCulture("es-ES")) }; - var content = ""; - if (message.Sender.Count > 0) - content = message.Sender.Aggregate(content, (current, chunk) => current + ProcessChunk(chunk)); - - content = message.Content.Aggregate(content, (current, chunk) => current + ProcessChunk(chunk)); - response.Message = content; + var sender = message.Sender.Select(ProcessChunk); + var content = message.Content.Select(ProcessChunk); + response.Templates = sender.Concat(content).ToArray(); return response; } @@ -56,13 +52,12 @@ public class Processing sse.OutboundQueue.Enqueue(new ChannelListEvent(new ChannelList(channels.ToDictionary(pair => pair.Key, pair => (uint)pair.Value)))); } - private string ProcessChunk(Chunk chunk, bool noColor = false) + private MessageTemplate ProcessChunk(Chunk chunk) { if (chunk is IconChunk { } icon) { - return IconUtil.GfdFileView.TryGetEntry((uint) icon.Icon, out _) - ? $"" - : ""; + var iconId = (uint)icon.Icon; + return IconUtil.GfdFileView.TryGetEntry(iconId, out _) ? new MessageTemplate {Payload = "icon", Id = iconId}: MessageTemplate.Empty; } if (chunk is TextChunk { } text) @@ -71,20 +66,18 @@ public class Processing { var image = EmoteCache.GetEmote(emotePayload.Code); - // The emote name should be safe, it is checked against a list from BTTV. - // Still sanitizing it for the extra safety. if (image is { Failed: false }) - return $""; + return new MessageTemplate { Payload = "emote", Color = 0, Content = emotePayload.Code }; } - var colour = text.Foreground; - if (colour == null && text.FallbackColour != null) + var color = text.Foreground; + if (color == null && text.FallbackColour != null) { var type = text.FallbackColour.Value; - colour = Plugin.Config.ChatColours.TryGetValue(type, out var col) ? col : type.DefaultColor(); + color = Plugin.Config.ChatColours.TryGetValue(type, out var col) ? col : type.DefaultColor(); } - var color = ColourUtil.RgbaToComponents(colour ?? 0); + color ??= 0; var userContent = text.Content ?? ""; if (Plugin.ChatLogWindow.ScreenshotMode) @@ -95,17 +88,10 @@ public class Processing userContent = Plugin.ChatLogWindow.HidePlayerInString(userContent, player.Name.TextValue, player.HomeWorld.Id); } - // HTML encode any user content to prevent xss - userContent = WebUtility.HtmlEncode(userContent); - - if (text.Link is UriPayload uri) - userContent = $"{userContent}"; - - return noColor - ? userContent - : $"{userContent}"; + var isNotUrl = text.Link is not UriPayload; + return new MessageTemplate { Payload = isNotUrl ? "text" : "url", Color = color.Value, Content = userContent }; } - return string.Empty; + return MessageTemplate.Empty; } } diff --git a/ChatTwo/Http/static/start.js b/ChatTwo/Http/static/start.js index bd48767..a9fe997 100644 --- a/ChatTwo/Http/static/start.js +++ b/ChatTwo/Http/static/start.js @@ -72,8 +72,12 @@ }); } - updateChannelHint(labelHTML) { - this.elements.channelHint.innerHTML = labelHTML; + updateChannelHint(templates) { + this.elements.channelHint.innerHTML = ''; + + for(const template of templates) { + this.elements.channelHint.appendChild(this.processTemplate(template)); + } } updateChannels(channels) { @@ -131,7 +135,10 @@ spanMessage.classList.add('message'); spanTimestamp.innerText = messageData.timestamp; - spanMessage.innerHTML = messageData.messageHTML; + + for(const template of messageData.templates) { + spanMessage.appendChild(this.processTemplate(template)); + } liMessage.appendChild(spanTimestamp); liMessage.appendChild(spanMessage); @@ -142,6 +149,67 @@ } } + processTemplate(template) { + const spanElement = document.createElement('span'); + switch (template.payload) { + case 'text': + this.processTextTemplate(template, spanElement); + break; + case 'url': + this.processUrlTemplate(template, spanElement); + break; + case 'emote': + this.processEmote(template, spanElement); + break; + case 'icon': + this.processIcon(template, spanElement); + break; + case 'empty': + // Do nothing + break; + } + + return spanElement; + } + + processTextTemplate(template, spanContent) { + spanContent.innerText = template.content; + this.processColor(template, spanContent); + } + + processUrlTemplate(template, spanContent) { + // TODO Sanitize href? + let urlElement = document.createElement('a'); + urlElement.innerText = template.content; + urlElement.href = template.content; + urlElement.target = '_blank' + + this.processColor(template, spanContent); + } + + processColor(template, spanContent) { + let r = (template.color & 0xFF000000) >>> 24; + let g = (template.color & 0xFF0000) >>> 16; + let b = (template.color & 0xFF00) >>> 8; + let a = (template.color & 0xFF) / 255.0; + + spanContent.style.color = `rgba(${r}, ${g}, ${b}, ${a})`; + } + + processEmote(template, spanContent) { + // TODO Sanitize url? + let imgElement = document.createElement('img'); + imgElement.src = `/emote/${template.content}`; + + spanContent.classList.add('emote-icon'); + spanContent.appendChild(imgElement); + } + + processIcon(template, spanContent) { + spanContent.classList.add('gfd-icon'); + spanContent.classList.add(`gfd-icon-hq-${template.id}`); + } + clearAllMessages() { this.elements.messagesList.innerHTML = ''; } @@ -156,7 +224,7 @@ this.sse.addEventListener('switch-channel', (event) => { try { - this.updateChannelHint(JSON.parse(event.data).channel); + this.updateChannelHint(JSON.parse(event.data).channelName); } catch (error) { console.error(error); }