Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
6920d1a2b3 | |||
82c8313cb9 | |||
a6ee52f70e |
@ -26,6 +26,9 @@ public class BotConfiguration : IBotConfiguration, IBotConfigurator {
|
||||
[JsonProperty("admins", Required = Required.DisallowNull)]
|
||||
[Option("admins", Default = new ulong[] { }, HelpText = "The list of bot administrators.")]
|
||||
public ulong[] Administrators { get; init; } = Array.Empty<ulong>();
|
||||
[JsonProperty("logchannel", Required = Required.DisallowNull)]
|
||||
[Option("logchannel", Default = null, HelpText = "Optionally the id of a channel to mirror log to.")]
|
||||
public ulong? LogChannel { get; init; } = null;
|
||||
[JsonIgnore]
|
||||
public BotConfiguration Config => this;
|
||||
}
|
||||
|
@ -57,6 +57,16 @@ local function runRsCommand(params)
|
||||
return textutils.serializeJSON(retvals)
|
||||
end
|
||||
|
||||
local function getPeripheralInfo(side)
|
||||
return {type = peripheral.getType(side), methods = peripheral.getMethods(side), side = side}
|
||||
end
|
||||
|
||||
local function getPeripheralList()
|
||||
local pers = {}
|
||||
for i,side in pairs(peripheral.getNames()) do pers[side] = getPeripheralInfo(side) end
|
||||
return pers
|
||||
end
|
||||
|
||||
-- error: any error during execution
|
||||
-- return string result
|
||||
local function getResponse(parsed)
|
||||
@ -76,6 +86,8 @@ local function getResponse(parsed)
|
||||
return textutils.serializeJSON(item)
|
||||
elseif parsed.method == "command" then
|
||||
return runRsCommand(parsed.params)
|
||||
elseif parsed.method == "peripherals" then
|
||||
return textutils.serializeJSON(getPeripheralList())
|
||||
elseif parsed.method == "getonline" then
|
||||
return textutils.serializeJSON(getPeripheral("playerDetector").getOnlinePlayers())
|
||||
elseif parsed.method == "whereis" then
|
||||
@ -160,10 +172,6 @@ local function chatEventListener(socket)
|
||||
end
|
||||
end
|
||||
|
||||
local function getPeripheralInfo(side)
|
||||
return {type = peripheral.getType(side), methods = peripheral.getMethods(side), side = side}
|
||||
end
|
||||
|
||||
local function peripheralDetachEventListener(socket)
|
||||
while true do
|
||||
event, side = os.pullEvent("peripheral_detach")
|
||||
@ -180,10 +188,51 @@ local function peripheralAttachEventListener(socket)
|
||||
end
|
||||
end
|
||||
|
||||
local function listAsSet(list)
|
||||
local asSet = {}
|
||||
for i,elem in pairs(list) do asSet[elem] = true end
|
||||
return asSet
|
||||
end
|
||||
|
||||
local function joinSets(a, b)
|
||||
local joined = {}
|
||||
for elem,exists in pairs(a) do if exists then joined[elem] = true end end
|
||||
for elem,exists in pairs(b) do if exists then joined[elem] = true end end
|
||||
return joined
|
||||
end
|
||||
|
||||
local function playerStatusEventListener(socket)
|
||||
local players = {}
|
||||
while true do
|
||||
local pd = peripheral.find("playerDetector")
|
||||
if not not pd then
|
||||
players = listAsSet(pd.getOnlinePlayers())
|
||||
break
|
||||
end
|
||||
printError("playerDetector not connected!")
|
||||
end
|
||||
while true do
|
||||
local pd = peripheral.find("playerDetector")
|
||||
if not not pd then
|
||||
local newPlayers = listAsSet(pd.getOnlinePlayers())
|
||||
for player,_ in pairs(joinSets(players, newPlayers)) do
|
||||
if players[player] and (not newPlayers[player]) then
|
||||
sendJson(socket, {type = "playerstatus", player = player, status = false})
|
||||
elseif (not players[player]) and newPlayers[player] then
|
||||
sendJson(socket, {type = "playerstatus", player = player, status = true})
|
||||
end
|
||||
end
|
||||
players = newPlayers
|
||||
end
|
||||
sleep(1)
|
||||
end
|
||||
end
|
||||
|
||||
local function eventListeners(socket)
|
||||
parallel.waitForAny(
|
||||
termWaiter,
|
||||
function() chatEventListener(socket) end,
|
||||
function() playerStatusEventListener(socket) end,
|
||||
function() peripheralDetachEventListener(socket) end,
|
||||
function() peripheralAttachEventListener(socket) end
|
||||
)
|
||||
|
@ -6,7 +6,7 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<Version>1.1.2</Version>
|
||||
<Version>1.1.3</Version>
|
||||
<Authors>Michael Chen</Authors>
|
||||
<Company>$(Authors)</Company>
|
||||
<RepositoryUrl>https://gitlab.com/chenmichael/mcdiscordbot</RepositoryUrl>
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Discord.WebSocket;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace MinecraftDiscordBot.Models;
|
||||
|
||||
@ -39,14 +40,17 @@ public abstract class Message {
|
||||
}
|
||||
|
||||
[MessageType(TYPE)]
|
||||
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
|
||||
public class CapabilityMessage : Message {
|
||||
private const string TYPE = "roles";
|
||||
public override string Type => TYPE;
|
||||
[JsonProperty("role", Required = Required.Always)]
|
||||
public string[] Role { get; set; } = default!;
|
||||
public override string ToString() => $"Capabilities: {string.Join(", ", Role)}";
|
||||
}
|
||||
|
||||
[MessageType(TYPE)]
|
||||
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
|
||||
public class ReplyMessage : Message {
|
||||
private const string TYPE = "reply";
|
||||
public override string Type => TYPE;
|
||||
@ -60,19 +64,35 @@ public class ReplyMessage : Message {
|
||||
public int Total { get; set; } = 1;
|
||||
[JsonProperty("success", Required = Required.DisallowNull)]
|
||||
public ResultState State { get; set; } = ResultState.Successful;
|
||||
public override string ToString() => $"Reply [{AnswerId}] {State} ({Chunk}/{Total}) Length {Result.Length}";
|
||||
}
|
||||
|
||||
public abstract class EventMessage : Message { }
|
||||
|
||||
[MessageType(TYPE)]
|
||||
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
|
||||
public class PeripheralDetachEvent : EventMessage {
|
||||
private const string TYPE = "peripheral_detach";
|
||||
public override string Type => TYPE;
|
||||
[JsonProperty("side", Required = Required.Always)]
|
||||
public string Side { get; set; } = default!;
|
||||
public override string ToString() => $"Detached '{Side}'!";
|
||||
}
|
||||
|
||||
[MessageType(TYPE)]
|
||||
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
|
||||
public class PlayerStatusEvent : EventMessage {
|
||||
private const string TYPE = "playerstatus";
|
||||
public override string Type => TYPE;
|
||||
[JsonProperty("player", Required = Required.Always)]
|
||||
public string Player { get; set; } = default!;
|
||||
[JsonProperty("status", Required = Required.Always)]
|
||||
public bool Online { get; set; }
|
||||
public override string ToString() => $"{Player} is now {(Online ? "on" : "off")}line!";
|
||||
}
|
||||
|
||||
[MessageType(TYPE)]
|
||||
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
|
||||
public class PeripheralAttachEvent : EventMessage {
|
||||
private const string TYPE = "peripheral";
|
||||
public override string Type => TYPE;
|
||||
@ -80,6 +100,7 @@ public class PeripheralAttachEvent : EventMessage {
|
||||
public string Side => Peripheral.Side;
|
||||
[JsonProperty("peripheral", Required = Required.Always)]
|
||||
public Peripheral Peripheral { get; set; } = default!;
|
||||
public override string ToString() => $"Attached {Peripheral}!";
|
||||
}
|
||||
|
||||
public class Peripheral {
|
||||
@ -89,9 +110,11 @@ public class Peripheral {
|
||||
public string Type { get; set; } = default!;
|
||||
[JsonProperty("methods", Required = Required.Always)]
|
||||
public string[] Methods { get; set; } = default!;
|
||||
public override string ToString() => $"{Type} at '{Side}'";
|
||||
}
|
||||
|
||||
[MessageType(TYPE)]
|
||||
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
|
||||
public class ChatEvent : EventMessage {
|
||||
private const string TYPE = "chat";
|
||||
public override string Type => TYPE;
|
||||
@ -103,9 +126,11 @@ public class ChatEvent : EventMessage {
|
||||
public string UUID { get; set; } = default!;
|
||||
[JsonProperty("hidden", Required = Required.Always)]
|
||||
public bool IsHidden { get; set; }
|
||||
public override string ToString() => $"{(IsHidden ? "HIDDEN: " : string.Empty)}[{Username}] {Message} ({UUID})";
|
||||
}
|
||||
|
||||
[MessageType(TYPE)]
|
||||
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
|
||||
public class RequestMessage : Message {
|
||||
private const string TYPE = "request";
|
||||
public override string Type => TYPE;
|
||||
@ -121,4 +146,5 @@ public class RequestMessage : Message {
|
||||
public string Method { get; set; }
|
||||
[JsonProperty("params")]
|
||||
public Dictionary<string, object> Parameters { get; }
|
||||
public override string ToString() => $"Request [{AnswerId}] {Method}({JsonConvert.SerializeObject(Parameters)})";
|
||||
}
|
||||
|
@ -31,9 +31,11 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
|
||||
public IEnumerable<ActiveChannel> Channels => _channels ?? throw new InvalidProgramException("Channels used before verification!");
|
||||
public ActiveChannel[]? _channels;
|
||||
private bool disposedValue;
|
||||
private static ITextChannel? LogChannel;
|
||||
private readonly RootCommandService _computer;
|
||||
|
||||
public static bool OnlineNotifications => true;
|
||||
public const LogSeverity DiscordLogSeverity = LogSeverity.Warning;
|
||||
private const string ClientScriptName = "MinecraftDiscordBot.ClientScript.lua";
|
||||
private const string WebhookName = "minecraftbot";
|
||||
public readonly string ClientScript;
|
||||
@ -58,6 +60,9 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
|
||||
_computer = new(this);
|
||||
_computer.ChatMessageReceived += MinecraftMessageReceived;
|
||||
_computer.SocketChanged += ComputerConnectedChanged;
|
||||
_computer.PlayerStatusChanged += PlayerStatusChanged;
|
||||
_computer.PeripheralAttached += PeripheralAttached;
|
||||
_computer.PeripheralDetached += PeripheralDetached;
|
||||
_administrators = config.Administrators.ToHashSet();
|
||||
ClientScript = GetClientScript(config);
|
||||
_client.Log += LogAsync;
|
||||
@ -70,11 +75,14 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
|
||||
_whitelistedChannels = config.Channels.ToHashSet();
|
||||
}
|
||||
|
||||
private void ComputerConnectedChanged(object? sender, IWebSocketConnection? e) {
|
||||
_ = Task.Run(() => Broadcast(i => i.SendMessageAsync(e is not null
|
||||
? "The Minecraft client is now available!"
|
||||
: "The Minecraft client disconnected!")));
|
||||
}
|
||||
private void PlayerStatusChanged(object? sender, PlayerStatusEvent e)
|
||||
=> _ = Task.Run(() => Broadcast(i => i.SendMessageAsync($"{e.Player} just {(e.Online ? "joined" : "left")} the server!")));
|
||||
private void PeripheralAttached(object? sender, PeripheralAttachEvent e) => LogInfo("Computer", $"Peripheral {e.Peripheral.Type} was attached on side {e.Side}.");
|
||||
private void PeripheralDetached(object? sender, PeripheralDetachEvent e) => LogInfo("Computer", $"Peripheral on side {e.Side} was detached.");
|
||||
private void ComputerConnectedChanged(object? sender, IWebSocketConnection? e)
|
||||
=> _ = Task.Run(() => Broadcast(i => i.SendMessageAsync(e is not null
|
||||
? "The Minecraft client is now available!"
|
||||
: "The Minecraft client disconnected!")));
|
||||
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))));
|
||||
@ -109,7 +117,12 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
|
||||
}
|
||||
|
||||
private async Task<bool> HasValidChannels() {
|
||||
if (await GetValidChannels(_whitelistedChannels).ToArrayAsync() is not { Length: > 0 } channels) {
|
||||
if (_config.LogChannel is ulong logChannelId) {
|
||||
LogChannel = await IsValidChannel(logChannelId);
|
||||
if (LogChannel is null)
|
||||
await LogWarningAsync(BotSource, $"The given log channel ID is not valid '{logChannelId}'!");
|
||||
}
|
||||
if (await GetValidChannels(_whitelistedChannels) is not { Length: > 0 } channels) {
|
||||
await LogErrorAsync(BotSource, new InvalidOperationException("No valid textchannel was whitelisted!"));
|
||||
return false;
|
||||
}
|
||||
@ -127,23 +140,23 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
|
||||
socket.OnMessage = async message => await SocketReceived(socket, message);
|
||||
});
|
||||
|
||||
private async IAsyncEnumerable<ITextChannel> GetValidChannels(IEnumerable<ulong> ids) {
|
||||
foreach (var channelId in ids) {
|
||||
var channel = await _client.GetChannelAsync(channelId);
|
||||
if (channel is not ITextChannel textChannel) {
|
||||
if (channel is null) await LogWarningAsync(BotSource, $"Channel with id [{channelId}] does not exist!");
|
||||
else await LogWarningAsync(BotSource, $"Channel is not a text channels and will not be used: {channel.Name} [{channel.Id}]!");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (textChannel.Guild is RestGuild guild) {
|
||||
await guild.UpdateAsync();
|
||||
await LogInfoAsync(BotSource, $"Whitelisted in channel: {channel.Name} [{channel.Id}] on server {guild.Name} [{guild.Id}]");
|
||||
} else {
|
||||
await LogWarningAsync(BotSource, $"Whitelisted in channel: {channel.Name} [{channel.Id}] on unknown server!");
|
||||
}
|
||||
yield return textChannel;
|
||||
private async Task<ITextChannel[]> GetValidChannels(IEnumerable<ulong> ids)
|
||||
=> (await Task.WhenAll(ids.Select(i => IsValidChannel(i)))).OfType<ITextChannel>().ToArray();
|
||||
private async Task<ITextChannel?> IsValidChannel(ulong channelId) {
|
||||
var channel = await _client.GetChannelAsync(channelId);
|
||||
if (channel is not ITextChannel textChannel) {
|
||||
if (channel is null) await LogWarningAsync(BotSource, $"Channel with id [{channelId}] does not exist!");
|
||||
else await LogWarningAsync(BotSource, $"Channel is not a text channels and will not be used: {channel.Name} [{channel.Id}]!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (textChannel.Guild is RestGuild guild) {
|
||||
await guild.UpdateAsync();
|
||||
await LogInfoAsync(BotSource, $"Whitelisted in channel: {channel.Name} [{channel.Id}] on server {guild.Name} [{guild.Id}]");
|
||||
} else {
|
||||
await LogWarningAsync(BotSource, $"Whitelisted in channel: {channel.Name} [{channel.Id}] on unknown server!");
|
||||
}
|
||||
return textChannel;
|
||||
}
|
||||
|
||||
private async Task SocketReceived(IWebSocketConnection socket, string message) {
|
||||
@ -276,11 +289,6 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
|
||||
public static void LogError(string source, Exception exception) => Log(new(LogSeverity.Error, source, exception?.Message, exception));
|
||||
|
||||
private static async Task LogAsync(LogMessage msg) {
|
||||
Log(msg);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static void Log(LogMessage msg) {
|
||||
lock (LogLock) {
|
||||
var oldColor = Console.ForegroundColor;
|
||||
try {
|
||||
@ -298,8 +306,13 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
|
||||
Console.ForegroundColor = oldColor;
|
||||
}
|
||||
}
|
||||
if (msg.Severity <= DiscordLogSeverity && LogChannel is ITextChannel log) {
|
||||
await log.SendMessageAsync($"{msg.Severity}: {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Log(LogMessage msg) => _ = Task.Run(() => LogAsync(msg));
|
||||
|
||||
protected virtual void Dispose(bool disposing) {
|
||||
if (!disposedValue) {
|
||||
if (disposing) {
|
||||
|
@ -3,6 +3,7 @@ using Fleck;
|
||||
using MinecraftDiscordBot.Commands;
|
||||
using MinecraftDiscordBot.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System.Linq;
|
||||
|
||||
namespace MinecraftDiscordBot.Services;
|
||||
|
||||
@ -18,13 +19,14 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
|
||||
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) {
|
||||
public static async Task<T> Method<T>(ITaskWaitSource taskSource, string methodName, Func<string, T> parser, CancellationToken ct, Dictionary<string, object>? parameters = null) {
|
||||
var waiter = taskSource.GetWaiter(parser, ct);
|
||||
await taskSource.Send(new RequestMessage(waiter.ID, methodName, parameters));
|
||||
return await waiter.Task;
|
||||
}
|
||||
|
||||
public event EventHandler<ChatEvent>? ChatMessageReceived;
|
||||
public event EventHandler<PlayerStatusEvent>? PlayerStatusChanged;
|
||||
public event EventHandler<PeripheralAttachEvent>? PeripheralAttached;
|
||||
public event EventHandler<PeripheralDetachEvent>? PeripheralDetached;
|
||||
public event EventHandler<IWebSocketConnection?>? SocketChanged;
|
||||
@ -48,6 +50,9 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
|
||||
case ChatEvent msg:
|
||||
ChatMessageReceived?.Invoke(this, msg);
|
||||
break;
|
||||
case PlayerStatusEvent msg:
|
||||
PlayerStatusChanged?.Invoke(this, msg);
|
||||
break;
|
||||
case PeripheralAttachEvent msg:
|
||||
PeripheralAttached?.Invoke(this, msg);
|
||||
break;
|
||||
@ -97,9 +102,13 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
|
||||
return waiter;
|
||||
}
|
||||
|
||||
public Task<Dictionary<string, Peripheral>> GetPeripherals(CancellationToken ct) => Method(this, "peripherals", Deserialize<Dictionary<string, Peripheral>>(), ct);
|
||||
[CommandHandler("rs", HelpText = "Provides some commands for interacting with the Refined Storage system.")]
|
||||
public Task<ResponseType> RefinedStorageHandler(SocketUserMessage message, string[] parameters, CancellationToken ct)
|
||||
=> RefinedStorage.HandleCommand(message, parameters, ct);
|
||||
[CommandHandler("peripherals", HelpText = "Gets a list of peripherals that are attached.")]
|
||||
public async Task<ResponseType> HandleGetPeripherals(SocketUserMessage message, string[] parameters, CancellationToken ct)
|
||||
=> ResponseType.AsString(string.Join("\n", (await GetPeripherals(ct)).Values.Select(i => $"On side {i.Side}: {i.Type}")));
|
||||
[CommandHandler("pd", HelpText = "Provides some commands for interacting with the Player Detector.")]
|
||||
public Task<ResponseType> PlayerDetectorHandler(SocketUserMessage message, string[] parameters, CancellationToken ct)
|
||||
=> Players.HandleCommand(message, parameters, ct);
|
||||
|
Reference in New Issue
Block a user