Re-added chat relaying

Fixed async naming scheme
Added webhook for every channel
Added colorful logging
This commit is contained in:
Michael Chen 2022-01-18 10:10:49 +01:00
parent e7b056342f
commit f912b9db8f
No known key found for this signature in database
GPG Key ID: 1CBC7AA5671437BB
6 changed files with 104 additions and 29 deletions

View File

@ -79,6 +79,13 @@ local function getResponse(parsed)
local pos = getPeripheral("playerDetector").getPlayerPos(parsed.params.username)
if not pos then return "null" end
return textutils.serializeJSON(pos)
elseif parsed.method == "send" then
if not parsed.params.username then
getPeripheral("chatBox").sendMessage(parsed.params.message, parsed.params.prefix)
else
getPeripheral("chatBox").sendMessageToPlayer(parsed.params.message, parsed.params.username, parsed.params.prefix)
end
return "true"
end
error({message = "No message handler for method: "..parsed.method.."!"})

View File

@ -2,9 +2,11 @@
using Discord;
using Discord.Commands;
using Discord.Rest;
using Discord.Webhook;
using Discord.WebSocket;
using Fleck;
using MinecraftDiscordBot.Commands;
using MinecraftDiscordBot.Models;
using MinecraftDiscordBot.Services;
using System.Collections.Concurrent;
using System.Reflection;
@ -26,12 +28,14 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
private readonly HashSet<ulong> _whitelistedChannels;
private readonly ConcurrentDictionary<Guid, RootCommandService> _connections = new();
private static readonly char[] WhiteSpace = new char[] { '\t', '\n', ' ', '\r' };
public ITextChannel[] _channels = Array.Empty<ITextChannel>();
public IEnumerable<ActiveChannel> Channels => _channels ?? throw new InvalidProgramException("Channels used before verification!");
public ActiveChannel[]? _channels;
private bool disposedValue;
private readonly RootCommandService _computer;
public static bool OnlineNotifications => true;
private const string ClientScriptName = "MinecraftDiscordBot.ClientScript.lua";
private const string WebhookName = "minecraftbot";
public readonly string ClientScript;
private readonly ITokenProvider _tokenProvider = new TimeoutTokenProvider(InstanceId, 10);
private static readonly int InstanceId = new Random().Next();
@ -47,10 +51,12 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
.Replace("$HOST", $"ws://{config.SocketHost}:{config.Port}");
}
private async Task Broadcast(Func<ITextChannel, Task<IUserMessage>> message) => _ = await Task.WhenAll(_channels.Select(message));
private Task Broadcast(Func<ITextChannel, Task<IUserMessage>> message)
=> Task.WhenAll(Channels.Select(i => message(i.Channel)));
public Program(BotConfiguration config) {
_config = config;
_computer = new(this);
_computer.ChatMessageReceived += MinecraftMessageReceived;
_administrators = config.Administrators.ToHashSet();
ClientScript = GetClientScript(config);
_client.Log += LogAsync;
@ -63,6 +69,9 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
_whitelistedChannels = config.Channels.ToHashSet();
}
private void MinecraftMessageReceived(object? sender, ChatEvent e)
=> Task.Run(() => WebhookBroadcast(i => i.SendMessageAsync(e.Message, username: e.Username, avatarUrl: $"https://crafatar.com/renders/head/{e.UUID}")));
private Task<T[]> WebhookBroadcast<T>(Func<DiscordWebhookClient, Task<T>> apply) => Task.WhenAll(Channels.Select(i => apply(new DiscordWebhookClient(i.Webhook))));
private void LogWebSocket(LogLevel level, string message, Exception exception) => Log(new(level switch {
LogLevel.Debug => LogSeverity.Debug,
LogLevel.Info => LogSeverity.Info,
@ -98,7 +107,11 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
await LogErrorAsync(BotSource, new InvalidOperationException("No valid textchannel was whitelisted!"));
return false;
}
_channels = channels;
_channels = await Task.WhenAll(channels.Select(async i => new ActiveChannel(i, await GetOrCreateWebhook(i))));
static async Task<IWebhook> GetOrCreateWebhook(ITextChannel i) {
var hooks = (await i.GetWebhooksAsync()).Where(i => i.Name == WebhookName).FirstOrDefault();
return hooks ?? await i.CreateWebhookAsync(WebhookName);
}
return true;
}
@ -192,7 +205,7 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
}
await LogInfoAsync("Discord", $"[{arg.Author.Username}] {arg.Content}");
// TODO: Relay Message to Chat Receiver
_ = Task.Run(() => _computer.Chat.SendMessageAsync(arg.Content, arg.Author.Username, cts.Token));
}
private Task SendResponse(SocketUserMessage message, ResponseType response) => response switch {
@ -261,8 +274,22 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
}
public static void Log(LogMessage msg) {
lock (LogLock)
Console.WriteLine(msg.ToString());
var oldColor = Console.ForegroundColor;
try {
Console.ForegroundColor = msg.Severity switch {
LogSeverity.Critical => ConsoleColor.Magenta,
LogSeverity.Error => ConsoleColor.Red,
LogSeverity.Warning => ConsoleColor.Yellow,
LogSeverity.Info => ConsoleColor.White,
LogSeverity.Verbose => ConsoleColor.Blue,
LogSeverity.Debug => ConsoleColor.DarkBlue,
_ => ConsoleColor.Cyan,
};
lock (LogLock)
Console.WriteLine(msg.ToString());
} finally {
Console.ForegroundColor = oldColor;
}
}
protected virtual void Dispose(bool disposing) {
@ -298,6 +325,16 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
}
}
public class ActiveChannel {
public ActiveChannel(ITextChannel channel, IWebhook webhook) {
Channel = channel;
Webhook = webhook;
}
public IWebhook Webhook { get; }
public ITextChannel Channel { get; }
}
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);

View File

@ -0,0 +1,26 @@
using Discord.WebSocket;
using MinecraftDiscordBot.Commands;
namespace MinecraftDiscordBot.Services;
public class ChatBoxService : CommandRouter {
private readonly ITaskWaitSource _taskSource;
public ChatBoxService(ITaskWaitSource taskSource) => _taskSource = taskSource;
public override string HelpTextPrefix => "!chat ";
public override Task<ResponseType> FallbackHandler(SocketUserMessage message, string method, string[] parameters, CancellationToken ct)
=> throw new ReplyException($"The chat box cannot do '{method}'!");
private Task<T> Method<T>(string methodName, Func<string, T> parser, CancellationToken ct, Dictionary<string, object>? parameters = null)
=> RootCommandService.Method(_taskSource, methodName, parser, ct, parameters);
public Task<bool> SendMessageAsync(string message, string prefix, CancellationToken ct) => Method("send", RootCommandService.Deserialize<bool>(), ct, new() {
["message"] = message,
["prefix"] = prefix
});
public Task<bool> SendMessageToPlayerAsync(string message, string username, string prefix, CancellationToken ct) => Method("send", RootCommandService.Deserialize<bool>(), ct, new() {
["message"] = message,
["username"] = username,
["prefix"] = prefix
});
}

View File

@ -5,7 +5,7 @@ using Newtonsoft.Json;
namespace MinecraftDiscordBot.Services;
internal class PlayerDetectorService : CommandRouter {
public class PlayerDetectorService : CommandRouter {
private readonly ITaskWaitSource _taskSource;
public PlayerDetectorService(ITaskWaitSource taskSource) => _taskSource = taskSource;
@ -16,21 +16,21 @@ internal class PlayerDetectorService : CommandRouter {
private Task<T> Method<T>(string methodName, Func<string, T> parser, CancellationToken ct, Dictionary<string, object>? parameters = null)
=> RootCommandService.Method(_taskSource, methodName, parser, ct, parameters);
public Task<string[]> GetOnlinePlayers(CancellationToken ct) => Method("getonline", RootCommandService.Deserialize<string[]>(), ct);
public Task<string[]> GetOnlinePlayersAsync(CancellationToken ct) => Method("getonline", RootCommandService.Deserialize<string[]>(), ct);
public async Task<PlayerPosition> GetPlayerPosition(string username, CancellationToken ct)
=> (await FindPlayer(username, ct)) ?? throw new ReplyException($"User '{username}' is not online!");
private Task<PlayerPosition?> FindPlayer(string username, CancellationToken ct) => Method("whereis", i => JsonConvert.DeserializeObject<PlayerPosition?>(i), ct, new() {
=> (await FindPlayerAsync(username, ct)) ?? throw new ReplyException($"User '{username}' is not online!");
private Task<PlayerPosition?> FindPlayerAsync(string username, CancellationToken ct) => Method("whereis", i => JsonConvert.DeserializeObject<PlayerPosition?>(i), ct, new() {
["username"] = username
});
[CommandHandler("getonline", HelpText ="Get a list of online players.")]
[CommandHandler("getonline", HelpText = "Get a list of online players.")]
public async Task<ResponseType> HandleOnlinePlayers(SocketUserMessage message, string[] parameters, CancellationToken ct)
=> ResponseType.AsString($"The following players are currently online:\n{string.Join("\n", await GetOnlinePlayers(ct))}");
=> ResponseType.AsString($"The following players are currently online:\n{string.Join("\n", await GetOnlinePlayersAsync(ct))}");
[CommandHandler("whereis", HelpText = "Find a player in the world.")]
public async Task<ResponseType> HandleFindPlayers(SocketUserMessage message, string[] parameters, CancellationToken ct) {
if (parameters is not { Length: 1 }) throw new ReplyException($"Give me only one username!");
var username = parameters[0];
var player = await FindPlayer(username, ct);
var player = await FindPlayerAsync(username, ct);
if (player is null) throw new ReplyException($"{username} is currently offline!");
return ResponseType.AsString($"{username} is at coordinates {player.X} {player.Y} {player.Z} in dimension {player.Dimension}.");
}

View File

@ -19,7 +19,7 @@ public class RefinedStorageService : CommandRouter {
=> throw new ReplyException($"The RS system has no command '{method}'!");
private Task<T> Method<T>(string methodName, Func<string, T> parser, CancellationToken ct, Dictionary<string, object>? parameters = null)
=> RootCommandService.Method<T>(_taskSource, methodName, parser, ct, parameters);
=> RootCommandService.Method(_taskSource, methodName, parser, ct, parameters);
private const string CmdEnergyUsage = "energyusage";
private const string CmdEnergyStorage = "energystorage";
@ -34,17 +34,17 @@ public class RefinedStorageService : CommandRouter {
public async Task<int> GetEnergyStorageAsync(CancellationToken ct) => await Method(CmdEnergyStorage, int.Parse, ct);
public async Task<IEnumerable<Item>> ListItemsAsync(CancellationToken ct) => await Method(CmdListItems, RootCommandService.Deserialize<IEnumerable<Item>>(), ct);
public async Task<IEnumerable<Fluid>> ListFluidsAsync(CancellationToken ct) => await Method(CmdListFluids, RootCommandService.Deserialize<IEnumerable<Fluid>>(), ct);
public async Task<Item> GetItemData(string itemid, CancellationToken ct) => await Method(CmdGetItem, RootCommandService.Deserialize<Item>(), ct, new() {
public async Task<Item> GetItemDataAsync(string itemid, CancellationToken ct) => await Method(CmdGetItem, RootCommandService.Deserialize<Item>(), ct, new() {
["name"] = itemid
});
public async Task<Item> GetItemData(Md5Hash fingerprint, CancellationToken ct) => await Method(CmdGetItem, RootCommandService.Deserialize<Item>(), ct, new() {
public async Task<Item> GetItemDataAsync(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> CraftItemAsync(string itemid, int amount, CancellationToken ct) => await Method(CmdCraftItem, RootCommandService.Deserialize<bool>(), ct, new() {
["name"] = itemid,
["count"] = amount
});
public async Task<LuaPackedArray> RawCommand(string command, CancellationToken ct) => await Method(CmdCommand, LuaPackedArray.Deserialize, ct, new() {
public async Task<LuaPackedArray> RawCommandAsync(string command, CancellationToken ct) => await Method(CmdCommand, LuaPackedArray.Deserialize, ct, new() {
["command"] = command
});
@ -91,7 +91,7 @@ public class RefinedStorageService : CommandRouter {
: parameters.Length is > 2
? throw new ReplyException("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)
return await CraftItemAsync(itemid, amount, ct)
? ResponseType.AsString($"Alright, I'm starting to craft {amount} {itemid}.")
: ResponseType.AsString($"Nope, that somehow doesn't work!");
}
@ -101,8 +101,8 @@ public class RefinedStorageService : CommandRouter {
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];
var item = await (Md5Hash.TryParse(itemid, out var fingerprint)
? GetItemData(fingerprint, ct)
: GetItemData(itemid, ct));
? GetItemDataAsync(fingerprint, ct)
: GetItemDataAsync(itemid, ct));
var sb = new StringBuilder();
sb.Append($"We currently have {item.Amount:n0} {item.CleanDisplayName}!");
if (item.Tags is not null and var tags) {
@ -140,7 +140,7 @@ public class RefinedStorageService : CommandRouter {
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);
var response = await RawCommandAsync(command, ct);
return ResponseType.AsString(response.ToString());
}

View File

@ -13,8 +13,9 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
protected IWebSocketConnection? _socketField;
public override string HelpTextPrefix => "!";
public RootCommandService(IUserRoleManager roleManager) : base() {
_rs = new RefinedStorageService(this, roleManager);
_pd = new PlayerDetectorService(this);
RefinedStorage = new RefinedStorageService(this, roleManager);
Players = new PlayerDetectorService(this);
Chat = new ChatBoxService(this);
}
public static async Task<T> Method<T>(ITaskWaitSource taskSource, string methodName, Func<string, T> parser, CancellationToken ct, Dictionary<string, object>? parameters) {
@ -38,6 +39,10 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
}
}
public RefinedStorageService RefinedStorage { get; }
public PlayerDetectorService Players { get; }
public ChatBoxService Chat { get; }
private void OnMessage(string message) {
switch (Message.Deserialize(message)) {
case ChatEvent msg:
@ -92,15 +97,15 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
return waiter;
}
private readonly ICommandHandler<ResponseType> _rs;
private readonly ICommandHandler<ResponseType> _pd;
[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);
=> RefinedStorage.HandleCommand(message, parameters, ct);
[CommandHandler("pd", HelpText = "Provides some commands for interacting with the Player Detector.")]
public Task<ResponseType> PlayerDetectorHandler(SocketUserMessage message, string[] parameters, CancellationToken ct)
=> _pd.HandleCommand(message, parameters, ct);
=> Players.HandleCommand(message, parameters, ct);
[CommandHandler("chat", HelpText = "Provides some commands for chatting.")]
public Task<ResponseType> ChatBoxHandler(SocketUserMessage message, string[] parameters, CancellationToken ct)
=> Chat.HandleCommand(message, parameters, ct);
public static Func<string, T> Deserialize<T>() => msg
=> JsonConvert.DeserializeObject<T>(msg) ?? throw new InvalidProgramException("Empty response!");