Added command routing
Added router class using attributes Added success parameter to mc computer response Added generic answer type class (for future choice results) Changelog: added
This commit is contained in:
		
							
								
								
									
										30
									
								
								MinecraftDiscordBot/CommandRouter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								MinecraftDiscordBot/CommandRouter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| using Discord; | ||||
| using Discord.WebSocket; | ||||
| using System.Reflection; | ||||
|  | ||||
| namespace MinecraftDiscordBot; | ||||
|  | ||||
| public abstract class CommandRouter<T> : ICommandHandler<T> { | ||||
|     private readonly Dictionary<string, HandleCommandDelegate<T>> _handlers = new(); | ||||
|     public CommandRouter() { | ||||
|         foreach (var method in GetType().GetMethods()) | ||||
|             if (GetHandlerAttribute(method) is CommandHandlerAttribute handler) | ||||
|                 try { | ||||
|                     _handlers.Add(handler.CommandName, method.CreateDelegate<HandleCommandDelegate<T>>(this)); | ||||
|                 } catch (Exception) { | ||||
|                     Program.LogWarning("CommandRouter", $"Could not add delegate for method {handler.CommandName} in function {method.ReturnType} {method.Name}(...)!"); | ||||
|                     throw; | ||||
|                 } | ||||
|     } | ||||
|  | ||||
|     private static CommandHandlerAttribute? GetHandlerAttribute(MethodInfo method) | ||||
|         => method.GetCustomAttributes(typeof(CommandHandlerAttribute), true).OfType<CommandHandlerAttribute>().FirstOrDefault(); | ||||
|     public abstract Task<T> RootAnswer(SocketUserMessage message, CancellationToken ct); | ||||
|     public abstract Task<T> FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct); | ||||
|     public Task<T> HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct) | ||||
|         => parameters is { Length: 0 } | ||||
|             ? RootAnswer(message, ct) | ||||
|             : _handlers.TryGetValue(parameters[0], out var handler) | ||||
|                 ? handler(message, parameters[1..], ct) | ||||
|                 : FallbackHandler(message, parameters[0], parameters[1..], ct); | ||||
| } | ||||
| @@ -3,13 +3,23 @@ using Discord.WebSocket; | ||||
| using Fleck; | ||||
| using Newtonsoft.Json; | ||||
| using System.Diagnostics; | ||||
| using System.Runtime.Serialization; | ||||
| using System.Text; | ||||
|  | ||||
| namespace MinecraftDiscordBot; | ||||
|  | ||||
| public class ConnectedComputer { | ||||
| 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 class ConnectedComputer : CommandRouter<ResponseType> { | ||||
|     protected readonly IWebSocketConnection _socket; | ||||
|     public ConnectedComputer(IWebSocketConnection socket) { | ||||
|     public ConnectedComputer(IWebSocketConnection socket) : base() { | ||||
|         socket.OnMessage = OnMessage; | ||||
|         _socket = socket; | ||||
|     } | ||||
| @@ -23,6 +33,7 @@ public class ConnectedComputer { | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|         if (!msg.Success) waiter.SetUnsuccessful(); | ||||
|         waiter.AddChunk(msg.Chunk, msg.Total, msg.Result); | ||||
|         if (waiter.Finished || waiter.IsCancellationRequested) | ||||
|             lock (_syncRoot) | ||||
| @@ -40,6 +51,7 @@ public class ConnectedComputer { | ||||
|         int ID { get; } | ||||
|         bool IsCancellationRequested { get; } | ||||
|         void AddChunk(int chunkId, int totalChunks, string value); | ||||
|         void SetUnsuccessful(); | ||||
|     } | ||||
|  | ||||
|     protected class ChunkWaiter<T> : IChunkWaiter { | ||||
| @@ -57,6 +69,7 @@ public class ConnectedComputer { | ||||
|         public bool IsCancellationRequested => _ct.IsCancellationRequested; | ||||
|         private string?[]? _chunks = null; | ||||
|         private int _receivedChunks = 0; | ||||
|         private bool _success = true; | ||||
|         private readonly object _syncRoot = new(); | ||||
|         public void AddChunk(int chunkId, int totalChunks, string value) { | ||||
|             lock (_syncRoot) { | ||||
| @@ -75,9 +88,12 @@ public class ConnectedComputer { | ||||
|             if (++_receivedChunks == totalChunks) FinalizeResult(_chunks); | ||||
|         } | ||||
|         private void FinalizeResult(string?[] _chunks) { | ||||
|             tcs.SetResult(resultParser(string.Concat(_chunks))); | ||||
|             var resultString = string.Concat(_chunks); | ||||
|             if (_success) tcs.SetResult(resultParser(resultString)); | ||||
|             else tcs.SetException(new ReplyException(resultString)); | ||||
|             Finished = true; | ||||
|         } | ||||
|         public void SetUnsuccessful() => _success = false; | ||||
|     } | ||||
|  | ||||
|     protected int GetFreeId() { | ||||
| @@ -101,9 +117,7 @@ public class ConnectedComputer { | ||||
|  | ||||
|     protected static Func<string, T> Deserialize<T>() => msg | ||||
|          => JsonConvert.DeserializeObject<T>(msg) ?? throw new InvalidProgramException("Empty response!"); | ||||
| } | ||||
|  | ||||
| public class RefinedStorageComputer : ConnectedComputer { | ||||
|     public const string Role = "rs"; | ||||
|     private const string CmdEnergyUsage = "energyusage"; | ||||
|     private const string CmdEnergyStorage = "energystorage"; | ||||
| @@ -111,7 +125,6 @@ public class RefinedStorageComputer : ConnectedComputer { | ||||
|     private const string CmdItemName = "itemname"; | ||||
|     private const string CmdListFluids = "listfluids"; | ||||
|  | ||||
|     public RefinedStorageComputer(IWebSocketConnection socket) : base(socket) { } | ||||
|     public async Task<int> GetEnergyUsageAsync(CancellationToken ct) { | ||||
|         var waiter = GetWaiter(int.Parse, ct); | ||||
|         await Send(new RequestMessage(waiter.ID, CmdEnergyUsage)); | ||||
| @@ -134,46 +147,21 @@ public class RefinedStorageComputer : ConnectedComputer { | ||||
|         return await waiter.Task; | ||||
|     } | ||||
|  | ||||
|     public async Task HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct) { | ||||
|         if (parameters is not { Length: > 0 }) { | ||||
|             await message.ReplyAsync($"Refined Storage system is online"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             switch (parameters[0].ToLower()) { | ||||
|             case CmdEnergyUsage: | ||||
|                 await message.ReplyAsync($"Refined Storage system currently uses {await GetEnergyUsageAsync(ct)} RF/t"); | ||||
|                 break; | ||||
|             case CmdEnergyStorage: | ||||
|                 await message.ReplyAsync($"Refined Storage system stores {await GetEnergyStorageAsync(ct)} RF/t"); | ||||
|                 break; | ||||
|             case CmdListItems: | ||||
|                 await HandleItemListing(message, ct); | ||||
|                 break; | ||||
|             case CmdItemName: | ||||
|                 await HandleItemName(message, parameters, ct); | ||||
|                 break; | ||||
|             case CmdListFluids: | ||||
|                 await HandleFluidListing(message, ct); | ||||
|                 break; | ||||
|             case string other: | ||||
|                 await message.ReplyAsync($"Refined Storages cannot do '{other}', bruh"); | ||||
|                 break; | ||||
|             } | ||||
|         } catch (TaskCanceledException) { | ||||
|             await message.ReplyAsync("The Refined Storage system request timed out!"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task HandleItemName(SocketUserMessage message, string[] parameters, CancellationToken ct) { | ||||
|         if (parameters.Length < 2) await message.ReplyAsync($"Usage: {CmdItemName} filters..."); | ||||
|     [CommandHandler(CmdEnergyStorage)] | ||||
|     public async Task<ResponseType> HandleEnergyStorage(SocketUserMessage message, string[] parameters, CancellationToken ct) | ||||
|         => ResponseType.AsString($"Refined Storage system stores {await GetEnergyStorageAsync(ct)} RF/t"); | ||||
|     [CommandHandler(CmdEnergyUsage)] | ||||
|     public async Task<ResponseType> HandleEnergyUsage(SocketUserMessage message, string[] parameters, CancellationToken ct) | ||||
|         => ResponseType.AsString($"Refined Storage system currently uses {await GetEnergyUsageAsync(ct)} RF/t"); | ||||
|     [CommandHandler(CmdItemName)] | ||||
|     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())); | ||||
|             await message.ReplyAsync(sb.ToString()); | ||||
|             return ResponseType.AsString(sb.ToString()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -188,51 +176,22 @@ public class RefinedStorageComputer : ConnectedComputer { | ||||
|         return items.ToList(); | ||||
|     } | ||||
|  | ||||
|     public abstract class ItemFilter { | ||||
|         public abstract bool Match(Fluid item); | ||||
|         public virtual bool MatchItem(Item item) => Match(item); | ||||
|  | ||||
|         public static ItemFilter Parse(string filter) | ||||
|             => filter.StartsWith('@') | ||||
|                 ? new ModNameFilter(filter[1..]) | ||||
|                 : filter.StartsWith('$') | ||||
|                 ? new TagFilter(filter[1..]) | ||||
|                 : new ItemNameFilter(filter); | ||||
|  | ||||
|         private class ModNameFilter : ItemFilter { | ||||
|             private readonly string filter; | ||||
|             public ModNameFilter(string filter) => this.filter = filter; | ||||
|             public override bool Match(Fluid item) => item.ItemId.ModName.Contains(filter, StringComparison.InvariantCultureIgnoreCase); | ||||
|         } | ||||
|  | ||||
|         private class TagFilter : ItemFilter { | ||||
|             private readonly string filter; | ||||
|             public TagFilter(string filter) => this.filter = filter; | ||||
|             public override bool Match(Fluid item) | ||||
|                 => item.Tags?.Any(tag => tag.Contains(filter, StringComparison.InvariantCultureIgnoreCase)) ?? false; | ||||
|         } | ||||
|  | ||||
|         private class ItemNameFilter : ItemFilter { | ||||
|             private readonly string filter; | ||||
|             public ItemNameFilter(string filter) => this.filter = filter; | ||||
|             public override bool Match(Fluid item) => item.DisplayName.Contains(filter, StringComparison.InvariantCultureIgnoreCase); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task HandleFluidListing(SocketUserMessage message, CancellationToken ct) { | ||||
|     [CommandHandler(CmdListFluids)] | ||||
|     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); | ||||
|         await message.ReplyAsync(sb.ToString()); | ||||
|         return ResponseType.AsString(sb.ToString()); | ||||
|     } | ||||
|  | ||||
|     private List<Item>? Items; | ||||
|     private readonly object _itemLock = new(); | ||||
|  | ||||
|     private async Task HandleItemListing(SocketUserMessage message, CancellationToken ct) { | ||||
|     [CommandHandler(CmdListItems)] | ||||
|     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); | ||||
| @@ -245,7 +204,7 @@ public class RefinedStorageComputer : ConnectedComputer { | ||||
|             } | ||||
|             if (items.Count > taken) sb.AppendFormat("\nand {0} more items.", items.Skip(taken).Sum(i => i.Amount)); | ||||
|         } | ||||
|         await message.ReplyAsync(sb.ToString()); | ||||
|         return ResponseType.AsString(sb.ToString()); | ||||
|     } | ||||
|  | ||||
|     private async Task<List<Item>> RefreshItemList(CancellationToken ct) { | ||||
| @@ -255,6 +214,19 @@ public class RefinedStorageComputer : ConnectedComputer { | ||||
|             return Items; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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}'?")); | ||||
| } | ||||
|  | ||||
| [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)] | ||||
|   | ||||
							
								
								
									
										11
									
								
								MinecraftDiscordBot/ICommandHandler.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								MinecraftDiscordBot/ICommandHandler.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| using Discord.WebSocket; | ||||
|  | ||||
| namespace MinecraftDiscordBot; | ||||
|  | ||||
| public interface ICommandHandler { | ||||
|     Task HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct); | ||||
| } | ||||
|  | ||||
| public interface ICommandHandler<T> { | ||||
|     Task<T> HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct); | ||||
| } | ||||
							
								
								
									
										32
									
								
								MinecraftDiscordBot/ItemFilter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								MinecraftDiscordBot/ItemFilter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| namespace MinecraftDiscordBot; | ||||
|  | ||||
| public abstract class ItemFilter { | ||||
|     public abstract bool Match(Fluid item); | ||||
|     public virtual bool MatchItem(Item item) => Match(item); | ||||
|  | ||||
|     public static ItemFilter Parse(string filter) | ||||
|         => filter.StartsWith('@') | ||||
|             ? new ModNameFilter(filter[1..]) | ||||
|             : filter.StartsWith('$') | ||||
|             ? new TagFilter(filter[1..]) | ||||
|             : new ItemNameFilter(filter); | ||||
|  | ||||
|     private class ModNameFilter : ItemFilter { | ||||
|         private readonly string filter; | ||||
|         public ModNameFilter(string filter) => this.filter = filter; | ||||
|         public override bool Match(Fluid item) => item.ItemId.ModName.Contains(filter, StringComparison.InvariantCultureIgnoreCase); | ||||
|     } | ||||
|  | ||||
|     private class TagFilter : ItemFilter { | ||||
|         private readonly string filter; | ||||
|         public TagFilter(string filter) => this.filter = filter; | ||||
|         public override bool Match(Fluid item) | ||||
|             => item.Tags?.Any(tag => tag.Contains(filter, StringComparison.InvariantCultureIgnoreCase)) ?? false; | ||||
|     } | ||||
|  | ||||
|     private class ItemNameFilter : ItemFilter { | ||||
|         private readonly string filter; | ||||
|         public ItemNameFilter(string filter) => this.filter = filter; | ||||
|         public override bool Match(Fluid item) => item.DisplayName.Contains(filter, StringComparison.InvariantCultureIgnoreCase); | ||||
|     } | ||||
| } | ||||
| @@ -11,7 +11,7 @@ public abstract class Message { | ||||
| public class CapabilityMessage : Message { | ||||
|     public override string Type => "roles"; | ||||
|     [JsonProperty("role", Required = Required.Always)] | ||||
|     public string Role { get; set; } = default!; | ||||
|     public string[] Role { get; set; } = default!; | ||||
| } | ||||
|  | ||||
| public class TextMessage : Message { | ||||
| @@ -36,10 +36,15 @@ public class ReplyMessage : Message { | ||||
|     public int AnswerId { get; set; } | ||||
|     [JsonProperty("result", Required = Required.Always)] | ||||
|     public string Result { get; set; } | ||||
|     [JsonProperty("chunk", Required = Required.Always)] | ||||
|     public int Chunk { get; set; } | ||||
|     [JsonProperty("total", Required = Required.Always)] | ||||
|     public int Total { get; set; } | ||||
|     [JsonProperty("chunk", Required = Required.DisallowNull)] | ||||
|     public int Chunk { get; set; } = 1; | ||||
|     [JsonProperty("total", Required = Required.DisallowNull)] | ||||
|     public int Total { get; set; } = 1; | ||||
|     /// <summary> | ||||
|     /// If at least one packet was received where  | ||||
|     /// </summary> | ||||
|     [JsonProperty("success", Required = Required.DisallowNull)] | ||||
|     public bool Success { get; set; } = true; | ||||
|     public override string Type => "reply"; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ | ||||
|     <PackageReference Include="Fleck" Version="1.2.0" /> | ||||
|     <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" /> | ||||
|     <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> | ||||
|     <PackageReference Include="OneOf" Version="3.0.205" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
|   | ||||
| @@ -1,17 +1,18 @@ | ||||
| using CommandLine; | ||||
| using CommandLine; | ||||
| using Discord; | ||||
| 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; | ||||
|  | ||||
| namespace MinecraftDiscordBot; | ||||
|  | ||||
| public class Program : IDisposable { | ||||
| public class Program : IDisposable, ICommandHandler<ResponseType> { | ||||
|     public const string WebSocketSource = "WebSocket"; | ||||
|     public const string BotSource = "Bot"; | ||||
|     private static readonly object LogLock = new(); | ||||
| @@ -25,16 +26,18 @@ public class Program : IDisposable { | ||||
|     private readonly ConcurrentDictionary<Guid, ConnectedComputer> _connections = new(); | ||||
|     private static readonly char[] WhiteSpace = new char[] { '\t', '\n', ' ', '\r' }; | ||||
|     public ITextChannel[] _channels = Array.Empty<ITextChannel>(); | ||||
|     private RefinedStorageComputer? _rsSystem = null; | ||||
|     private ConnectedComputer? _rsSystem = null; | ||||
|     private bool disposedValue; | ||||
|     public static bool OnlineNotifications => false; | ||||
|  | ||||
|     public RefinedStorageComputer? RsSystem { | ||||
|     public ConnectedComputer? Computer { | ||||
|         get => _rsSystem; set { | ||||
|             if (_rsSystem != value) { | ||||
|                 _rsSystem = value; | ||||
|                 _ = Task.Run(() => Broadcast(i => i.SendMessageAsync(value is null | ||||
|                     ? $"The Refined Storage went offline. Please check the server!" | ||||
|                     : $"The Refined Storage is back online!"))); | ||||
|                 if (OnlineNotifications) | ||||
|                     _ = Task.Run(() => Broadcast(i => i.SendMessageAsync(value is null | ||||
|                           ? $"The Refined Storage went offline. Please check the server!" | ||||
|                           : $"The Refined Storage is back online!"))); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -115,27 +118,13 @@ public class Program : IDisposable { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private async Task SocketReceived(IWebSocketConnection socket, string message) { | ||||
|         if (JsonConvert.DeserializeObject<CapabilityMessage>(message) is not CapabilityMessage capability) return; | ||||
|     private static async Task SocketReceived(IWebSocketConnection socket, string message) | ||||
|         => await LogInfoAsync(WebSocketSource, $"[{socket.ConnectionInfo.Id}] Received: {message}"); | ||||
|  | ||||
|         try { | ||||
|             var pc = capability.Role switch { | ||||
|                 RefinedStorageComputer.Role => new RefinedStorageComputer(socket), | ||||
|                 string role => throw new ArgumentException($"Invalid role '{role}'!") | ||||
|             }; | ||||
|             AddComputerSocket(socket, pc); | ||||
|             await LogInfoAsync(WebSocketSource, $"[{socket.ConnectionInfo.Id}] Presented capability as {pc.GetType().Name}"); | ||||
|         } catch (ArgumentException e) { | ||||
|             await LogErrorAsync(WebSocketSource, e); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void AddComputerSocket(IWebSocketConnection socket, ConnectedComputer pc) { | ||||
|         if (pc is RefinedStorageComputer rs) RsSystem = rs; | ||||
|     } | ||||
|     private void AddComputerSocket(IWebSocketConnection socket, ConnectedComputer pc) => Computer = pc; | ||||
|  | ||||
|     private void RemoveComputerSocket(IWebSocketConnection socket) { | ||||
|         if (RsSystem?.ConnectionInfo.Id == socket.ConnectionInfo.Id) RsSystem = null; | ||||
|         if (Computer is { ConnectionInfo.Id: Guid id } && id == socket.ConnectionInfo.Id) Computer = null; | ||||
|     } | ||||
|  | ||||
|     private async Task SocketClosed(IWebSocketConnection socket) { | ||||
| @@ -143,8 +132,10 @@ public class Program : IDisposable { | ||||
|         await LogInfoAsync(WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client disconnected!"); | ||||
|     } | ||||
|  | ||||
|     private static async Task SocketOpened(IWebSocketConnection socket) | ||||
|         => await LogInfoAsync(WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client connected from {socket.ConnectionInfo.ClientIpAddress}:{socket.ConnectionInfo.ClientPort}!"); | ||||
|     private async Task SocketOpened(IWebSocketConnection socket) { | ||||
|         AddComputerSocket(socket, new(socket)); | ||||
|         await LogInfoAsync(WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client connected from {socket.ConnectionInfo.ClientIpAddress}:{socket.ConnectionInfo.ClientPort}!"); | ||||
|     } | ||||
|  | ||||
|     private async Task DiscordMessageReceived(SocketMessage arg, int timeout = 10000) { | ||||
|         if (arg is not SocketUserMessage message) return; | ||||
| @@ -155,7 +146,10 @@ public class Program : IDisposable { | ||||
|  | ||||
|         if (IsCommand(message, out var argPos)) { | ||||
|             var parameters = message.Content[argPos..].Split(WhiteSpace, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); | ||||
|             _ = Task.Run(() => HandleCommand(message, parameters, cts.Token)); | ||||
|             _ = Task.Run(async () => { | ||||
|                 var response = await HandleCommand(message, parameters, cts.Token); | ||||
|                 await SendResponse(message, response); | ||||
|             }); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -163,18 +157,34 @@ public class Program : IDisposable { | ||||
|         // TODO: Relay Message to Chat Receiver    | ||||
|     } | ||||
|  | ||||
|     private Task HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct) | ||||
|         => parameters is { Length: > 0 } | ||||
|             ? parameters[0].ToLower() switch { | ||||
|                 RefinedStorageComputer.Role => HandleRefinedStorageCommand(message, parameters[1..], ct), | ||||
|                 _ => message.ReplyAsync($"What the fuck do you mean by '{parameters[0]}'?") | ||||
|             } | ||||
|             : message.ReplyAsync($"You really think an empty command works?"); | ||||
|     private static Task SendResponse(SocketUserMessage message, ResponseType response) => response switch { | ||||
|         ResponseType.IChoiceResponse res => HandleChoice(message, res), | ||||
|         ResponseType.StringResponse res => message.ReplyAsync(res.Message), | ||||
|         _ => message.ReplyAsync($"Whoops, someone forgot to implement '{response.GetType()}' responses?"), | ||||
|     }; | ||||
|  | ||||
|     private Task HandleRefinedStorageCommand(SocketUserMessage message, string[] parameters, CancellationToken ct) | ||||
|         => RsSystem is RefinedStorageComputer rs | ||||
|             ? rs.HandleCommand(message, parameters, ct) | ||||
|             : message.ReplyAsync("The Refined Storage system is currently unavailable!"); | ||||
|     private static async Task HandleChoice(SocketUserMessage message, ResponseType.IChoiceResponse res) { | ||||
|         var reply = await message.ReplyAsync($"{res.Query}\n{string.Join("\n", res.Options)}"); | ||||
|         var reactions = new Emoji[] { new("0️⃣"), new("1️⃣"), new("2️⃣"), new("3️⃣"), new("4️⃣"), new("5️⃣"), new("6️⃣"), new("7️⃣"), new("8️⃣"), new("9️⃣") }; | ||||
|         await reply.AddReactionsAsync(reactions); | ||||
|     } | ||||
|  | ||||
|     public async Task<ResponseType> HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct) { | ||||
|         return ResponseType.FromChoice("Select an emoji:", new[] { "One", "Two", "Nine", "420" }, (choice) => message.ReplyAsync($"You chose: {choice}")); | ||||
|         if (Computer is ICommandHandler<ResponseType> handler) | ||||
|             try { | ||||
|                 return await handler.HandleCommand(message, parameters, ct); | ||||
|             } catch (TaskCanceledException) { | ||||
|                 return ResponseType.AsString("Your request could not be processed in time!"); | ||||
|             } catch (ReplyException e) { | ||||
|                 await LogWarningAsync(BotSource, e.Message); | ||||
|                 return ResponseType.AsString($"Your request failed: {e.Message}"); | ||||
|             } catch (Exception e) { | ||||
|                 await LogErrorAsync(BotSource, e); | ||||
|                 return ResponseType.AsString($"Oopsie doopsie, this should not have happened!"); | ||||
|             } | ||||
|         else return ResponseType.AsString("The Minecraft server is currently unavailable!"); | ||||
|     } | ||||
|  | ||||
|     private bool IsCommand(SocketUserMessage message, out int argPos) { | ||||
|         argPos = 0; | ||||
| @@ -227,3 +237,32 @@ public class Program : IDisposable { | ||||
|         GC.SuppressFinalize(this); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public abstract class ResponseType { | ||||
|     private static string DefaultDisplay<T>(T obj) => obj?.ToString() ?? throw new InvalidProgramException("ToString did not yield anything!"); | ||||
|     public static ResponseType AsString(string message) => new StringResponse(message); | ||||
|     public static ResponseType FromChoice<T>(string query, IEnumerable<T> choice, Func<T, Task> resultHandler, Func<T, string>? display = null) => new ChoiceResponse<T>(query, choice, resultHandler, display ?? DefaultDisplay); | ||||
|     public class StringResponse : ResponseType { | ||||
|         public StringResponse(string message) => Message = message; | ||||
|         public string Message { get; } | ||||
|     } | ||||
|     public interface IChoiceResponse { | ||||
|         IEnumerable<string> Options { get; } | ||||
|         string Query { get; } | ||||
|         Task HandleResult(int index); | ||||
|     } | ||||
|     public class ChoiceResponse<T> : ResponseType, IChoiceResponse { | ||||
|         private readonly Func<T, Task> _resultHandler; | ||||
|         private readonly T[] _options; | ||||
|         private readonly Func<T, string> _displayer; | ||||
|         public IEnumerable<string> Options => _options.Select(_displayer); | ||||
|         public string Query { get; } | ||||
|         public Task HandleResult(int index) => _resultHandler(_options[index]); | ||||
|         public ChoiceResponse(string query, IEnumerable<T> choice, Func<T, Task> resultHandler, Func<T, string> display) { | ||||
|             Query = query; | ||||
|             _resultHandler = resultHandler; | ||||
|             _options = choice.ToArray(); | ||||
|             _displayer = display; | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user