using Discord; using Discord.Commands; using Discord.Rest; using Discord.WebSocket; using Fleck; using Newtonsoft.Json; using System.Collections.Concurrent; using System.Reflection; namespace MinecraftDiscordBot; public class Program { private const string WebSocketSource = "WebSocket"; private readonly object _logLock = new(); private readonly DiscordSocketClient _client = new(new() { LogLevel = LogSeverity.Verbose }); private readonly WebSocketServer _wssv; private readonly BotConfiguration _config; private readonly HashSet _whitelistedChannels; private readonly ConcurrentDictionary _connections = new(); private static readonly char[] WhiteSpace = new char[] { '\t', '\n', ' ', '\r' }; private RefinedStorageComputer? _rsSystem = null; public Program(BotConfiguration config) { _config = config; _client.Log += Log; _client.MessageReceived += (msg) => DiscordMessageReceived(msg); _wssv = new WebSocketServer($"ws://0.0.0.0:{config.Port}") { RestartAfterListenError = true }; _whitelistedChannels = config.Channels.ToHashSet(); } public static Task Main(string[] args) => JsonConvert.DeserializeObject(File.ReadAllText("config.json")) is BotConfiguration config ? new Program(config).RunAsync() : throw new InvalidProgramException("Configuration file missing!"); public async Task RunAsync() { _wssv.Start(socket => { socket.OnOpen = async () => await SocketOpened(socket); socket.OnClose = async () => await SocketClosed(socket); socket.OnMessage = async message => await SocketReceived(socket, message); }); await _client.LoginAsync(TokenType.Bot, _config.Token); await _client.StartAsync(); #if !DEBUG await VerifyTextChannels(); #endif // Block this task until the program is closed. await Task.Delay(-1); return 0; } private async Task VerifyTextChannels() { var channels = await Task.WhenAll(_whitelistedChannels.Select(id => _client.GetChannelAsync(id).AsTask()).ToArray()); await Task.WhenAll(channels.Where(i => i is ITextChannel { Guild: RestGuild }).Select(i => ((RestGuild)((ITextChannel)i).Guild).UpdateAsync())); foreach (var channel in channels) { if (channel is ITextChannel tchannel) Console.WriteLine($"Whitelisted in channel: {channel.Name} [{channel.Id}] on server {tchannel.Guild.Name} [{tchannel.Guild.Id}]"); else throw new InvalidProgramException($"Cannot use this bot on non-text channel {channel.Name} [{channel.Id}]!"); } } private async Task SocketReceived(IWebSocketConnection socket, string message) { var capability = JsonConvert.DeserializeObject(message); if (capability is null) return; var pc = capability.Role switch { RefinedStorageComputer.Role => new RefinedStorageComputer(socket), string role => throw new ArgumentException($"Invalid role '{role}'!") }; AddComputerSocket(socket, pc); await Log(new LogMessage(LogSeverity.Info, WebSocketSource, $"[{socket.ConnectionInfo.Id}] Presented capability as {pc.GetType().Name}")).ConfigureAwait(false); } private void AddComputerSocket(IWebSocketConnection socket, RefinedStorageComputer pc) { _connections[socket.ConnectionInfo.Id] = pc; if (pc is not null) _rsSystem = pc; } private void RemoveComputerSocket(IWebSocketConnection socket) { if (!_connections.TryRemove(socket.ConnectionInfo.Id, out _)) throw new InvalidProgramException("Could not remove non-existing client!"); if (_rsSystem?.ConnectionInfo.Id == socket.ConnectionInfo.Id) _rsSystem = null; } private async Task SocketClosed(IWebSocketConnection socket) { RemoveComputerSocket(socket); await Log(new LogMessage(LogSeverity.Info, WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client disconnected!")).ConfigureAwait(false); } private async Task SocketOpened(IWebSocketConnection socket) => await Log(new LogMessage(LogSeverity.Info, WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client connected from {socket.ConnectionInfo.ClientIpAddress}:{socket.ConnectionInfo.ClientPort}!")).ConfigureAwait(false); private async Task DiscordMessageReceived(SocketMessage arg, int timeout = 10000) { if (arg is not SocketUserMessage message) return; if (message.Author.IsBot) return; if (!IsChannelWhitelisted(arg.Channel)) return; var cts = new CancellationTokenSource(timeout #if DEBUG * 1000 #endif ); if (IsCommand(message, out var argPos)) { var parameters = message.Content[argPos..].Split(WhiteSpace, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); _ = Task.Run(() => HandleCommand(message, parameters, cts.Token)); return; } await Log(new LogMessage(LogSeverity.Info, "Discord", $"[{arg.Author.Username}] {arg.Content}")).ConfigureAwait(false); await SendToAll(JsonConvert.SerializeObject(new TextMessage(arg))); } private Task HandleCommand(SocketUserMessage message, string[] parameters, CancellationToken ct) => parameters is { Length: > 0 } ? parameters[0].ToLower() switch { RefinedStorageComputer.Role => HandleRefinedStorageCommand(message, parameters[1..], ct), _ => message.ReplyAsync($"What the fuck do you mean by '{parameters[0]}'?") } : message.ReplyAsync($"You really think an empty command works?"); private Task HandleRefinedStorageCommand(SocketUserMessage message, string[] parameters, CancellationToken ct) => _rsSystem is null ? message.ReplyAsync("The Refined Storage system is currently unavailable!") : _rsSystem.HandleCommand(message, parameters, ct); private bool IsCommand(SocketUserMessage message, out int argPos) { argPos = 0; return message.HasStringPrefix(_config.Prefix, ref argPos); } private bool IsChannelWhitelisted(ISocketMessageChannel channel) => _whitelistedChannels.Contains(channel.Id); private async Task SendToAll(string message) { async Task SendToClient(KeyValuePair cp) { try { await cp.Value.Send(message); } catch (Exception e) { await Log(new LogMessage(LogSeverity.Warning, WebSocketSource, $"[{cp.Key}] Sending message failed!", e)).ConfigureAwait(false); } } await Task.WhenAll(_connections.Select(SendToClient).ToArray()); } private async Task Log(LogMessage msg) { lock (_logLock) Console.WriteLine(msg.ToString()); await Task.CompletedTask; } }