using Discord; using Discord.WebSocket; using Fleck; using Newtonsoft.Json; using System.Diagnostics; using System.Runtime.Serialization; using System.Text; 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 RefinedStorageService : CommandRouter { private readonly ITaskWaitSource _taskSource; public override string HelpTextPrefix => "!rs "; public RefinedStorageService(ITaskWaitSource taskSource) : base() => _taskSource = taskSource; public override Task FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct) => Task.FromResult(ResponseType.AsString($"The RS system has no command '{method}'!")); public override Task RootAnswer(SocketUserMessage message, CancellationToken ct) => Task.FromResult(ResponseType.AsString("The RS system is online!")); private async Task Method(string methodName, Func parser, CancellationToken ct) { var waiter = _taskSource.GetWaiter(parser, ct); await _taskSource.Send(new RequestMessage(waiter.ID, methodName)); return await waiter.Task; } private const string CmdEnergyUsage = "energyusage"; private const string CmdEnergyStorage = "energystorage"; private const string CmdListItems = "listitems"; private const string CmdItemName = "itemname"; private const string CmdListFluids = "listfluids"; public async Task GetEnergyUsageAsync(CancellationToken ct) => await Method(CmdEnergyUsage, int.Parse, ct); public async Task GetEnergyStorageAsync(CancellationToken ct) => await Method(CmdEnergyStorage, int.Parse, ct); public async Task> ListItemsAsync(CancellationToken ct) => await Method(CmdListItems, ConnectedComputer.Deserialize>(), ct); public async Task> ListFluidsAsync(CancellationToken ct) => await Method(CmdListFluids, ConnectedComputer.Deserialize>(), ct); private Task> FilterItems(SocketUserMessage message, IEnumerable filters, CancellationToken ct) => FilterItems(message, filters.Select(ItemFilter.Parse), ct); private async Task> FilterItems(SocketUserMessage message, IEnumerable filters, CancellationToken ct) { var items = Items?.ToList().AsEnumerable(); if (items is null) items = (await RefreshItemList(ct)).ToList(); foreach (var filter in filters) items = items.Where(filter.MatchItem); return items.ToList(); } private async Task> RefreshItemList(CancellationToken ct) { var response = await ListItemsAsync(ct); lock (_itemLock) { Items = response.OrderByDescending(i => i.Amount).ToList(); return Items; } } private List? Items; private readonly object _itemLock = new(); [CommandHandler(CmdEnergyStorage, HelpText = "Get the amount of energy stored in the RS system.")] public async Task HandleEnergyStorage(SocketUserMessage message, string[] parameters, CancellationToken ct) => ResponseType.AsString($"Refined Storage system stores {await GetEnergyStorageAsync(ct)} RF/t"); [CommandHandler(CmdEnergyUsage, HelpText = "Get the amount of energy used by the RS system.")] public async Task HandleEnergyUsage(SocketUserMessage message, string[] parameters, CancellationToken ct) => ResponseType.AsString($"Refined Storage system currently uses {await GetEnergyUsageAsync(ct)} RF/t"); [CommandHandler(CmdItemName, HelpText = "Filter items by name.")] public async Task HandleItemName(SocketUserMessage message, string[] parameters, CancellationToken ct) { if (parameters.Length < 2) return ResponseType.AsString($"Usage: {CmdItemName} filters..."); else { var items = await FilterItems(message, parameters[1..], ct); var sb = new StringBuilder(); sb.AppendLine("Did you mean:"); sb.AppendJoin("\n", items.Select(i => i.ToString())); return ResponseType.AsString(sb.ToString()); } } [CommandHandler(CmdListFluids, HelpText = "Gets a list of fluids that are currently stored in the RS system.")] public async Task HandleFluidListing(SocketUserMessage message, string[] parameters, CancellationToken ct) { var sb = new StringBuilder(); sb.Append("The Refined Storage system stores those fluids:"); var fluids = await ListFluidsAsync(ct); foreach (var fluid in fluids.OrderByDescending(i => i.Amount)) if (fluid.Amount > 10000) sb.AppendFormat("\n{0:n2} B of {1}", fluid.Amount / 1000.0f, fluid.DisplayName); else sb.AppendFormat("\n{0:n0} mB of {1}", fluid.Amount, fluid.DisplayName); return ResponseType.AsString(sb.ToString()); } [CommandHandler(CmdListItems, HelpText = "Gets a list of items that are currently stored in the RS system.")] public async Task HandleItemListing(SocketUserMessage message, string[] parameters, CancellationToken ct) { var sb = new StringBuilder(); sb.Append("The Refined Storage system currently stores these items:"); var items = await RefreshItemList(ct); lock (_itemLock) { int taken = 0; foreach (var item in items) { if (sb.Length > 500) break; sb.AppendFormat("\n{0:n0}x {1}", item.Amount, item.DisplayName); taken++; } if (items.Count > taken) sb.AppendFormat("\nand {0} more items.", items.Skip(taken).Sum(i => i.Amount)); } return ResponseType.AsString(sb.ToString()); } } 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) => Task.FromResult(ResponseType.AsString($"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}"; } [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()); } } }