diff --git a/MinecraftDiscordBot/ClientScript.lua b/MinecraftDiscordBot/ClientScript.lua index 04dea44..c0e20eb 100644 --- a/MinecraftDiscordBot/ClientScript.lua +++ b/MinecraftDiscordBot/ClientScript.lua @@ -54,6 +54,8 @@ local function getResponse(parsed) return textutils.serializeJSON(getPeripheral("rsBridge").listFluids()) elseif parsed.method == "craft" then return tostring(getPeripheral("rsBridge").craftItem(parsed.params)) + elseif parsed.method == "getitem" then + return textutils.serializeJSON(getPeripheral("rsBridge").getItem(parsed.params)) end error("No message handler for method: "..parsed.method.."!") diff --git a/MinecraftDiscordBot/ConnectedComputer.cs b/MinecraftDiscordBot/ConnectedComputer.cs index 0abe9c4..5022156 100644 --- a/MinecraftDiscordBot/ConnectedComputer.cs +++ b/MinecraftDiscordBot/ConnectedComputer.cs @@ -4,7 +4,6 @@ using Fleck; using Newtonsoft.Json; using System.Diagnostics; using System.Runtime.Serialization; -using System.Text; namespace MinecraftDiscordBot; @@ -18,124 +17,6 @@ public sealed class CommandHandlerAttribute : Attribute { 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, Dictionary? 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 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); - public async Task CraftItem(string itemid, int amount, CancellationToken ct) => await Method(CmdCraftItem, ConnectedComputer.Deserialize(), ct, new() { - ["name"] = itemid, - ["count"] = amount - }); - - 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(CmdCraftItem, HelpText = "Craft a specific item given an item ID and optionally an amount.")] - public async Task 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 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 => "!"; diff --git a/MinecraftDiscordBot/MinecraftDiscordBot.csproj b/MinecraftDiscordBot/MinecraftDiscordBot.csproj index 63bff82..7855006 100644 --- a/MinecraftDiscordBot/MinecraftDiscordBot.csproj +++ b/MinecraftDiscordBot/MinecraftDiscordBot.csproj @@ -25,7 +25,6 @@ - diff --git a/MinecraftDiscordBot/Program.cs b/MinecraftDiscordBot/Program.cs index b84fa77..77adb5b 100644 --- a/MinecraftDiscordBot/Program.cs +++ b/MinecraftDiscordBot/Program.cs @@ -4,8 +4,6 @@ using Discord.Commands; using Discord.Rest; using Discord.WebSocket; using Fleck; -using Newtonsoft.Json; -using OneOf; using System.Collections.Concurrent; using System.Reflection; using System.Runtime.CompilerServices; @@ -31,7 +29,8 @@ public class Program : IDisposable, ICommandHandler { private bool disposedValue; public static bool OnlineNotifications => false; public static readonly string ClientScript = GetClientScript(); - private readonly ITokenProvider _tokenProvider = new TimeoutTokenProvider(10); + private readonly ITokenProvider _tokenProvider = new TimeoutTokenProvider(InstanceId, 10); + private static readonly int InstanceId = new Random().Next(); private string GetVerifiedClientScript() => ClientScript.Replace("$TOKEN", _tokenProvider.GenerateToken()); @@ -86,11 +85,11 @@ public class Program : IDisposable, ICommandHandler { private static Task RunWithConfig(IBotConfigurator arg) => new Program(arg.Config).RunAsync(); public async Task RunAsync() { + StartWebSocketServer(); await _client.LoginAsync(TokenType.Bot, _config.Token); await _client.StartAsync(); if (!await HasValidChannels()) return 1; - StartWebSocketServer(); // Block this task until the program is closed. await Task.Delay(-1); diff --git a/MinecraftDiscordBot/RefinedStorageService.cs b/MinecraftDiscordBot/RefinedStorageService.cs new file mode 100644 index 0000000..5e7292b --- /dev/null +++ b/MinecraftDiscordBot/RefinedStorageService.cs @@ -0,0 +1,143 @@ +using Discord.WebSocket; +using System.Text; + +namespace MinecraftDiscordBot; + +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, Dictionary? 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"; + private const string CmdGetItem = "getitem"; + + 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); + public async Task GetItemData(string itemid, CancellationToken ct) => await Method(CmdGetItem, ConnectedComputer.Deserialize(), ct, new() { + ["name"] = itemid + }); + public async Task CraftItem(string itemid, int amount, CancellationToken ct) => await Method(CmdCraftItem, ConnectedComputer.Deserialize(), ct, new() { + ["name"] = itemid, + ["count"] = amount + }); + + 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(CmdCraftItem, HelpText = "Craft a specific item given an item ID and optionally an amount.")] + public async Task 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(CmdGetItem, HelpText = "Get information about a specific item.")] + public async Task HandleGetItemData(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}!"); + var data = await GetItemData(itemid, ct); + return ResponseType.AsString(data.ToString()); + } + [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()); + } +} diff --git a/MinecraftDiscordBot/TimeoutTokenProvider.cs b/MinecraftDiscordBot/TimeoutTokenProvider.cs index 03cb7aa..53f1df8 100644 --- a/MinecraftDiscordBot/TimeoutTokenProvider.cs +++ b/MinecraftDiscordBot/TimeoutTokenProvider.cs @@ -1,13 +1,17 @@ namespace MinecraftDiscordBot; public class TimeoutTokenProvider : ITokenProvider { - public TimeoutTokenProvider(int timeoutSeconds, ICipher? cipher = null) { + public TimeoutTokenProvider(int instanceId, int timeoutSeconds, ICipher? cipher = null) { + InstancePrefix = Convert.ToHexString(BitConverter.GetBytes(instanceId)); _timeout = timeoutSeconds; _cipher = cipher ?? new AesCipher(); } private readonly ICipher _cipher; private readonly int _timeout; + public string InstancePrefix { get; } public bool VerifyToken(string token) { + if (!token.StartsWith(InstancePrefix)) return false; + token = token[InstancePrefix.Length..]; byte[] data; try { data = _cipher.Decrypt(Convert.FromHexString(token)); @@ -21,7 +25,7 @@ public class TimeoutTokenProvider : ITokenProvider { public string GenerateToken() { var time = BitConverter.GetBytes(DateTime.UtcNow.ToBinary()); var key = Guid.NewGuid().ToByteArray(); - var token = Convert.ToHexString(_cipher.Encrypt(time.Concat(key).ToArray())); + var token = InstancePrefix + Convert.ToHexString(_cipher.Encrypt(time.Concat(key).ToArray())); return token; } }