server:
Fixed cli help texts Added administrator options for critical methods Added result state for client and server specific errors Redirect root to help text Fixed fingerprint error, fingerprint must be case sensitive Re-Added online messages Added typing trigger for discord bot messages client: fixed chunkString for empty results preemtive wrap error objects for server messages both: added raw lua RS Bridge command entry
This commit is contained in:
parent
4a98d4cb50
commit
9fd50ee01e
@ -21,8 +21,11 @@ public class BotConfiguration : IBotConfiguration, IBotConfigurator {
|
|||||||
[Option("prefix", Default = DEFAULT_PREFIX, HelpText = "The Discord bot command prefix")]
|
[Option("prefix", Default = DEFAULT_PREFIX, HelpText = "The Discord bot command prefix")]
|
||||||
public string Prefix { get; init; } = DEFAULT_PREFIX;
|
public string Prefix { get; init; } = DEFAULT_PREFIX;
|
||||||
[JsonProperty("host", Required = Required.Always)]
|
[JsonProperty("host", Required = Required.Always)]
|
||||||
[Option("host", Default = DEFAULT_PREFIX, HelpText = "The Discord bot command prefix", Required = true)]
|
[Option("host", Default = DEFAULT_PREFIX, HelpText = "The external websocket hostname.", Required = true)]
|
||||||
public string SocketHost { get; init; } = default!;
|
public string SocketHost { get; init; } = default!;
|
||||||
|
[JsonProperty("admins", Required = Required.DisallowNull)]
|
||||||
|
[Option("admins", Default = new ulong[] { }, HelpText = "The list of bot administrators.")]
|
||||||
|
public ulong[] Administrators { get; init; } = Array.Empty<ulong>();
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public BotConfiguration Config => this;
|
public BotConfiguration Config => this;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using MinecraftDiscordBot.Services;
|
using MinecraftDiscordBot.Models;
|
||||||
|
using MinecraftDiscordBot.Services;
|
||||||
|
|
||||||
namespace MinecraftDiscordBot;
|
namespace MinecraftDiscordBot;
|
||||||
|
|
||||||
@ -17,7 +18,7 @@ public class ChunkWaiter<T> : IChunkWaiter {
|
|||||||
public bool IsCancellationRequested => _ct.IsCancellationRequested;
|
public bool IsCancellationRequested => _ct.IsCancellationRequested;
|
||||||
private string?[]? _chunks = null;
|
private string?[]? _chunks = null;
|
||||||
private int _receivedChunks = 0;
|
private int _receivedChunks = 0;
|
||||||
private bool _success = true;
|
private ResultState? _state = null;
|
||||||
private readonly object _syncRoot = new();
|
private readonly object _syncRoot = new();
|
||||||
public void AddChunk(int chunkId, int totalChunks, string value) {
|
public void AddChunk(int chunkId, int totalChunks, string value) {
|
||||||
lock (_syncRoot) {
|
lock (_syncRoot) {
|
||||||
@ -37,9 +38,15 @@ public class ChunkWaiter<T> : IChunkWaiter {
|
|||||||
}
|
}
|
||||||
private void FinalizeResult(string?[] _chunks) {
|
private void FinalizeResult(string?[] _chunks) {
|
||||||
var resultString = string.Concat(_chunks);
|
var resultString = string.Concat(_chunks);
|
||||||
if (_success) tcs.SetResult(resultParser(resultString));
|
switch (_state) {
|
||||||
else tcs.SetException(new ReplyException(resultString));
|
case ResultState.Successful: tcs.SetResult(resultParser(resultString)); break;
|
||||||
|
case ResultState.Unsuccessful: tcs.SetException(new ReplyException(resultString)); break;
|
||||||
|
case ResultState.Fatal: tcs.SetException(new InvalidProgramException($"Client script failed: {resultString}")); break;
|
||||||
|
default: throw new InvalidProgramException($"Program cannot handle result state '{_state}'!");
|
||||||
|
}
|
||||||
Finished = true;
|
Finished = true;
|
||||||
}
|
}
|
||||||
public void SetUnsuccessful() => _success = false;
|
public void SetResultState(ResultState state) => _state = _state is ResultState oldState && state != oldState
|
||||||
|
? throw new InvalidOperationException("Cannot set two different result states for same message!")
|
||||||
|
: state;
|
||||||
}
|
}
|
||||||
|
@ -7,11 +7,16 @@ local function chunkString(value, chunkSize)
|
|||||||
local length = value:len()
|
local length = value:len()
|
||||||
local total = math.ceil(length / chunkSize)
|
local total = math.ceil(length / chunkSize)
|
||||||
local chunks = {}
|
local chunks = {}
|
||||||
|
if length == 0 then
|
||||||
|
total = 1
|
||||||
|
chunks[1] = ""
|
||||||
|
else
|
||||||
local i = 1
|
local i = 1
|
||||||
for i=1,total do
|
for i=1,total do
|
||||||
local pos = 1 + ((i - 1) * chunkSize)
|
local pos = 1 + ((i - 1) * chunkSize)
|
||||||
chunks[i] = value:sub(pos, pos + chunkSize - 1)
|
chunks[i] = value:sub(pos, pos + chunkSize - 1)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
return total, chunks
|
return total, chunks
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -20,12 +25,7 @@ local function sendJson(socket, message)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function sendResponse(socket, id, result, success)
|
local function sendResponse(socket, id, result, success)
|
||||||
if success == nil then success = true end
|
if success == nil then success = 0 end
|
||||||
|
|
||||||
if not success then
|
|
||||||
sendJson(socket, { id = id, result = result, success = success })
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local total, chunks = chunkString(result)
|
local total, chunks = chunkString(result)
|
||||||
for i, chunk in pairs(chunks) do
|
for i, chunk in pairs(chunks) do
|
||||||
@ -37,10 +37,23 @@ end
|
|||||||
-- return rssystem rs
|
-- return rssystem rs
|
||||||
local function getPeripheral(name)
|
local function getPeripheral(name)
|
||||||
local dev = peripheral.find(name)
|
local dev = peripheral.find(name)
|
||||||
if not dev then error("No peripheral '"..name.."' attached to the computer!") end
|
if not dev then error({message = "No peripheral '"..name.."' attached to the computer!"}) end
|
||||||
return dev
|
return dev
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function runRsCommand(params)
|
||||||
|
local script, reason = loadstring("local rs = peripheral.find(\"rsBridge\") if not rs then error({message = \"RS Bridge is not attached!\"}) end return rs."..params.command)
|
||||||
|
if not script then error({message = "Invalid command: "..reason.."!"}) end
|
||||||
|
local result = table.pack(pcall(script))
|
||||||
|
local success = result[1]
|
||||||
|
if not success then error({message = "Command execution failed: "..result[2].."!"}) end
|
||||||
|
|
||||||
|
local retvals = {}
|
||||||
|
retvals.n = result.n - 1
|
||||||
|
for i=1,retvals.n do retvals[tostring(i)] = result[i + 1] end
|
||||||
|
return textutils.serializeJSON(retvals)
|
||||||
|
end
|
||||||
|
|
||||||
-- error: any error during execution
|
-- error: any error during execution
|
||||||
-- return string result
|
-- return string result
|
||||||
local function getResponse(parsed)
|
local function getResponse(parsed)
|
||||||
@ -55,10 +68,14 @@ local function getResponse(parsed)
|
|||||||
elseif parsed.method == "craft" then
|
elseif parsed.method == "craft" then
|
||||||
return tostring(getPeripheral("rsBridge").craftItem(parsed.params))
|
return tostring(getPeripheral("rsBridge").craftItem(parsed.params))
|
||||||
elseif parsed.method == "getitem" then
|
elseif parsed.method == "getitem" then
|
||||||
return textutils.serializeJSON(getPeripheral("rsBridge").getItem(parsed.params))
|
local item = getPeripheral("rsBridge").getItem(parsed.params)
|
||||||
|
if not item then error({message = "Requested item not found!"}) end
|
||||||
|
return textutils.serializeJSON(item)
|
||||||
|
elseif parsed.method == "command" then
|
||||||
|
return runRsCommand(parsed.params)
|
||||||
end
|
end
|
||||||
|
|
||||||
error("No message handler for method: "..parsed.method.."!")
|
error({message = "No message handler for method: "..parsed.method.."!"})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function logJSON(json, prefix)
|
local function logJSON(json, prefix)
|
||||||
@ -86,7 +103,15 @@ local function handleMessage(socket, message)
|
|||||||
|
|
||||||
if parsed.type == "request" then
|
if parsed.type == "request" then
|
||||||
local success, result = pcall(function() return getResponse(parsed) end)
|
local success, result = pcall(function() return getResponse(parsed) end)
|
||||||
sendResponse(socket, parsed.id, result, success)
|
if not success then
|
||||||
|
if not result.message then
|
||||||
|
sendResponse(socket, parsed.id, result, 2)
|
||||||
|
else
|
||||||
|
sendResponse(socket, parsed.id, result.message, 1)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
sendResponse(socket, parsed.id, result, 0)
|
||||||
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ public abstract class CommandRouter : ICommandHandler<ResponseType> {
|
|||||||
=> Task.FromResult(ResponseType.AsString(GenerateHelp()));
|
=> Task.FromResult(ResponseType.AsString(GenerateHelp()));
|
||||||
private static CommandHandlerAttribute? GetHandlerAttribute(MethodInfo method)
|
private static CommandHandlerAttribute? GetHandlerAttribute(MethodInfo method)
|
||||||
=> method.GetCustomAttributes(typeof(CommandHandlerAttribute), true).OfType<CommandHandlerAttribute>().FirstOrDefault();
|
=> method.GetCustomAttributes(typeof(CommandHandlerAttribute), true).OfType<CommandHandlerAttribute>().FirstOrDefault();
|
||||||
public abstract Task<ResponseType> RootAnswer(SocketUserMessage message, CancellationToken ct);
|
public virtual Task<ResponseType> RootAnswer(SocketUserMessage message, CancellationToken ct) => GetHelpText(message, Array.Empty<string>(), ct);
|
||||||
public abstract Task<ResponseType> FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct);
|
public abstract Task<ResponseType> FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct);
|
||||||
public Task<ResponseType> HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct)
|
public Task<ResponseType> HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct)
|
||||||
=> parameters is { Length: 0 }
|
=> parameters is { Length: 0 }
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
namespace MinecraftDiscordBot;
|
using MinecraftDiscordBot.Models;
|
||||||
|
|
||||||
|
namespace MinecraftDiscordBot;
|
||||||
|
|
||||||
public interface IChunkWaiter {
|
public interface IChunkWaiter {
|
||||||
bool Finished { get; }
|
bool Finished { get; }
|
||||||
int ID { get; }
|
int ID { get; }
|
||||||
bool IsCancellationRequested { get; }
|
bool IsCancellationRequested { get; }
|
||||||
void AddChunk(int chunkId, int totalChunks, string value);
|
void AddChunk(int chunkId, int totalChunks, string value);
|
||||||
void SetUnsuccessful();
|
void SetResultState(ResultState state);
|
||||||
}
|
}
|
||||||
|
13
MinecraftDiscordBot/IUserRoleManager.cs
Normal file
13
MinecraftDiscordBot/IUserRoleManager.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Discord.WebSocket;
|
||||||
|
|
||||||
|
namespace MinecraftDiscordBot;
|
||||||
|
|
||||||
|
public interface IUserRoleManager {
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a user is a bot administrator.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">User ID.</param>
|
||||||
|
/// <param name="message">An optional message to throw when user is not authorized.</param>
|
||||||
|
/// <exception cref="ReplyException">User is not authorized.</exception>
|
||||||
|
void RequireAdministrator(ulong user, string? message = null);
|
||||||
|
}
|
@ -6,7 +6,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<Version>1.1.0</Version>
|
<Version>1.1.1</Version>
|
||||||
<Authors>Michael Chen</Authors>
|
<Authors>Michael Chen</Authors>
|
||||||
<Company>$(Authors)</Company>
|
<Company>$(Authors)</Company>
|
||||||
<RepositoryUrl>https://gitlab.com/chenmichael/mcdiscordbot</RepositoryUrl>
|
<RepositoryUrl>https://gitlab.com/chenmichael/mcdiscordbot</RepositoryUrl>
|
||||||
|
21
MinecraftDiscordBot/Models/LuaPackedArray.cs
Normal file
21
MinecraftDiscordBot/Models/LuaPackedArray.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace MinecraftDiscordBot.Models;
|
||||||
|
|
||||||
|
public class LuaPackedArray {
|
||||||
|
public ref object? this[int i] => ref _items[i];
|
||||||
|
private readonly object?[] _items;
|
||||||
|
public LuaPackedArray(IDictionary<string, object> packedTable) {
|
||||||
|
if (packedTable["n"] is not long n) throw new ArgumentException("No length in packed array!");
|
||||||
|
_items = new object?[n];
|
||||||
|
for (var i = 0; i < _items.Length; i++)
|
||||||
|
_items[i] = packedTable.TryGetValue((i + 1).ToString(), out var val) ? val : null;
|
||||||
|
}
|
||||||
|
public static LuaPackedArray Deserialize(string value) {
|
||||||
|
var dict = JsonConvert.DeserializeObject<Dictionary<string, object>>(value);
|
||||||
|
return new LuaPackedArray(dict ?? throw new Exception("Not a packed table (empty object)!"));
|
||||||
|
}
|
||||||
|
public override string ToString() => _items is { Length: 0 }
|
||||||
|
? "Empty Array"
|
||||||
|
: string.Join(", ", _items.Select(i => i is null ? "nil" : i.ToString()));
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace MinecraftDiscordBot.Models;
|
namespace MinecraftDiscordBot.Models;
|
||||||
|
|
||||||
@ -19,7 +20,7 @@ public class Md5Hash : IEquatable<Md5Hash?> {
|
|||||||
hashCode.AddBytes(_hash);
|
hashCode.AddBytes(_hash);
|
||||||
return hashCode.ToHashCode();
|
return hashCode.ToHashCode();
|
||||||
}
|
}
|
||||||
public override string ToString() => Convert.ToHexString(_hash);
|
public override string ToString() => Convert.ToHexString(_hash).ToLower();
|
||||||
|
|
||||||
public class Md5JsonConverter : JsonConverter<Md5Hash> {
|
public class Md5JsonConverter : JsonConverter<Md5Hash> {
|
||||||
public override Md5Hash? ReadJson(JsonReader reader, Type objectType, Md5Hash? existingValue, bool hasExistingValue, JsonSerializer serializer)
|
public override Md5Hash? ReadJson(JsonReader reader, Type objectType, Md5Hash? existingValue, bool hasExistingValue, JsonSerializer serializer)
|
||||||
@ -31,4 +32,14 @@ public class Md5Hash : IEquatable<Md5Hash?> {
|
|||||||
else writer.WriteValue(value.ToString());
|
else writer.WriteValue(value.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryParse(string itemid, [NotNullWhen(true)] out Md5Hash? fingerprint) {
|
||||||
|
try {
|
||||||
|
fingerprint = new Md5Hash(itemid);
|
||||||
|
return true;
|
||||||
|
} catch (Exception) {
|
||||||
|
fingerprint = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -40,11 +40,8 @@ public class ReplyMessage : Message {
|
|||||||
public int Chunk { get; set; } = 1;
|
public int Chunk { get; set; } = 1;
|
||||||
[JsonProperty("total", Required = Required.DisallowNull)]
|
[JsonProperty("total", Required = Required.DisallowNull)]
|
||||||
public int Total { get; set; } = 1;
|
public int Total { get; set; } = 1;
|
||||||
/// <summary>
|
|
||||||
/// If at least one packet was received where
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("success", Required = Required.DisallowNull)]
|
[JsonProperty("success", Required = Required.DisallowNull)]
|
||||||
public bool Success { get; set; } = true;
|
public ResultState State { get; set; } = ResultState.Successful;
|
||||||
public override string Type => "reply";
|
public override string Type => "reply";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,3 +60,9 @@ public class RequestMessage : Message {
|
|||||||
public Dictionary<string, object> Parameters { get; }
|
public Dictionary<string, object> Parameters { get; }
|
||||||
public override string Type => "request";
|
public override string Type => "request";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum ResultState {
|
||||||
|
Successful,
|
||||||
|
Unsuccessful,
|
||||||
|
Fatal
|
||||||
|
}
|
@ -12,7 +12,7 @@ using System.Runtime.CompilerServices;
|
|||||||
|
|
||||||
namespace MinecraftDiscordBot;
|
namespace MinecraftDiscordBot;
|
||||||
|
|
||||||
public class Program : IDisposable, ICommandHandler<ResponseType> {
|
public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleManager {
|
||||||
public const string WebSocketSource = "WebSocket";
|
public const string WebSocketSource = "WebSocket";
|
||||||
public const string BotSource = "Bot";
|
public const string BotSource = "Bot";
|
||||||
private static readonly object LogLock = new();
|
private static readonly object LogLock = new();
|
||||||
@ -29,7 +29,7 @@ public class Program : IDisposable, ICommandHandler<ResponseType> {
|
|||||||
public ITextChannel[] _channels = Array.Empty<ITextChannel>();
|
public ITextChannel[] _channels = Array.Empty<ITextChannel>();
|
||||||
private RootCommandService? _rsSystem = null;
|
private RootCommandService? _rsSystem = null;
|
||||||
private bool disposedValue;
|
private bool disposedValue;
|
||||||
public static bool OnlineNotifications => false;
|
public static bool OnlineNotifications => true;
|
||||||
private const string ClientScriptName = "MinecraftDiscordBot.ClientScript.lua";
|
private const string ClientScriptName = "MinecraftDiscordBot.ClientScript.lua";
|
||||||
public readonly string ClientScript;
|
public readonly string ClientScript;
|
||||||
private readonly ITokenProvider _tokenProvider = new TimeoutTokenProvider(InstanceId, 10);
|
private readonly ITokenProvider _tokenProvider = new TimeoutTokenProvider(InstanceId, 10);
|
||||||
@ -38,7 +38,7 @@ public class Program : IDisposable, ICommandHandler<ResponseType> {
|
|||||||
private string GetVerifiedClientScript() => ClientScript
|
private string GetVerifiedClientScript() => ClientScript
|
||||||
.Replace("$TOKEN", _tokenProvider.GenerateToken());
|
.Replace("$TOKEN", _tokenProvider.GenerateToken());
|
||||||
|
|
||||||
private string GetClientScript(BotConfiguration config) {
|
private static string GetClientScript(BotConfiguration config) {
|
||||||
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ClientScriptName);
|
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(ClientScriptName);
|
||||||
if (stream is null) throw new FileNotFoundException("Client script could not be loaded!");
|
if (stream is null) throw new FileNotFoundException("Client script could not be loaded!");
|
||||||
using var sr = new StreamReader(stream);
|
using var sr = new StreamReader(stream);
|
||||||
@ -52,8 +52,8 @@ public class Program : IDisposable, ICommandHandler<ResponseType> {
|
|||||||
_rsSystem = value;
|
_rsSystem = value;
|
||||||
if (OnlineNotifications)
|
if (OnlineNotifications)
|
||||||
_ = Task.Run(() => Broadcast(i => i.SendMessageAsync(value is null
|
_ = Task.Run(() => Broadcast(i => i.SendMessageAsync(value is null
|
||||||
? $"The Refined Storage went offline. Please check the server!"
|
? $"The Minecraft client has gone offline!"
|
||||||
: $"The Refined Storage is back online!")));
|
: $"The Minecraft client is now online!")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,6 +61,7 @@ public class Program : IDisposable, ICommandHandler<ResponseType> {
|
|||||||
private async Task Broadcast(Func<ITextChannel, Task<IUserMessage>> message) => _ = await Task.WhenAll(_channels.Select(message));
|
private async Task Broadcast(Func<ITextChannel, Task<IUserMessage>> message) => _ = await Task.WhenAll(_channels.Select(message));
|
||||||
public Program(BotConfiguration config) {
|
public Program(BotConfiguration config) {
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_administrators = config.Administrators.ToHashSet();
|
||||||
ClientScript = GetClientScript(config);
|
ClientScript = GetClientScript(config);
|
||||||
_client.Log += LogAsync;
|
_client.Log += LogAsync;
|
||||||
_client.MessageReceived += (msg) => DiscordMessageReceived(msg);
|
_client.MessageReceived += (msg) => DiscordMessageReceived(msg);
|
||||||
@ -151,7 +152,7 @@ public class Program : IDisposable, ICommandHandler<ResponseType> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await LogInfoAsync(WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client logged in with valid script!");
|
await LogInfoAsync(WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client logged in with valid script!");
|
||||||
AddComputerSocket(socket, new(socket));
|
AddComputerSocket(socket, new(socket, this));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task DisruptClientConnection(IWebSocketConnection socket, string reason) {
|
private static async Task DisruptClientConnection(IWebSocketConnection socket, string reason) {
|
||||||
@ -182,10 +183,12 @@ public class Program : IDisposable, ICommandHandler<ResponseType> {
|
|||||||
if (arg is not SocketUserMessage message) return;
|
if (arg is not SocketUserMessage message) return;
|
||||||
if (message.Author.IsBot) return;
|
if (message.Author.IsBot) return;
|
||||||
if (!IsChannelWhitelisted(arg.Channel)) return;
|
if (!IsChannelWhitelisted(arg.Channel)) return;
|
||||||
|
if (arg.Type is not MessageType.Default) return;
|
||||||
|
|
||||||
var cts = new CancellationTokenSource(timeout);
|
var cts = new CancellationTokenSource(timeout);
|
||||||
|
|
||||||
if (IsCommand(message, out var argPos)) {
|
if (IsCommand(message, out var argPos)) {
|
||||||
|
await arg.Channel.TriggerTypingAsync();
|
||||||
var parameters = message.Content[argPos..].Split(WhiteSpace, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
var parameters = message.Content[argPos..].Split(WhiteSpace, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
_ = Task.Run(async () => {
|
_ = Task.Run(async () => {
|
||||||
var response = await HandleCommand(message, parameters, cts.Token);
|
var response = await HandleCommand(message, parameters, cts.Token);
|
||||||
@ -205,6 +208,7 @@ public class Program : IDisposable, ICommandHandler<ResponseType> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ulong, ResponseType.IChoiceResponse> _choiceWait = new();
|
private readonly ConcurrentDictionary<ulong, ResponseType.IChoiceResponse> _choiceWait = new();
|
||||||
|
private readonly HashSet<ulong> _administrators;
|
||||||
|
|
||||||
private async Task DiscordReactionAdded(Cacheable<IUserMessage, ulong> message, Cacheable<IMessageChannel, ulong> channel, SocketReaction reaction) {
|
private async Task DiscordReactionAdded(Cacheable<IUserMessage, ulong> message, Cacheable<IMessageChannel, ulong> channel, SocketReaction reaction) {
|
||||||
var msgObject = await message.GetOrDownloadAsync();
|
var msgObject = await message.GetOrDownloadAsync();
|
||||||
@ -293,6 +297,11 @@ public class Program : IDisposable, ICommandHandler<ResponseType> {
|
|||||||
Dispose(disposing: true);
|
Dispose(disposing: true);
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RequireAdministrator(ulong user, string? message = null) {
|
||||||
|
if (!_administrators.Contains(user))
|
||||||
|
throw new ReplyException(message ?? "User is not authorized to access this command!");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class ResponseType {
|
public abstract class ResponseType {
|
||||||
|
@ -2,17 +2,21 @@
|
|||||||
using MinecraftDiscordBot.Commands;
|
using MinecraftDiscordBot.Commands;
|
||||||
using MinecraftDiscordBot.Models;
|
using MinecraftDiscordBot.Models;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace MinecraftDiscordBot.Services;
|
namespace MinecraftDiscordBot.Services;
|
||||||
|
|
||||||
public class RefinedStorageService : CommandRouter {
|
public class RefinedStorageService : CommandRouter {
|
||||||
private readonly ITaskWaitSource _taskSource;
|
private readonly ITaskWaitSource _taskSource;
|
||||||
|
private readonly IUserRoleManager _roleManager;
|
||||||
public override string HelpTextPrefix => "!rs ";
|
public override string HelpTextPrefix => "!rs ";
|
||||||
public RefinedStorageService(ITaskWaitSource taskSource) : base() => _taskSource = taskSource;
|
public RefinedStorageService(ITaskWaitSource taskSource, IUserRoleManager roleManager) : base() {
|
||||||
|
_taskSource = taskSource;
|
||||||
|
_roleManager = roleManager;
|
||||||
|
}
|
||||||
|
|
||||||
public override Task<ResponseType> FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct)
|
public override Task<ResponseType> FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct)
|
||||||
=> throw new ReplyException($"The RS system has no command '{method}'!");
|
=> throw new ReplyException($"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) {
|
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);
|
var waiter = _taskSource.GetWaiter(parser, ct);
|
||||||
@ -27,6 +31,7 @@ public class RefinedStorageService : CommandRouter {
|
|||||||
private const string CmdListFluids = "listfluids";
|
private const string CmdListFluids = "listfluids";
|
||||||
private const string CmdCraftItem = "craft";
|
private const string CmdCraftItem = "craft";
|
||||||
private const string CmdGetItem = "getitem";
|
private const string CmdGetItem = "getitem";
|
||||||
|
private const string CmdCommand = "command";
|
||||||
|
|
||||||
public async Task<int> GetEnergyUsageAsync(CancellationToken ct) => await Method(CmdEnergyUsage, int.Parse, ct);
|
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<int> GetEnergyStorageAsync(CancellationToken ct) => await Method(CmdEnergyStorage, int.Parse, ct);
|
||||||
@ -35,10 +40,16 @@ public class RefinedStorageService : CommandRouter {
|
|||||||
public async Task<Item> GetItemData(string itemid, CancellationToken ct) => await Method(CmdGetItem, RootCommandService.Deserialize<Item>(), ct, new() {
|
public async Task<Item> GetItemData(string itemid, CancellationToken ct) => await Method(CmdGetItem, RootCommandService.Deserialize<Item>(), ct, new() {
|
||||||
["name"] = itemid
|
["name"] = itemid
|
||||||
});
|
});
|
||||||
|
public async Task<Item> GetItemData(Md5Hash fingerprint, CancellationToken ct) => await Method(CmdGetItem, RootCommandService.Deserialize<Item>(), ct, new() {
|
||||||
|
["fingerprint"] = fingerprint.ToString()
|
||||||
|
});
|
||||||
public async Task<bool> CraftItem(string itemid, int amount, CancellationToken ct) => await Method(CmdCraftItem, RootCommandService.Deserialize<bool>(), ct, new() {
|
public async Task<bool> CraftItem(string itemid, int amount, CancellationToken ct) => await Method(CmdCraftItem, RootCommandService.Deserialize<bool>(), ct, new() {
|
||||||
["name"] = itemid,
|
["name"] = itemid,
|
||||||
["count"] = amount
|
["count"] = amount
|
||||||
});
|
});
|
||||||
|
public async Task<LuaPackedArray> RawCommand(string command, CancellationToken ct) => await Method(CmdCommand, LuaPackedArray.Deserialize, ct, new() {
|
||||||
|
["command"] = command
|
||||||
|
});
|
||||||
|
|
||||||
private Task<IEnumerable<Item>> FilterItems(SocketUserMessage message, IEnumerable<string> filters, CancellationToken ct)
|
private Task<IEnumerable<Item>> FilterItems(SocketUserMessage message, IEnumerable<string> filters, CancellationToken ct)
|
||||||
=> FilterItems(message, filters.Select(ItemFilter.Parse), ct);
|
=> FilterItems(message, filters.Select(ItemFilter.Parse), ct);
|
||||||
@ -89,19 +100,12 @@ public class RefinedStorageService : CommandRouter {
|
|||||||
}
|
}
|
||||||
[CommandHandler(CmdGetItem, HelpText = "Get information about a specific item.")]
|
[CommandHandler(CmdGetItem, HelpText = "Get information about a specific item.")]
|
||||||
public async Task<ResponseType> HandleGetItemData(SocketUserMessage message, string[] parameters, CancellationToken ct) {
|
public async Task<ResponseType> HandleGetItemData(SocketUserMessage message, string[] parameters, CancellationToken ct) {
|
||||||
var amount = 1;
|
|
||||||
string itemid;
|
string itemid;
|
||||||
if (parameters.Length is 1 or 2) {
|
if (parameters.Length is not 1) throw new ReplyException($"I only want one name or fingerprint to search for, you gave me {parameters.Length} arguments!");
|
||||||
itemid = parameters[0];
|
itemid = parameters[0];
|
||||||
if (parameters.Length is 2)
|
var item = await (Md5Hash.TryParse(itemid, out var fingerprint)
|
||||||
if (int.TryParse(parameters[1], out var value)) amount = value;
|
? GetItemData(fingerprint, ct)
|
||||||
else return ResponseType.AsString($"I expected an amount to craft, not '{parameters[1]}'!");
|
: GetItemData(itemid, ct));
|
||||||
} 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 item = await GetItemData(itemid, ct);
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.Append($"We currently have {item.Amount:n0} {item.CleanDisplayName}!");
|
sb.Append($"We currently have {item.Amount:n0} {item.CleanDisplayName}!");
|
||||||
if (item.Tags is not null and var tags) {
|
if (item.Tags is not null and var tags) {
|
||||||
@ -111,6 +115,7 @@ public class RefinedStorageService : CommandRouter {
|
|||||||
sb.Append($"\nRefer to this item with fingerprint {item.Fingerprint}");
|
sb.Append($"\nRefer to this item with fingerprint {item.Fingerprint}");
|
||||||
return ResponseType.AsString(sb.ToString());
|
return ResponseType.AsString(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[CommandHandler(CmdItemName, HelpText = "Filter items by name.")]
|
[CommandHandler(CmdItemName, HelpText = "Filter items by name.")]
|
||||||
public async Task<ResponseType> HandleItemName(SocketUserMessage message, string[] parameters, CancellationToken ct) {
|
public async Task<ResponseType> HandleItemName(SocketUserMessage message, string[] parameters, CancellationToken ct) {
|
||||||
if (parameters.Length < 2) return ResponseType.AsString($"Usage: {CmdItemName} filters...");
|
if (parameters.Length < 2) return ResponseType.AsString($"Usage: {CmdItemName} filters...");
|
||||||
@ -134,6 +139,14 @@ public class RefinedStorageService : CommandRouter {
|
|||||||
return ResponseType.AsString(sb.ToString());
|
return ResponseType.AsString(sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[CommandHandler(CmdCommand, HelpText = "Runs a raw command on the RS system.")]
|
||||||
|
public async Task<ResponseType> HandleRawCommand(SocketUserMessage message, string[] parameters, CancellationToken ct) {
|
||||||
|
_roleManager.RequireAdministrator(message.Author.Id, "You are not authorized to run raw commands on this instance!");
|
||||||
|
var command = string.Join(' ', parameters);
|
||||||
|
var response = await RawCommand(command, ct);
|
||||||
|
return ResponseType.AsString(response.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
[CommandHandler(CmdListItems, HelpText = "Gets a list of items that are currently stored in the RS system.")]
|
[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) {
|
public async Task<ResponseType> HandleItemListing(SocketUserMessage message, string[] parameters, CancellationToken ct) {
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
@ -12,10 +12,10 @@ public delegate Task HandleCommandDelegate(SocketUserMessage message, string[] p
|
|||||||
public class RootCommandService : CommandRouter, ITaskWaitSource {
|
public class RootCommandService : CommandRouter, ITaskWaitSource {
|
||||||
protected readonly IWebSocketConnection _socket;
|
protected readonly IWebSocketConnection _socket;
|
||||||
public override string HelpTextPrefix => "!";
|
public override string HelpTextPrefix => "!";
|
||||||
public RootCommandService(IWebSocketConnection socket) : base() {
|
public RootCommandService(IWebSocketConnection socket, IUserRoleManager roleManager) : base() {
|
||||||
socket.OnMessage = OnMessage;
|
socket.OnMessage = OnMessage;
|
||||||
_socket = socket;
|
_socket = socket;
|
||||||
_rs = new RefinedStorageService(this);
|
_rs = new RefinedStorageService(this, roleManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMessage(string message) {
|
private void OnMessage(string message) {
|
||||||
@ -25,7 +25,7 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
|
|||||||
Program.LogWarningAsync("Socket", $"Invalid wait id '{msg.AnswerId}'!");
|
Program.LogWarningAsync("Socket", $"Invalid wait id '{msg.AnswerId}'!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!msg.Success) waiter.SetUnsuccessful();
|
waiter.SetResultState(msg.State);
|
||||||
waiter.AddChunk(msg.Chunk, msg.Total, msg.Result);
|
waiter.AddChunk(msg.Chunk, msg.Total, msg.Result);
|
||||||
if (waiter.Finished || waiter.IsCancellationRequested)
|
if (waiter.Finished || waiter.IsCancellationRequested)
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@ -65,8 +65,6 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
|
|||||||
|
|
||||||
public static Func<string, T> Deserialize<T>() => msg
|
public static Func<string, T> Deserialize<T>() => msg
|
||||||
=> JsonConvert.DeserializeObject<T>(msg) ?? throw new InvalidProgramException("Empty response!");
|
=> 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)
|
public override Task<ResponseType> FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct)
|
||||||
=> throw new ReplyException($"What the fuck do you mean by '{method}'?");
|
=> throw new ReplyException($"What the fuck do you mean by '{method}'?");
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user