Smarter message parsing
Future: make return params dynamic, not string now requires type in message objects
This commit is contained in:
parent
9fd50ee01e
commit
92aafcde70
@ -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
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
7
MinecraftDiscordBot/Models/MessageTypeAttribute.cs
Normal file
7
MinecraftDiscordBot/Models/MessageTypeAttribute.cs
Normal 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; }
|
||||||
|
}
|
7
MinecraftDiscordBot/Models/ResultState.cs
Normal file
7
MinecraftDiscordBot/Models/ResultState.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace MinecraftDiscordBot.Models;
|
||||||
|
|
||||||
|
public enum ResultState {
|
||||||
|
Successful,
|
||||||
|
Unsuccessful,
|
||||||
|
Fatal
|
||||||
|
}
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user