Smarter message parsing

Future: make return params dynamic, not string
now requires type in message objects
This commit is contained in:
Michael Chen 2022-01-17 16:22:15 +01:00
parent 9fd50ee01e
commit 92aafcde70
No known key found for this signature in database
GPG Key ID: 1CBC7AA5671437BB
5 changed files with 90 additions and 39 deletions

View File

@ -29,7 +29,7 @@ local function sendResponse(socket, id, result, success)
local total, chunks = chunkString(result) local total, chunks = chunkString(result)
for i, chunk in pairs(chunks) do 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
end end

View File

@ -1,51 +1,89 @@
using Discord.WebSocket; using Discord.WebSocket;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MinecraftDiscordBot.Models; namespace MinecraftDiscordBot.Models;
public abstract class Message { public abstract class Message {
public static Message Deserialize(string strMessage) {
var obj = JObject.Parse(strMessage);
var typeName = GetKey<string>(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<string, Type> Parsers = GetMessageTypes();
private static Dictionary<string, Type> GetMessageTypes() {
var types = new Dictionary<string, Type>();
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<MessageTypeAttribute>().FirstOrDefault();
private static T GetKey<T>(JObject msg, string key)
=> (msg.TryGetValue(key, out var type) ? type : throw new FormatException($"Message has no '{key}' param!"))
.ToObject<T>() ?? throw new FormatException($"'{key}' param is not of expected type '{typeof(T).Name}'!");
[JsonProperty("type")] [JsonProperty("type")]
public abstract string Type { get; } public abstract string Type { get; }
} }
[MessageType(TYPE)]
public class CapabilityMessage : Message { public class CapabilityMessage : Message {
public override string Type => "roles"; private const string TYPE = "roles";
public override string Type => TYPE;
[JsonProperty("role", Required = Required.Always)] [JsonProperty("role", Required = Required.Always)]
public string[] Role { get; set; } = default!; public string[] Role { get; set; } = default!;
} }
public class TextMessage : Message { [MessageType(TYPE)]
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; }
}
public class ReplyMessage : Message { public class ReplyMessage : Message {
public ReplyMessage(int answerId, string result) { private const string TYPE = "reply";
AnswerId = answerId; public override string Type => TYPE;
Result = result;
}
[JsonProperty("id", Required = Required.Always)] [JsonProperty("id", Required = Required.Always)]
public int AnswerId { get; set; } public int AnswerId { get; set; }
[JsonProperty("result", Required = Required.Always)] [JsonProperty("result", Required = Required.Always)]
public string Result { get; set; } public string Result { get; set; } = default!;
[JsonProperty("chunk", Required = Required.DisallowNull)] [JsonProperty("chunk", Required = Required.DisallowNull)]
public int Chunk { get; set; } = 1; public int Chunk { get; set; } = 1;
[JsonProperty("total", Required = Required.DisallowNull)] [JsonProperty("total", Required = Required.DisallowNull)]
public int Total { get; set; } = 1; public int Total { get; set; } = 1;
[JsonProperty("success", Required = Required.DisallowNull)] [JsonProperty("success", Required = Required.DisallowNull)]
public ResultState State { get; set; } = ResultState.Successful; 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 { public class RequestMessage : Message {
private const string TYPE = "request";
public override string Type => TYPE;
public RequestMessage(int answerId, string method, Dictionary<string, object>? parameters = null) { public RequestMessage(int answerId, string method, Dictionary<string, object>? parameters = null) {
AnswerId = answerId; AnswerId = answerId;
Method = method; Method = method;
@ -58,11 +96,4 @@ public class RequestMessage : Message {
public string Method { get; set; } public string Method { get; set; }
[JsonProperty("params")] [JsonProperty("params")]
public Dictionary<string, object> Parameters { get; } public Dictionary<string, object> Parameters { get; }
public override string Type => "request";
}
public enum ResultState {
Successful,
Unsuccessful,
Fatal
} }

View File

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

View File

@ -0,0 +1,7 @@
namespace MinecraftDiscordBot.Models;
public enum ResultState {
Successful,
Unsuccessful,
Fatal
}

View File

@ -19,7 +19,8 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
} }
private void OnMessage(string message) { private void OnMessage(string message) {
if (JsonConvert.DeserializeObject<ReplyMessage>(message) is not ReplyMessage msg) return; switch (Message.Deserialize(message)) {
case ReplyMessage msg:
IChunkWaiter? waiter; IChunkWaiter? waiter;
lock (_syncRoot) if (!_waits.TryGetValue(msg.AnswerId, out waiter)) { lock (_syncRoot) if (!_waits.TryGetValue(msg.AnswerId, out waiter)) {
Program.LogWarningAsync("Socket", $"Invalid wait id '{msg.AnswerId}'!"); Program.LogWarningAsync("Socket", $"Invalid wait id '{msg.AnswerId}'!");
@ -30,6 +31,11 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
if (waiter.Finished || waiter.IsCancellationRequested) if (waiter.Finished || waiter.IsCancellationRequested)
lock (_syncRoot) lock (_syncRoot)
_waits.Remove(waiter.ID); _waits.Remove(waiter.ID);
break;
default:
Program.LogInfo(Program.WebSocketSource, $"Received unhandled message: {message}!");
break;
}
} }
public Task Send(string message) => _socket.Send(message); public Task Send(string message) => _socket.Send(message);