From 92aafcde70a7458083f52117f3d3c4d1ae7ea512 Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Mon, 17 Jan 2022 16:22:15 +0100 Subject: [PATCH] Smarter message parsing Future: make return params dynamic, not string now requires type in message objects --- MinecraftDiscordBot/ClientScript.lua | 2 +- MinecraftDiscordBot/Models/Message.cs | 85 +++++++++++++------ .../Models/MessageTypeAttribute.cs | 7 ++ MinecraftDiscordBot/Models/ResultState.cs | 7 ++ .../Services/RootCommandService.cs | 28 +++--- 5 files changed, 90 insertions(+), 39 deletions(-) create mode 100644 MinecraftDiscordBot/Models/MessageTypeAttribute.cs create mode 100644 MinecraftDiscordBot/Models/ResultState.cs diff --git a/MinecraftDiscordBot/ClientScript.lua b/MinecraftDiscordBot/ClientScript.lua index 453edd7..16b1d91 100644 --- a/MinecraftDiscordBot/ClientScript.lua +++ b/MinecraftDiscordBot/ClientScript.lua @@ -29,7 +29,7 @@ local function sendResponse(socket, id, result, success) local total, chunks = chunkString(result) for i, chunk in pairs(chunks) do - sendJson(socket, { id = id, result = chunk, chunk = i, total = total, success = success }) + sendJson(socket, { type = "reply", id = id, result = chunk, chunk = i, total = total, success = success }) end end diff --git a/MinecraftDiscordBot/Models/Message.cs b/MinecraftDiscordBot/Models/Message.cs index 241802f..7c09d22 100644 --- a/MinecraftDiscordBot/Models/Message.cs +++ b/MinecraftDiscordBot/Models/Message.cs @@ -1,51 +1,89 @@ using Discord.WebSocket; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace MinecraftDiscordBot.Models; public abstract class Message { + public static Message Deserialize(string strMessage) { + var obj = JObject.Parse(strMessage); + var typeName = GetKey(obj, "type"); + if (!Parsers.TryGetValue(typeName, out var type)) + throw new FormatException($"Unknown message type '{typeName}'!"); + if (obj.ToObject(type) is not Message message) + throw new FormatException($"Message cannot be casted to '{type}'!"); + return message; + } + + private static readonly Dictionary Parsers = GetMessageTypes(); + private static Dictionary GetMessageTypes() { + var types = new Dictionary(); + var messageTypes = + AppDomain.CurrentDomain.GetAssemblies().SelectMany(domainAssembly => domainAssembly.GetTypes()) + .Where(typeof(Message).IsAssignableFrom); + foreach (var type in messageTypes) + if (GetTypeAttribute(type) is MessageTypeAttribute attr) + types.Add(attr.Name, type); + return types; + } + + private static MessageTypeAttribute? GetTypeAttribute(Type type) + => type.GetCustomAttributes(typeof(MessageTypeAttribute), false).OfType().FirstOrDefault(); + + private static T GetKey(JObject msg, string key) + => (msg.TryGetValue(key, out var type) ? type : throw new FormatException($"Message has no '{key}' param!")) + .ToObject() ?? throw new FormatException($"'{key}' param is not of expected type '{typeof(T).Name}'!"); + [JsonProperty("type")] public abstract string Type { get; } } +[MessageType(TYPE)] public class CapabilityMessage : Message { - public override string Type => "roles"; + private const string TYPE = "roles"; + public override string Type => TYPE; [JsonProperty("role", Required = Required.Always)] public string[] Role { get; set; } = default!; } -public class TextMessage : Message { - public TextMessage(SocketMessage arg) : this(arg.Author.Username, arg.Content) { } - public TextMessage(string author, string content) { - Author = author; - Content = content; - } - public override string Type => "text"; - [JsonProperty("author", Required = Required.Always)] - public string Author { get; set; } - [JsonProperty("message", Required = Required.Always)] - public string Content { get; set; } -} - +[MessageType(TYPE)] public class ReplyMessage : Message { - public ReplyMessage(int answerId, string result) { - AnswerId = answerId; - Result = result; - } + private const string TYPE = "reply"; + public override string Type => TYPE; [JsonProperty("id", Required = Required.Always)] public int AnswerId { get; set; } [JsonProperty("result", Required = Required.Always)] - public string Result { get; set; } + public string Result { get; set; } = default!; [JsonProperty("chunk", Required = Required.DisallowNull)] public int Chunk { get; set; } = 1; [JsonProperty("total", Required = Required.DisallowNull)] public int Total { get; set; } = 1; [JsonProperty("success", Required = Required.DisallowNull)] public ResultState State { get; set; } = ResultState.Successful; - public override string Type => "reply"; } +public abstract class EventMessage : Message { } + +[MessageType(TYPE)] +public class ChatEvent : EventMessage { + private const string TYPE = "chat"; + public override string Type => TYPE; + [JsonProperty("name", Required = Required.Always)] + public string Name { get; set; } = default!; + [JsonProperty("username", Required = Required.Always)] + public string Username { get; set; } = default!; + [JsonProperty("message", Required = Required.Always)] + public string Message { get; set; } = default!; + [JsonProperty("uuid", Required = Required.Always)] + public string UUID { get; set; } = default!; + [JsonProperty("hidden", Required = Required.Always)] + public bool IsHidden { get; set; } +} + +[MessageType(TYPE)] public class RequestMessage : Message { + private const string TYPE = "request"; + public override string Type => TYPE; public RequestMessage(int answerId, string method, Dictionary? parameters = null) { AnswerId = answerId; Method = method; @@ -58,11 +96,4 @@ public class RequestMessage : Message { public string Method { get; set; } [JsonProperty("params")] public Dictionary Parameters { get; } - public override string Type => "request"; } - -public enum ResultState { - Successful, - Unsuccessful, - Fatal -} \ No newline at end of file diff --git a/MinecraftDiscordBot/Models/MessageTypeAttribute.cs b/MinecraftDiscordBot/Models/MessageTypeAttribute.cs new file mode 100644 index 0000000..e674940 --- /dev/null +++ b/MinecraftDiscordBot/Models/MessageTypeAttribute.cs @@ -0,0 +1,7 @@ +namespace MinecraftDiscordBot.Models; + +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class MessageTypeAttribute : Attribute { + public MessageTypeAttribute(string type) => Name = type; + public string Name { get; } +} diff --git a/MinecraftDiscordBot/Models/ResultState.cs b/MinecraftDiscordBot/Models/ResultState.cs new file mode 100644 index 0000000..416d22a --- /dev/null +++ b/MinecraftDiscordBot/Models/ResultState.cs @@ -0,0 +1,7 @@ +namespace MinecraftDiscordBot.Models; + +public enum ResultState { + Successful, + Unsuccessful, + Fatal +} \ No newline at end of file diff --git a/MinecraftDiscordBot/Services/RootCommandService.cs b/MinecraftDiscordBot/Services/RootCommandService.cs index 9d28c45..f1fbc5c 100644 --- a/MinecraftDiscordBot/Services/RootCommandService.cs +++ b/MinecraftDiscordBot/Services/RootCommandService.cs @@ -19,17 +19,23 @@ public class RootCommandService : CommandRouter, ITaskWaitSource { } private void OnMessage(string message) { - if (JsonConvert.DeserializeObject(message) is not ReplyMessage msg) return; - IChunkWaiter? waiter; - lock (_syncRoot) if (!_waits.TryGetValue(msg.AnswerId, out waiter)) { - Program.LogWarningAsync("Socket", $"Invalid wait id '{msg.AnswerId}'!"); - return; - } - waiter.SetResultState(msg.State); - waiter.AddChunk(msg.Chunk, msg.Total, msg.Result); - if (waiter.Finished || waiter.IsCancellationRequested) - lock (_syncRoot) - _waits.Remove(waiter.ID); + switch (Message.Deserialize(message)) { + case ReplyMessage msg: + IChunkWaiter? waiter; + lock (_syncRoot) if (!_waits.TryGetValue(msg.AnswerId, out waiter)) { + Program.LogWarningAsync("Socket", $"Invalid wait id '{msg.AnswerId}'!"); + return; + } + waiter.SetResultState(msg.State); + waiter.AddChunk(msg.Chunk, msg.Total, msg.Result); + if (waiter.Finished || waiter.IsCancellationRequested) + lock (_syncRoot) + _waits.Remove(waiter.ID); + break; + default: + Program.LogInfo(Program.WebSocketSource, $"Received unhandled message: {message}!"); + break; + } } public Task Send(string message) => _socket.Send(message);