using Discord; using Discord.Rest; using Discord.WebSocket; using Fleck; using Newtonsoft.Json; using System.Collections.Concurrent; 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(); public Program(BotConfiguration config) { _config = config; _client.Log += Log; _client.MessageReceived += MessageReceived; _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(); await VerifyTextChannels(); // 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) => await Log(new LogMessage(LogSeverity.Info, WebSocketSource, $"[{socket.ConnectionInfo.Id}] Received: {message}")).ConfigureAwait(false); private async Task SocketClosed(IWebSocketConnection socket) { if (!_connections.TryRemove(socket.ConnectionInfo.Id, out _)) throw new InvalidProgramException("Could not remove non-existing client!"); await Log(new LogMessage(LogSeverity.Info, WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client disconnected!")).ConfigureAwait(false); } private async Task SocketOpened(IWebSocketConnection socket) { if (!_connections.TryAdd(socket.ConnectionInfo.Id, socket)) throw new InvalidProgramException("Could not add already-existing client!"); await Log(new LogMessage(LogSeverity.Info, WebSocketSource, $"[{socket.ConnectionInfo.Id}] Client connected from {socket.ConnectionInfo.ClientIpAddress}:{socket.ConnectionInfo.ClientPort}!")).ConfigureAwait(false); } private async Task MessageReceived(SocketMessage arg) { if (arg.Author.IsBot) return; if (IsChannelWhitelisted(arg.Channel)) await Log(new LogMessage(LogSeverity.Info, "Discord", $"[{arg.Author.Username}] {arg.Content}")).ConfigureAwait(false); await SendToAll(JsonConvert.SerializeObject(new TextMessage(arg))); } 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; } }