Compare commits

...

6 Commits
1.1.2 ... main

Author SHA1 Message Date
0e09aaef5c
Bumped dependencies 2022-11-30 23:33:48 +01:00
29b8c59c9e
Automatically get version number from project file 2022-11-23 00:35:14 +01:00
bbdd5ce586
Bump version to 1.1.4
Added build script to solution files
Added address to configure client connection destination (if proxied)
Added automatic semantic versioning build
2022-11-23 00:28:01 +01:00
Michael Chen
6920d1a2b3
Bump version to 1.1.3
Added player status events
Added debugger displays for messages
2022-01-18 16:48:44 +01:00
Michael Chen
82c8313cb9
Added peripheral list 2022-01-18 16:01:45 +01:00
Michael Chen
a6ee52f70e
Log to discord log channel 2022-01-18 13:05:34 +01:00
8 changed files with 169 additions and 52 deletions

View File

@ -6,6 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CE65C879-794A-4695-B659-7376FE7DB5E3}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CE65C879-794A-4695-B659-7376FE7DB5E3}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore .gitignore = .gitignore
build.py = build.py
MinecraftDiscordBot\bin\Debug\net6.0\config.json = MinecraftDiscordBot\bin\Debug\net6.0\config.json MinecraftDiscordBot\bin\Debug\net6.0\config.json = MinecraftDiscordBot\bin\Debug\net6.0\config.json
EndProjectSection EndProjectSection
EndProject EndProject

View File

@ -11,8 +11,11 @@ public class BotConfiguration : IBotConfiguration, IBotConfigurator {
[JsonProperty("token", Required = Required.Always)] [JsonProperty("token", Required = Required.Always)]
[Option('t', "token", HelpText = "The Discord bot token", Required = true)] [Option('t', "token", HelpText = "The Discord bot token", Required = true)]
public string Token { get; init; } = default!; public string Token { get; init; } = default!;
[JsonProperty("address", Required = Required.Always)]
[Option('a', "address", HelpText = "The connection string for the websocket", Required = true)]
public string Address { get; init; } = default!;
[JsonProperty("port", Required = Required.DisallowNull)] [JsonProperty("port", Required = Required.DisallowNull)]
[Option('p', "port", Default = DEFAULT_PORT, HelpText = "The websocket server port")] [Option('p', "port", Default = DEFAULT_PORT, HelpText = "The websocket server listen port")]
public int Port { get; init; } = DEFAULT_PORT; public int Port { get; init; } = DEFAULT_PORT;
[JsonProperty("channels", Required = Required.Always)] [JsonProperty("channels", Required = Required.Always)]
[Option('c', "channel", HelpText = "The list of whitelisted channels", Required = true, Min = 1)] [Option('c', "channel", HelpText = "The list of whitelisted channels", Required = true, Min = 1)]
@ -26,6 +29,9 @@ public class BotConfiguration : IBotConfiguration, IBotConfigurator {
[JsonProperty("admins", Required = Required.DisallowNull)] [JsonProperty("admins", Required = Required.DisallowNull)]
[Option("admins", Default = new ulong[] { }, HelpText = "The list of bot administrators.")] [Option("admins", Default = new ulong[] { }, HelpText = "The list of bot administrators.")]
public ulong[] Administrators { get; init; } = Array.Empty<ulong>(); 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] [JsonIgnore]
public BotConfiguration Config => this; public BotConfiguration Config => this;
} }

View File

@ -57,6 +57,16 @@ local function runRsCommand(params)
return textutils.serializeJSON(retvals) return textutils.serializeJSON(retvals)
end 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 -- error: any error during execution
-- return string result -- return string result
local function getResponse(parsed) local function getResponse(parsed)
@ -76,6 +86,8 @@ local function getResponse(parsed)
return textutils.serializeJSON(item) return textutils.serializeJSON(item)
elseif parsed.method == "command" then elseif parsed.method == "command" then
return runRsCommand(parsed.params) return runRsCommand(parsed.params)
elseif parsed.method == "peripherals" then
return textutils.serializeJSON(getPeripheralList())
elseif parsed.method == "getonline" then elseif parsed.method == "getonline" then
return textutils.serializeJSON(getPeripheral("playerDetector").getOnlinePlayers()) return textutils.serializeJSON(getPeripheral("playerDetector").getOnlinePlayers())
elseif parsed.method == "whereis" then elseif parsed.method == "whereis" then
@ -160,10 +172,6 @@ local function chatEventListener(socket)
end end
end end
local function getPeripheralInfo(side)
return {type = peripheral.getType(side), methods = peripheral.getMethods(side), side = side}
end
local function peripheralDetachEventListener(socket) local function peripheralDetachEventListener(socket)
while true do while true do
event, side = os.pullEvent("peripheral_detach") event, side = os.pullEvent("peripheral_detach")
@ -180,10 +188,52 @@ local function peripheralAttachEventListener(socket)
end end
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!")
sleep(5)
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) local function eventListeners(socket)
parallel.waitForAny( parallel.waitForAny(
termWaiter, termWaiter,
function() chatEventListener(socket) end, function() chatEventListener(socket) end,
function() playerStatusEventListener(socket) end,
function() peripheralDetachEventListener(socket) end, function() peripheralDetachEventListener(socket) end,
function() peripheralAttachEventListener(socket) end function() peripheralAttachEventListener(socket) end
) )

View File

@ -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.2</Version> <Version>1.1.5</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>
@ -20,11 +20,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.1.0" /> <PackageReference Include="Discord.Net" Version="3.8.1" />
<PackageReference Include="Fleck" Version="1.2.0" /> <PackageReference Include="Fleck" Version="1.2.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,6 +1,7 @@
using Discord.WebSocket; using Discord.WebSocket;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System.Diagnostics;
namespace MinecraftDiscordBot.Models; namespace MinecraftDiscordBot.Models;
@ -39,14 +40,17 @@ public abstract class Message {
} }
[MessageType(TYPE)] [MessageType(TYPE)]
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
public class CapabilityMessage : Message { public class CapabilityMessage : Message {
private const string TYPE = "roles"; private const string TYPE = "roles";
public override string Type => TYPE; public override string Type => TYPE;
[JsonProperty("role", Required = Required.Always)] [JsonProperty("role", Required = Required.Always)]
public string[] Role { get; set; } = default!; public string[] Role { get; set; } = default!;
public override string ToString() => $"Capabilities: {string.Join(", ", Role)}";
} }
[MessageType(TYPE)] [MessageType(TYPE)]
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
public class ReplyMessage : Message { public class ReplyMessage : Message {
private const string TYPE = "reply"; private const string TYPE = "reply";
public override string Type => TYPE; public override string Type => TYPE;
@ -60,19 +64,35 @@ public class ReplyMessage : Message {
public int Total { get; set; } = 1; public int Total { get; set; } = 1;
[JsonProperty("success", Required = Required.DisallowNull)] [JsonProperty("success", Required = Required.DisallowNull)]
public ResultState State { get; set; } = ResultState.Successful; public ResultState State { get; set; } = ResultState.Successful;
public override string ToString() => $"Reply [{AnswerId}] {State} ({Chunk}/{Total}) Length {Result.Length}";
} }
public abstract class EventMessage : Message { } public abstract class EventMessage : Message { }
[MessageType(TYPE)] [MessageType(TYPE)]
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
public class PeripheralDetachEvent : EventMessage { public class PeripheralDetachEvent : EventMessage {
private const string TYPE = "peripheral_detach"; private const string TYPE = "peripheral_detach";
public override string Type => TYPE; public override string Type => TYPE;
[JsonProperty("side", Required = Required.Always)] [JsonProperty("side", Required = Required.Always)]
public string Side { get; set; } = default!; public string Side { get; set; } = default!;
public override string ToString() => $"Detached '{Side}'!";
} }
[MessageType(TYPE)] [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 { public class PeripheralAttachEvent : EventMessage {
private const string TYPE = "peripheral"; private const string TYPE = "peripheral";
public override string Type => TYPE; public override string Type => TYPE;
@ -80,6 +100,7 @@ public class PeripheralAttachEvent : EventMessage {
public string Side => Peripheral.Side; public string Side => Peripheral.Side;
[JsonProperty("peripheral", Required = Required.Always)] [JsonProperty("peripheral", Required = Required.Always)]
public Peripheral Peripheral { get; set; } = default!; public Peripheral Peripheral { get; set; } = default!;
public override string ToString() => $"Attached {Peripheral}!";
} }
public class Peripheral { public class Peripheral {
@ -89,9 +110,11 @@ public class Peripheral {
public string Type { get; set; } = default!; public string Type { get; set; } = default!;
[JsonProperty("methods", Required = Required.Always)] [JsonProperty("methods", Required = Required.Always)]
public string[] Methods { get; set; } = default!; public string[] Methods { get; set; } = default!;
public override string ToString() => $"{Type} at '{Side}'";
} }
[MessageType(TYPE)] [MessageType(TYPE)]
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
public class ChatEvent : EventMessage { public class ChatEvent : EventMessage {
private const string TYPE = "chat"; private const string TYPE = "chat";
public override string Type => TYPE; public override string Type => TYPE;
@ -103,9 +126,11 @@ public class ChatEvent : EventMessage {
public string UUID { get; set; } = default!; public string UUID { get; set; } = default!;
[JsonProperty("hidden", Required = Required.Always)] [JsonProperty("hidden", Required = Required.Always)]
public bool IsHidden { get; set; } public bool IsHidden { get; set; }
public override string ToString() => $"{(IsHidden ? "HIDDEN: " : string.Empty)}[{Username}] {Message} ({UUID})";
} }
[MessageType(TYPE)] [MessageType(TYPE)]
[DebuggerDisplay($"{{{nameof(ToString)}(),nq}}")]
public class RequestMessage : Message { public class RequestMessage : Message {
private const string TYPE = "request"; private const string TYPE = "request";
public override string Type => TYPE; public override string Type => TYPE;
@ -121,4 +146,5 @@ public class RequestMessage : Message {
public string Method { get; set; } public string Method { get; set; }
[JsonProperty("params")] [JsonProperty("params")]
public Dictionary<string, object> Parameters { get; } public Dictionary<string, object> Parameters { get; }
public override string ToString() => $"Request [{AnswerId}] {Method}({JsonConvert.SerializeObject(Parameters)})";
} }

View File

@ -31,9 +31,11 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
public IEnumerable<ActiveChannel> Channels => _channels ?? throw new InvalidProgramException("Channels used before verification!"); public IEnumerable<ActiveChannel> Channels => _channels ?? throw new InvalidProgramException("Channels used before verification!");
public ActiveChannel[]? _channels; public ActiveChannel[]? _channels;
private bool disposedValue; private bool disposedValue;
private static ITextChannel? LogChannel;
private readonly RootCommandService _computer; private readonly RootCommandService _computer;
public static bool OnlineNotifications => true; public static bool OnlineNotifications => true;
public const LogSeverity DiscordLogSeverity = LogSeverity.Warning;
private const string ClientScriptName = "MinecraftDiscordBot.ClientScript.lua"; private const string ClientScriptName = "MinecraftDiscordBot.ClientScript.lua";
private const string WebhookName = "minecraftbot"; private const string WebhookName = "minecraftbot";
public readonly string ClientScript; public readonly string ClientScript;
@ -48,7 +50,7 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
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);
return sr.ReadToEnd() return sr.ReadToEnd()
.Replace("$HOST", $"ws://{config.SocketHost}:{config.Port}"); .Replace("$HOST", config.Address);
} }
private Task Broadcast(Func<ITextChannel, Task<IUserMessage>> message) private Task Broadcast(Func<ITextChannel, Task<IUserMessage>> message)
@ -58,6 +60,9 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
_computer = new(this); _computer = new(this);
_computer.ChatMessageReceived += MinecraftMessageReceived; _computer.ChatMessageReceived += MinecraftMessageReceived;
_computer.SocketChanged += ComputerConnectedChanged; _computer.SocketChanged += ComputerConnectedChanged;
_computer.PlayerStatusChanged += PlayerStatusChanged;
_computer.PeripheralAttached += PeripheralAttached;
_computer.PeripheralDetached += PeripheralDetached;
_administrators = config.Administrators.ToHashSet(); _administrators = config.Administrators.ToHashSet();
ClientScript = GetClientScript(config); ClientScript = GetClientScript(config);
_client.Log += LogAsync; _client.Log += LogAsync;
@ -70,11 +75,14 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
_whitelistedChannels = config.Channels.ToHashSet(); _whitelistedChannels = config.Channels.ToHashSet();
} }
private void ComputerConnectedChanged(object? sender, IWebSocketConnection? e) { private void PlayerStatusChanged(object? sender, PlayerStatusEvent e)
_ = Task.Run(() => Broadcast(i => i.SendMessageAsync(e is not null => _ = 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 is now available!"
: "The Minecraft client disconnected!"))); : "The Minecraft client disconnected!")));
}
private void MinecraftMessageReceived(object? sender, ChatEvent e) 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}"))); => 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 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() { 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!")); await LogErrorAsync(BotSource, new InvalidOperationException("No valid textchannel was whitelisted!"));
return false; return false;
} }
@ -127,13 +140,14 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
socket.OnMessage = async message => await SocketReceived(socket, message); socket.OnMessage = async message => await SocketReceived(socket, message);
}); });
private async IAsyncEnumerable<ITextChannel> GetValidChannels(IEnumerable<ulong> ids) { private async Task<ITextChannel[]> GetValidChannels(IEnumerable<ulong> ids)
foreach (var channelId in 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); var channel = await _client.GetChannelAsync(channelId);
if (channel is not ITextChannel textChannel) { if (channel is not ITextChannel textChannel) {
if (channel is null) await LogWarningAsync(BotSource, $"Channel with id [{channelId}] does not exist!"); 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}]!"); else await LogWarningAsync(BotSource, $"Channel is not a text channels and will not be used: {channel.Name} [{channel.Id}]!");
continue; return null;
} }
if (textChannel.Guild is RestGuild guild) { if (textChannel.Guild is RestGuild guild) {
@ -142,8 +156,7 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
} else { } else {
await LogWarningAsync(BotSource, $"Whitelisted in channel: {channel.Name} [{channel.Id}] on unknown server!"); await LogWarningAsync(BotSource, $"Whitelisted in channel: {channel.Name} [{channel.Id}] on unknown server!");
} }
yield return textChannel; return textChannel;
}
} }
private async Task SocketReceived(IWebSocketConnection socket, string message) { 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)); public static void LogError(string source, Exception exception) => Log(new(LogSeverity.Error, source, exception?.Message, exception));
private static async Task LogAsync(LogMessage msg) { private static async Task LogAsync(LogMessage msg) {
Log(msg);
await Task.CompletedTask;
}
public static void Log(LogMessage msg) {
lock (LogLock) { lock (LogLock) {
var oldColor = Console.ForegroundColor; var oldColor = Console.ForegroundColor;
try { try {
@ -298,7 +306,12 @@ public class Program : IDisposable, ICommandHandler<ResponseType>, IUserRoleMana
Console.ForegroundColor = oldColor; 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) { protected virtual void Dispose(bool disposing) {
if (!disposedValue) { if (!disposedValue) {

View File

@ -3,6 +3,7 @@ using Fleck;
using MinecraftDiscordBot.Commands; using MinecraftDiscordBot.Commands;
using MinecraftDiscordBot.Models; using MinecraftDiscordBot.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Linq;
namespace MinecraftDiscordBot.Services; namespace MinecraftDiscordBot.Services;
@ -18,13 +19,14 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
Chat = new ChatBoxService(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) { 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); var waiter = taskSource.GetWaiter(parser, ct);
await taskSource.Send(new RequestMessage(waiter.ID, methodName, parameters)); await taskSource.Send(new RequestMessage(waiter.ID, methodName, parameters));
return await waiter.Task; return await waiter.Task;
} }
public event EventHandler<ChatEvent>? ChatMessageReceived; public event EventHandler<ChatEvent>? ChatMessageReceived;
public event EventHandler<PlayerStatusEvent>? PlayerStatusChanged;
public event EventHandler<PeripheralAttachEvent>? PeripheralAttached; public event EventHandler<PeripheralAttachEvent>? PeripheralAttached;
public event EventHandler<PeripheralDetachEvent>? PeripheralDetached; public event EventHandler<PeripheralDetachEvent>? PeripheralDetached;
public event EventHandler<IWebSocketConnection?>? SocketChanged; public event EventHandler<IWebSocketConnection?>? SocketChanged;
@ -48,6 +50,9 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
case ChatEvent msg: case ChatEvent msg:
ChatMessageReceived?.Invoke(this, msg); ChatMessageReceived?.Invoke(this, msg);
break; break;
case PlayerStatusEvent msg:
PlayerStatusChanged?.Invoke(this, msg);
break;
case PeripheralAttachEvent msg: case PeripheralAttachEvent msg:
PeripheralAttached?.Invoke(this, msg); PeripheralAttached?.Invoke(this, msg);
break; break;
@ -97,9 +102,13 @@ public class RootCommandService : CommandRouter, ITaskWaitSource {
return waiter; 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.")] [CommandHandler("rs", HelpText = "Provides some commands for interacting with the Refined Storage system.")]
public Task<ResponseType> RefinedStorageHandler(SocketUserMessage message, string[] parameters, CancellationToken ct) public Task<ResponseType> RefinedStorageHandler(SocketUserMessage message, string[] parameters, CancellationToken ct)
=> RefinedStorage.HandleCommand(message, parameters, 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.")] [CommandHandler("pd", HelpText = "Provides some commands for interacting with the Player Detector.")]
public Task<ResponseType> PlayerDetectorHandler(SocketUserMessage message, string[] parameters, CancellationToken ct) public Task<ResponseType> PlayerDetectorHandler(SocketUserMessage message, string[] parameters, CancellationToken ct)
=> Players.HandleCommand(message, parameters, ct); => Players.HandleCommand(message, parameters, ct);

View File

@ -2,11 +2,12 @@
import subprocess import subprocess
import argparse import argparse
from itertools import chain from itertools import chain
import re
dockercmd = 'docker' dockercmd = 'docker'
parser = argparse.ArgumentParser(description='Create custom recumock images.') parser = argparse.ArgumentParser(description='Create custom recumock images.')
parser.add_argument('tags', metavar='TAG', nargs='+', help='Version tags to build.') parser.add_argument('tags', metavar='TAG', nargs='*', help='Version tags to build.')
args = parser.parse_args() args = parser.parse_args()
@ -15,20 +16,31 @@ platforms = ['linux/amd64', 'linux/arm64', 'linux/arm/v7']
def pull(image): def pull(image):
subprocess.run([dockercmd, 'pull', baseimage], check=True) subprocess.run([dockercmd, 'pull', baseimage], check=True)
def build(image, directory, platforms, build_args = None): def build(images, directory, platforms, build_args = None):
if build_args is None: if build_args is None:
build_args = [] build_args = []
build_args = list(chain.from_iterable(['--build-arg', f'{arg}={val}'] for (arg, val) in build_args)) build_args = list(chain.from_iterable(['--build-arg', f'{arg}={val}'] for (arg, val) in build_args))
tags = list(chain.from_iterable(['-t', image] for image in images))
platformlist = ','.join(platforms) platformlist = ','.join(platforms)
subprocess.run([dockercmd, 'buildx', 'build', '-f', 'MinecraftDiscordBot/Dockerfile', '--platform', platformlist, '-t', image] + build_args + ['--push', directory], check=True) command = [dockercmd, 'buildx', 'build', '-f', 'MinecraftDiscordBot/Dockerfile', '--platform', platformlist, *tags] + build_args + ['--push', directory]
print(' '.join(command))
subprocess.run(command, check=True)
def version_from_project():
with open(r'MinecraftDiscordBot\MinecraftDiscordBot.csproj', 'r') as f:
project = f.read()
for tag in args.tags: regex = r"<Version>\s*([^<]*?)\s*<\/Version>"
targetimage = f'chenio/mcdiscordbot:{tag}' matches = re.search(regex, project, re.IGNORECASE)
baseimage = f'mcr.microsoft.com/dotnet/runtime:6.0' if not matches:
raise Exception("Could not read version from project file!")
return matches.group(1)
#print(f'Pulling base image {baseimage}') if len(args.tags) == 0:
#pull(baseimage) args.tags.append(version_from_project())
print(f'Building image {targetimage} from {baseimage}.')
build(targetimage, '.', platforms, [('TAG', tag)])
for version in args.tags:
parts = version.split('.')
tags = list('.'.join(parts[:i]) for i in range(1, len(parts) + 1))
tags.append('latest')
build([f'chenio/mcdiscordbot:{tag}' for tag in tags], '.', platforms)