diff --git a/.gitignore b/.gitignore index 9491a2f..a2d8c4a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +MinecraftDiscordBot/config.json + # User-specific files *.rsuser *.suo diff --git a/MinecraftDiscordBot.sln b/MinecraftDiscordBot.sln index 8333b2c..7343fb9 100644 --- a/MinecraftDiscordBot.sln +++ b/MinecraftDiscordBot.sln @@ -3,7 +3,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32104.313 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinecraftDiscordBot", "MinecraftDiscordBot\MinecraftDiscordBot.csproj", "{46DBB810-17C0-45E9-BD39-2EE3FE101AC7}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CE65C879-794A-4695-B659-7376FE7DB5E3}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + MinecraftDiscordBot\bin\Debug\net6.0\config.json = MinecraftDiscordBot\bin\Debug\net6.0\config.json + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinecraftDiscordBot", "MinecraftDiscordBot\MinecraftDiscordBot.csproj", "{7A4A00B4-FDD1-461E-B925-1A7F1B185C4A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +17,10 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {46DBB810-17C0-45E9-BD39-2EE3FE101AC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {46DBB810-17C0-45E9-BD39-2EE3FE101AC7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {46DBB810-17C0-45E9-BD39-2EE3FE101AC7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {46DBB810-17C0-45E9-BD39-2EE3FE101AC7}.Release|Any CPU.Build.0 = Release|Any CPU + {7A4A00B4-FDD1-461E-B925-1A7F1B185C4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7A4A00B4-FDD1-461E-B925-1A7F1B185C4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7A4A00B4-FDD1-461E-B925-1A7F1B185C4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7A4A00B4-FDD1-461E-B925-1A7F1B185C4A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MinecraftDiscordBot/BotConfiguration.cs b/MinecraftDiscordBot/BotConfiguration.cs new file mode 100644 index 0000000..7ee1705 --- /dev/null +++ b/MinecraftDiscordBot/BotConfiguration.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace MinecraftDiscordBot; + +public class BotConfiguration { + [JsonProperty("token", Required = Required.Always)] + public string Token { get; set; } = default!; + [JsonProperty("port", Required = Required.Always)] + public int Port { get; set; } = default!; + [JsonProperty("channels", Required = Required.Always)] + public IEnumerable Channels { get; set; } = default!; +} diff --git a/MinecraftDiscordBot/Dockerfile b/MinecraftDiscordBot/Dockerfile index 8dfdc8c..28c3073 100644 --- a/MinecraftDiscordBot/Dockerfile +++ b/MinecraftDiscordBot/Dockerfile @@ -1,8 +1,7 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base WORKDIR /app -EXPOSE 80 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src diff --git a/MinecraftDiscordBot/Message.cs b/MinecraftDiscordBot/Message.cs new file mode 100644 index 0000000..a55929e --- /dev/null +++ b/MinecraftDiscordBot/Message.cs @@ -0,0 +1,21 @@ +using Discord.WebSocket; +using Newtonsoft.Json; + +namespace MinecraftDiscordBot; + +public abstract class Message { + [JsonProperty("type")] + public abstract string Type { get; } +} +public class TextMessage : Message { + public TextMessage(SocketMessage arg) : this(arg.Author.Username, arg.Content) { } + public TextMessage(string author, string content) { + Author = author; + Content = content; + } + public override string Type => "text"; + [JsonProperty("author")] + public string Author { get; } + [JsonProperty("message")] + public string Content { get; } +} \ No newline at end of file diff --git a/MinecraftDiscordBot/MinecraftDiscordBot.csproj b/MinecraftDiscordBot/MinecraftDiscordBot.csproj index f31c710..4c0a87b 100644 --- a/MinecraftDiscordBot/MinecraftDiscordBot.csproj +++ b/MinecraftDiscordBot/MinecraftDiscordBot.csproj @@ -1,14 +1,18 @@ - + + Exe net6.0 - enable enable + enable Linux + + + diff --git a/MinecraftDiscordBot/Program.cs b/MinecraftDiscordBot/Program.cs index 1760df1..7071d8c 100644 --- a/MinecraftDiscordBot/Program.cs +++ b/MinecraftDiscordBot/Program.cs @@ -1,6 +1,101 @@ -var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); +using Discord; +using Discord.Rest; +using Discord.WebSocket; +using Fleck; +using Newtonsoft.Json; +using System.Collections.Concurrent; -app.MapGet("/", () => "Hello World!"); +namespace MinecraftDiscordBot; -app.Run(); +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; + } +} diff --git a/MinecraftDiscordBot/Properties/launchSettings.json b/MinecraftDiscordBot/Properties/launchSettings.json index b6dda34..741a8ac 100644 --- a/MinecraftDiscordBot/Properties/launchSettings.json +++ b/MinecraftDiscordBot/Properties/launchSettings.json @@ -1,34 +1,10 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:25710", - "sslPort": 0 - } - }, "profiles": { "MinecraftDiscordBot": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:5114", - "dotnetRunMessages": true - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } + "commandName": "Project" }, "Docker": { - "commandName": "Docker", - "launchBrowser": true, - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", - "publishAllPorts": true + "commandName": "Docker" } } } \ No newline at end of file diff --git a/MinecraftDiscordBot/appsettings.Development.json b/MinecraftDiscordBot/appsettings.Development.json deleted file mode 100644 index 0c208ae..0000000 --- a/MinecraftDiscordBot/appsettings.Development.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - } -} diff --git a/MinecraftDiscordBot/appsettings.json b/MinecraftDiscordBot/appsettings.json deleted file mode 100644 index 10f68b8..0000000 --- a/MinecraftDiscordBot/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -}