using Discord; using Discord.WebSocket; using Fleck; using Newtonsoft.Json; using System.Diagnostics; using System.Runtime.Serialization; namespace MinecraftDiscordBot; public delegate Task HandleCommandDelegate(SocketUserMessage message, string[] parameters, CancellationToken ct); public delegate Task HandleCommandDelegate(SocketUserMessage message, string[] parameters, CancellationToken ct); [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)] public sealed class CommandHandlerAttribute : Attribute { public CommandHandlerAttribute(string commandName) => CommandName = commandName; public string CommandName { get; } public string? HelpText { get; init; } } public class ConnectedComputer : CommandRouter, ITaskWaitSource { protected readonly IWebSocketConnection _socket; public override string HelpTextPrefix => "!"; public ConnectedComputer(IWebSocketConnection socket) : base() { socket.OnMessage = OnMessage; _socket = socket; _rs = new RefinedStorageService(this); } 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; } } if (!msg.Success) waiter.SetUnsuccessful(); waiter.AddChunk(msg.Chunk, msg.Total, msg.Result); if (waiter.Finished || waiter.IsCancellationRequested) lock (_syncRoot) _waits.Remove(waiter.ID); } public Task Send(string message) => _socket.Send(message); public Task Send(Message message) => Send(JsonConvert.SerializeObject(message)); private readonly object _syncRoot = new(); private readonly Dictionary _waits = new(); private readonly Random _rnd = new(); public IWebSocketConnectionInfo ConnectionInfo => _socket.ConnectionInfo; private int GetFreeId() { var attempts = 0; while (true) { var id = _rnd.Next(); if (!_waits.ContainsKey(id)) return id; Program.LogWarningAsync(Program.WebSocketSource, $"Could not get a free ID after {++attempts} attempts!"); } } public ChunkWaiter GetWaiter(Func resultParser, CancellationToken ct) { ChunkWaiter waiter; lock (_syncRoot) { waiter = new ChunkWaiter(GetFreeId(), resultParser, ct); _waits.Add(waiter.ID, waiter); } return waiter; } private readonly ICommandHandler _rs; [CommandHandler("rs", HelpText = "Provides some commands for interacting with the Refined Storage system.")] public Task RefinedStorageHandler(SocketUserMessage message, string[] parameters, CancellationToken ct) => _rs.HandleCommand(message, parameters, ct); public static Func Deserialize() => msg => JsonConvert.DeserializeObject(msg) ?? throw new InvalidProgramException("Empty response!"); public override Task RootAnswer(SocketUserMessage message, CancellationToken ct) => Task.FromResult(ResponseType.AsString("The Minecraft server is connected!")); public override Task FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct) => throw new ReplyException($"What the fuck do you mean by '{method}'?"); } public interface ITaskWaitSource { ChunkWaiter GetWaiter(Func resultParser, CancellationToken ct); Task Send(Message requestMessage); } [Serializable] public class ReplyException : Exception { public ReplyException() { } public ReplyException(string message) : base(message) { } public ReplyException(string message, Exception inner) : base(message, inner) { } protected ReplyException(SerializationInfo info, StreamingContext context) : base(info, context) { } } [JsonObject(MemberSerialization.OptIn, Description = "Describes an item in a Refined Storage system.", MissingMemberHandling = MissingMemberHandling.Ignore)] [DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] public class Item : Fluid { [JsonProperty("fingerprint", Required = Required.Always)] public Md5Hash Fingerprint { get; set; } = default!; [JsonProperty("nbt", Required = Required.DisallowNull)] public dynamic? NBT { get; set; } public override string ToString() => $"{Amount:n0}x {DisplayName}"; } [JsonObject(MemberSerialization.OptIn, Description = "Describes a fluid in a Refined Storage system.", MissingMemberHandling = MissingMemberHandling.Ignore)] [DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] public class Fluid { [JsonProperty("amount", Required = Required.Always)] public int Amount { get; set; } [JsonProperty("displayName", Required = Required.Always)] public string DisplayName { get; set; } = default!; [JsonProperty("tags", Required = Required.DisallowNull)] public string[]? Tags { get; set; } = default; [JsonProperty("name", Required = Required.Always)] public ModItemId ItemId { get; set; } = default!; public override string ToString() => Amount > 10000 ? $"{Amount / 1000.0f:n2} B of {DisplayName}" : $"{Amount:n0} mB of {DisplayName}"; [JsonIgnore] public string CleanDisplayName => DisplayName[1..^2]; } [JsonConverter(typeof(ModItemIdJsonConverter))] [DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] public class ModItemId { public ModItemId(string name) { var colon = name.IndexOf(':'); if (colon < 0) throw new ArgumentException("Invalid mod item id!", nameof(name)); ModName = name[..colon]; ModItem = name[(colon + 1)..]; if (ToString() != name) throw new InvalidProgramException("Bad Parsing!"); } public override string ToString() => $"{ModName}:{ModItem}"; public string ModName { get; } public string ModItem { get; } public class ModItemIdJsonConverter : JsonConverter { public override ModItemId? ReadJson(JsonReader reader, Type objectType, ModItemId? existingValue, bool hasExistingValue, JsonSerializer serializer) => reader.Value is string value ? new(value) : throw new JsonException($"Could not parse mod name with token '{reader.Value}'"); public override void WriteJson(JsonWriter writer, ModItemId? value, JsonSerializer serializer) { if (value is null) writer.WriteNull(); else writer.WriteValue(value.ToString()); } } } [JsonConverter(typeof(Md5JsonConverter))] [DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")] public class Md5Hash : IEquatable { private readonly byte[] _hash; public Md5Hash(string hash) : this(Convert.FromHexString(hash)) { } public Md5Hash(byte[] hash) { if (hash is not { Length: 16 }) throw new ArgumentException("Invalid digest size!", nameof(hash)); _hash = hash; } public override bool Equals(object? obj) => Equals(obj as Md5Hash); public bool Equals(Md5Hash? other) => other != null && _hash.SequenceEqual(other._hash); public override int GetHashCode() { var hashCode = new HashCode(); hashCode.AddBytes(_hash); return hashCode.ToHashCode(); } public override string ToString() => Convert.ToHexString(_hash); public class Md5JsonConverter : JsonConverter { public override Md5Hash? ReadJson(JsonReader reader, Type objectType, Md5Hash? existingValue, bool hasExistingValue, JsonSerializer serializer) => reader.Value is string { Length: 32 } value ? new(value) : throw new JsonException($"Could not parse MD5 hash with token '{reader.Value}'"); public override void WriteJson(JsonWriter writer, Md5Hash? value, JsonSerializer serializer) { if (value is null) writer.WriteNull(); else writer.WriteValue(value.ToString()); } } }