mcdiscordbot/MinecraftDiscordBot/ConnectedComputer.cs
Michael Chen 0b9cb03bae
Implemented auto updating lua script
Downloads latest script from server if outdated (10 seconds)
Server sends encrypted token to client to keep session new and rejects
..old tokens
This allows updating the script in this repository
2022-01-16 21:31:07 +01:00

296 lines
15 KiB
C#

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<TResponse> HandleCommandDelegate<TResponse>(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<ResponseType> FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct)
=> Task.FromResult(ResponseType.AsString($"The RS system has no command '{method}'!"));
public override Task<ResponseType> RootAnswer(SocketUserMessage message, CancellationToken ct)
=> Task.FromResult(ResponseType.AsString("The RS system is online!"));
private async Task<T> Method<T>(string methodName, Func<string, T> parser, CancellationToken ct, Dictionary<string, object>? parameters = null) {
var waiter = _taskSource.GetWaiter(parser, ct);
await _taskSource.Send(new RequestMessage(waiter.ID, methodName, parameters));
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";
private const string CmdCraftItem = "craft";
public async Task<int> GetEnergyUsageAsync(CancellationToken ct) => await Method(CmdEnergyUsage, int.Parse, ct);
public async Task<int> GetEnergyStorageAsync(CancellationToken ct) => await Method(CmdEnergyStorage, int.Parse, ct);
public async Task<IEnumerable<Item>> ListItemsAsync(CancellationToken ct) => await Method(CmdListItems, ConnectedComputer.Deserialize<IEnumerable<Item>>(), ct);
public async Task<IEnumerable<Fluid>> ListFluidsAsync(CancellationToken ct) => await Method(CmdListFluids, ConnectedComputer.Deserialize<IEnumerable<Fluid>>(), ct);
public async Task<bool> CraftItem(string itemid, int amount, CancellationToken ct) => await Method(CmdCraftItem, ConnectedComputer.Deserialize<bool>(), ct, new() {
["name"] = itemid,
["count"] = amount
});
private Task<IEnumerable<Item>> FilterItems(SocketUserMessage message, IEnumerable<string> filters, CancellationToken ct)
=> FilterItems(message, filters.Select(ItemFilter.Parse), ct);
private async Task<IEnumerable<Item>> FilterItems(SocketUserMessage message, IEnumerable<ItemFilter> 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<List<Item>> RefreshItemList(CancellationToken ct) {
var response = await ListItemsAsync(ct);
lock (_itemLock) {
Items = response.OrderByDescending(i => i.Amount).ToList();
return Items;
}
}
private List<Item>? Items;
private readonly object _itemLock = new();
[CommandHandler(CmdEnergyStorage, HelpText = "Get the amount of energy stored in the RS system.")]
public async Task<ResponseType> 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<ResponseType> HandleEnergyUsage(SocketUserMessage message, string[] parameters, CancellationToken ct)
=> ResponseType.AsString($"Refined Storage system currently uses {await GetEnergyUsageAsync(ct)} RF/t");
[CommandHandler(CmdCraftItem, HelpText = "Craft a specific item given an item ID and optionally an amount.")]
public async Task<ResponseType> HandleCraftItem(SocketUserMessage message, string[] parameters, CancellationToken ct) {
var amount = 1;
string itemid;
if (parameters.Length is 1 or 2) {
itemid = parameters[0];
if (parameters.Length is 2)
if (int.TryParse(parameters[1], out var value)) amount = value;
else return ResponseType.AsString($"I expected an amount to craft, not '{parameters[1]}'!");
} else return parameters.Length is < 1
? ResponseType.AsString("You have to give me at least an item name!")
: parameters.Length is > 2
? ResponseType.AsString("Yo, those are way too many arguments! I want only item name and maybe an amount!")
: throw new InvalidOperationException($"Forgot to match parameter length {parameters.Length}!");
return await CraftItem(itemid, amount, ct)
? ResponseType.AsString($"Alright, I'm starting to craft {amount} {itemid}.")
: ResponseType.AsString($"Nope, that somehow doesn't work!");
}
[CommandHandler(CmdItemName, HelpText = "Filter items by name.")]
public async Task<ResponseType> 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<ResponseType> 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<ResponseType> 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<ReplyMessage>(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<int, IChunkWaiter> _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<T> GetWaiter<T>(Func<string, T> resultParser, CancellationToken ct) {
ChunkWaiter<T> waiter;
lock (_syncRoot) {
waiter = new ChunkWaiter<T>(GetFreeId(), resultParser, ct);
_waits.Add(waiter.ID, waiter);
}
return waiter;
}
private readonly ICommandHandler<ResponseType> _rs;
[CommandHandler("rs", HelpText = "Provides some commands for interacting with the Refined Storage system.")]
public Task<ResponseType> RefinedStorageHandler(SocketUserMessage message, string[] parameters, CancellationToken ct)
=> _rs.HandleCommand(message, parameters, ct);
public static Func<string, T> Deserialize<T>() => msg
=> JsonConvert.DeserializeObject<T>(msg) ?? throw new InvalidProgramException("Empty response!");
public override Task<ResponseType> RootAnswer(SocketUserMessage message, CancellationToken ct)
=> Task.FromResult(ResponseType.AsString("The Minecraft server is connected!"));
public override Task<ResponseType> 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<T> GetWaiter<T>(Func<string, T> 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<ModItemId> {
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<Md5Hash?> {
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<Md5Hash> {
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());
}
}
}