Re-added chat relaying
Fixed async naming scheme Added webhook for every channel Added colorful logging
This commit is contained in:
parent
e7b056342f
commit
f912b9db8f
@ -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.."!"})
|
||||
|
@ -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);
|
||||
|
26
MinecraftDiscordBot/Services/ChatBoxService.cs
Normal file
26
MinecraftDiscordBot/Services/ChatBoxService.cs
Normal 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
|
||||
});
|
||||
}
|
@ -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}.");
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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!");
|
||||
|
Loading…
Reference in New Issue
Block a user