From 78f9c3580009c137ec3896173dd4799dfd3a7110 Mon Sep 17 00:00:00 2001 From: breadbyte Date: Thu, 20 Oct 2022 20:05:36 +0800 Subject: [PATCH 01/15] Fix scripting system to provide more information in errors Also make log lines for scripting more uniform --- MinecraftClient/ChatBots/Script.cs | 2 +- MinecraftClient/Scripting/CSharpRunner.cs | 21 ++++++++++++++----- .../DynamicRun/Builder/CompileRunner.cs | 2 +- .../Scripting/DynamicRun/Builder/Compiler.cs | 8 +------ 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/MinecraftClient/ChatBots/Script.cs b/MinecraftClient/ChatBots/Script.cs index 3fc1acaf..c72be5a2 100644 --- a/MinecraftClient/ChatBots/Script.cs +++ b/MinecraftClient/ChatBots/Script.cs @@ -158,7 +158,7 @@ namespace MinecraftClient.ChatBots { try { - CSharpRunner.Run(this, lines, args, localVars); + CSharpRunner.Run(this, lines, args, localVars, scriptName: file!); } catch (CSharpException e) { diff --git a/MinecraftClient/Scripting/CSharpRunner.cs b/MinecraftClient/Scripting/CSharpRunner.cs index c3dd20f5..48f52a45 100644 --- a/MinecraftClient/Scripting/CSharpRunner.cs +++ b/MinecraftClient/Scripting/CSharpRunner.cs @@ -26,7 +26,7 @@ namespace MinecraftClient /// Set to false to compile and cache the script without launching it /// Thrown if an error occured /// Result of the execution, returned by the script - public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary? localVars, bool run = true) + public static object? Run(ChatBot apiHandler, string[] lines, string[] args, Dictionary? localVars, bool run = true, string scriptName = "Unknown Script") { //Script compatibility check for handling future versions differently if (lines.Length < 1 || lines[0] != "//MCCScript 1.0") @@ -102,13 +102,24 @@ namespace MinecraftClient "}}", }); + ConsoleIO.WriteLogLine($"[Script] Starting compilation for {scriptName}..."); + //Compile the C# class in memory using all the currently loaded assemblies var result = compiler.Compile(code, Guid.NewGuid().ToString()); //Process compile warnings and errors - if (result.Failures != null) - throw new CSharpException(CSErrorType.LoadError, - new InvalidOperationException(result.Failures[0].GetMessage())); + if (result.Failures != null) { + + ConsoleIO.WriteLogLine("[Script] Compilation failed with error(s):"); + + foreach (var failure in result.Failures) { + ConsoleIO.WriteLogLine($"[Script] Error in {scriptName}, line:col{failure.Location.GetMappedLineSpan()}: [{failure.Id}] {failure.GetMessage()}"); + } + + throw new CSharpException(CSErrorType.InvalidScript, new InvalidProgramException("Compilation failed due to error.")); + } + + ConsoleIO.WriteLogLine("[Script] Compilation done with no errors."); //Retrieve compiled assembly assembly = result.Assembly; @@ -385,7 +396,7 @@ namespace MinecraftClient { throw new CSharpException(CSErrorType.FileReadError, e); } - return CSharpRunner.Run(this, lines, args, localVars); + return CSharpRunner.Run(this, lines, args, localVars, scriptName: script); } } } \ No newline at end of file diff --git a/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs b/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs index 555622fc..8a1e492c 100644 --- a/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs +++ b/MinecraftClient/Scripting/DynamicRun/Builder/CompileRunner.cs @@ -24,7 +24,7 @@ namespace DynamicRun.Builder GC.WaitForPendingFinalizers(); } - ConsoleIO.WriteLogLine(assemblyLoadContextWeakRef.Item1.IsAlive ? "Script continues to run." : "Script finished!"); + ConsoleIO.WriteLogLine(assemblyLoadContextWeakRef.Item1.IsAlive ? "[Script] Script continues to run." : "[Script] Script finished!"); return assemblyLoadContextWeakRef.Item2; } diff --git a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs index 0ade609f..5da4f15d 100644 --- a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs +++ b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs @@ -23,15 +23,11 @@ namespace DynamicRun.Builder { public CompileResult Compile(string filepath, string fileName) { - ConsoleIO.WriteLogLine($"Starting compilation..."); - using var peStream = new MemoryStream(); var result = GenerateCode(filepath, fileName).Emit(peStream); if (!result.Success) { - ConsoleIO.WriteLogLine("Compilation done with error."); - var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); return new CompileResult() @@ -41,9 +37,7 @@ namespace DynamicRun.Builder Failures = failures.ToList() }; } - - ConsoleIO.WriteLogLine("Compilation done without any error."); - + peStream.Seek(0, SeekOrigin.Begin); return new CompileResult() From e006943535ead1342795ee7743239368adc701ce Mon Sep 17 00:00:00 2001 From: breadbyte Date: Fri, 21 Oct 2022 00:29:46 +0800 Subject: [PATCH 02/15] Fix externally referenced DLLs in scripts External DLL references weren't being used, this commit fixes them so they are used. --- MinecraftClient/Scripting/AssemblyResolver.cs | 37 +++++++++++++++++++ MinecraftClient/Scripting/CSharpRunner.cs | 2 +- .../Scripting/DynamicRun/Builder/Compiler.cs | 30 +++++++++++---- 3 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 MinecraftClient/Scripting/AssemblyResolver.cs diff --git a/MinecraftClient/Scripting/AssemblyResolver.cs b/MinecraftClient/Scripting/AssemblyResolver.cs new file mode 100644 index 00000000..3ecdd02c --- /dev/null +++ b/MinecraftClient/Scripting/AssemblyResolver.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MinecraftClient.Scripting; + +public static class AssemblyResolver { + private static Dictionary ScriptAssemblies = new(); + static AssemblyResolver() { + // Manually resolve assemblies that .NET can't resolve automatically. + AppDomain.CurrentDomain.AssemblyResolve += (sender, args) => + { + var asmReqName = new AssemblyName(args.Name); + + // Check the script-referenced assemblies if we have the DLL that is required. + foreach (var dll in ScriptAssemblies) + { + // If we have the assembly, load it. + if (asmReqName.FullName == dll.Key) + { + return Assembly.LoadFile(dll.Value); + } + } + + ConsoleIO.WriteLogLine($"[Script Error] Failed to resolve assembly {args.Name} (are you missing a DLL file?)"); + return null; + }; + } + + internal static void AddAssembly(string AssemblyFullName, string AssemblyPath) + { + if (ScriptAssemblies.ContainsKey(AssemblyFullName)) + return; + + ScriptAssemblies.Add(AssemblyFullName, AssemblyPath); + } +} \ No newline at end of file diff --git a/MinecraftClient/Scripting/CSharpRunner.cs b/MinecraftClient/Scripting/CSharpRunner.cs index 48f52a45..87857b5f 100644 --- a/MinecraftClient/Scripting/CSharpRunner.cs +++ b/MinecraftClient/Scripting/CSharpRunner.cs @@ -105,7 +105,7 @@ namespace MinecraftClient ConsoleIO.WriteLogLine($"[Script] Starting compilation for {scriptName}..."); //Compile the C# class in memory using all the currently loaded assemblies - var result = compiler.Compile(code, Guid.NewGuid().ToString()); + var result = compiler.Compile(code, Guid.NewGuid().ToString(), dlls); //Process compile warnings and errors if (result.Failures != null) { diff --git a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs index 5da4f15d..4bb02b07 100644 --- a/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs +++ b/MinecraftClient/Scripting/DynamicRun/Builder/Compiler.cs @@ -15,16 +15,17 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using MinecraftClient; +using MinecraftClient.Scripting; using SingleFileExtractor.Core; namespace DynamicRun.Builder { internal class Compiler { - public CompileResult Compile(string filepath, string fileName) + public CompileResult Compile(string filepath, string fileName, List additionalAssemblies) { using var peStream = new MemoryStream(); - var result = GenerateCode(filepath, fileName).Emit(peStream); + var result = GenerateCode(filepath, fileName, additionalAssemblies).Emit(peStream); if (!result.Success) { @@ -48,24 +49,37 @@ namespace DynamicRun.Builder }; } - private static CSharpCompilation GenerateCode(string sourceCode, string fileName) + private static CSharpCompilation GenerateCode(string sourceCode, string fileName, List additionalAssemblies) { var codeString = SourceText.From(sourceCode); var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp9); var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(codeString, options); + + var references = new List(); - var mods = Assembly.GetEntryAssembly()!.GetModules(); - + // Find if any additional assembly DLL exists in the base directory where the .exe exists. + foreach (var assembly in additionalAssemblies) + { + var dllPath = Path.Combine(AppContext.BaseDirectory, assembly); + if (File.Exists(dllPath)) + { + references.Add(MetadataReference.CreateFromFile(dllPath)); + // Store the reference in our Assembly Resolver for future reference. + AssemblyResolver.AddAssembly(Assembly.LoadFile(dllPath).FullName!, dllPath); + } + else + { + ConsoleIO.WriteLogLine($"[Script Error] {assembly} is defined in script, but cannot find DLL! Script may not run."); + } + } #pragma warning disable IL3000 // We determine if we are in a self-contained binary by checking specifically if the Assembly file path is null. var SystemPrivateCoreLib = typeof(object).Assembly.Location; // System.Private.CoreLib var SystemConsole = typeof(Console).Assembly.Location; // System.Console var MinecraftClientDll = typeof(Program).Assembly.Location; // The path to MinecraftClient.dll - - var references = new List(); - + // We're on a self-contained binary, so we need to extract the executable to get the assemblies. if (string.IsNullOrEmpty(MinecraftClientDll)) { From ec27ec53d784bcba2c3cc701d6aec6f0fa0f7908 Mon Sep 17 00:00:00 2001 From: Milutinke Date: Fri, 21 Oct 2022 02:29:47 +0200 Subject: [PATCH 03/15] Implemented the Discord Bridge Chat Bot. TODO: Translate --- MinecraftClient/ChatBots/DiscordBridge.cs | 218 ++++++++++++++++++++++ MinecraftClient/McClient.cs | 1 + MinecraftClient/MinecraftClient.csproj | 1 + MinecraftClient/Settings.cs | 9 +- 4 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 MinecraftClient/ChatBots/DiscordBridge.cs diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs new file mode 100644 index 00000000..9d3d329c --- /dev/null +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -0,0 +1,218 @@ +using DSharpPlus; +using System.Threading.Tasks; +using System; +using Tomlet.Attributes; +using System.Linq; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; +using Microsoft.Extensions.Logging; + +namespace MinecraftClient.ChatBots +{ + public class DiscordBridge : ChatBot + { + private DiscordClient? _client; + private DiscordChannel? _channel; + + public static Configs Config = new(); + + [TomlDoNotInlineObject] + public class Configs + { + [NonSerialized] + private const string BotName = "DiscordBridge"; + + public bool Enabled = false; + + public string Token = "your bot token here"; + + public ulong GuildId = 1018553894831403028L; + + public ulong ChannelId = 1018565295654326364L; + + public ulong[]? OwnerIds = new[] { 978757810781323276UL }; + + public bool IgnoreMessagesSentByBots = true; + } + + public override void Initialize() + { + Task.Run(async () => await MainAsync()); + } + + ~DiscordBridge() + { + Disconnect(); + } + + public override void OnUnload() + { + Disconnect(); + } + + private void Disconnect() + { + if (_client != null) + { + if (_channel != null) + _client.SendMessageAsync(_channel, new DiscordEmbedBuilder + { + Description = $"Disconnected from the MCC!", + Color = new DiscordColor(0xFF0000) + }).Wait(); + + _client.DisconnectAsync().Wait(); + } + } + + public override void GetText(string text) + { + if (_client == null || _channel == null) + return; + + text = GetVerbatim(text).Trim(); + + string message = ""; + string username = ""; + bool teleportRequest = false; + + if (IsPrivateMessage(text, ref message, ref username)) + message = $"**[Private Message]** {username}: `{message}`"; + else if (IsChatMessage(text, ref message, ref username)) + message = $"{username}: `{message}`"; + else if (IsTeleportRequest(text, ref username)) + { + message = $"A new Teleport Request from **{username}**"; + teleportRequest = true; + } + else message = text; + + SendMessageToDiscord(message, teleportRequest); + } + + private void SendMessageToDiscord(string message, bool teleportRequest = false) + { + if (_client == null || _channel == null) + return; + + if (teleportRequest) + { + var messageBuilder = new DiscordMessageBuilder() + .WithContent(message) + .AddComponents(new DiscordButtonComponent(ButtonStyle.Success, "accept_teleport", "Accept")) + .AddComponents(new DiscordButtonComponent(ButtonStyle.Danger, "deny_teleport", "Deny")); + + _client.SendMessageAsync(_channel, messageBuilder).Wait(); + return; + } + + _client.SendMessageAsync(_channel, message).Wait(); + } + + async Task MainAsync() + { + try + { + if (string.IsNullOrEmpty(Config.Token.Trim())) + { + LogToConsole(Translations.TryGet("bot.DiscordBridge.missing_token")); + UnloadBot(); + return; + } + + _client = new DiscordClient(new DiscordConfiguration() + { + Token = Config.Token.Trim(), + TokenType = TokenType.Bot, + AutoReconnect = true, + Intents = DiscordIntents.All, + MinimumLogLevel = LogLevel.None + }); + + try + { + await _client.GetGuildAsync(Config.GuildId); + } + catch (Exception e) + { + if (e is NotFoundException) + { + LogToConsole(Translations.TryGet("bot.DiscordBridge.guild_not_found", Config.GuildId)); + UnloadBot(); + return; + } + } + + try + { + _channel = await _client.GetChannelAsync(Config.ChannelId); + } + catch (Exception e) + { + if (e is NotFoundException) + { + LogToConsole(Translations.TryGet("bot.DiscordBridge.channel_not_found", Config.ChannelId)); + UnloadBot(); + return; + } + } + + _client.MessageCreated += async (source, e) => + { + if (Config.IgnoreMessagesSentByBots && e.Author.IsBot) + return; + + if (e.Guild.Id != Config.GuildId) + return; + + if (e.Channel.Id != Config.ChannelId) + return; + + if (!Config.OwnerIds!.Contains(e.Author.Id)) + return; + + string message = e.Message.Content.Trim(); + + if (message.StartsWith(".")) + { + await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); + message = message[1..]; + string? result = ""; + PerformInternalCommand(message, ref result); + result = string.IsNullOrEmpty(result) ? "-" : result; + await e.Message.DeleteOwnReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); + await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":white_check_mark:")); + await e.Message.RespondAsync($"The command was executed with the result:\n```{result}```"); + } + else SendText(message); + }; + + _client.ComponentInteractionCreated += async (s, e) => + { + if (!(e.Id.Equals("accept_teleport") || e.Id.Equals("deny_teleport"))) + return; + + string result = e.Id.Equals("accept_teleport") ? "Accepted :white_check_mark:" : "Denied :x:"; + SendText(e.Id.Equals("accept_teleport") ? "/tpaccept" : "/tpdeny"); + await e.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().WithContent(result)); + }; + + await _client.ConnectAsync(); + + await _client.SendMessageAsync(_channel, new DiscordEmbedBuilder + { + Description = $"Succesfully connected with the MCC!", + Color = new DiscordColor(0x00FF00) + }); + + await Task.Delay(-1); + } + catch (Exception e) + { + LogToConsole(Translations.TryGet("bot.DiscordBridge.unknown_error")); + LogToConsole(e); + return; + } + } + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index 677cad72..f829745d 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -264,6 +264,7 @@ namespace MinecraftClient if (Config.ChatBot.AutoRelog.Enabled) { BotLoad(new AutoRelog()); } if (Config.ChatBot.AutoRespond.Enabled) { BotLoad(new AutoRespond()); } if (Config.ChatBot.ChatLog.Enabled) { BotLoad(new ChatLog()); } + if (Config.ChatBot.DiscordBridge.Enabled) { BotLoad(new DiscordBridge()); } if (Config.ChatBot.Farmer.Enabled) { BotLoad(new Farmer()); } if (Config.ChatBot.FollowPlayer.Enabled) { BotLoad(new FollowPlayer()); } if (Config.ChatBot.HangmanGame.Enabled) { BotLoad(new HangmanGame()); } diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index b7f45d16..67e98941 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -35,6 +35,7 @@ + diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index 7880ead0..f536c53a 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -567,7 +567,7 @@ namespace MinecraftClient public enum ForgeConfigType { no, auto, force }; - public enum TerminalColorDepthType { bit_4, bit_8, bit_24}; + public enum TerminalColorDepthType { bit_4, bit_8, bit_24 }; } public struct AccountInfoConfig @@ -1090,6 +1090,13 @@ namespace MinecraftClient set { ChatBots.ChatLog.Config = value; ChatBots.ChatLog.Config.OnSettingUpdate(); } } + //[TomlPrecedingComment("$config.ChatBot.DiscordBridge")] + public ChatBots.DiscordBridge.Configs DiscordBridge + { + get { return ChatBots.DiscordBridge.Config; } + set { ChatBots.DiscordBridge.Config = value; } + } + [TomlPrecedingComment("$config.ChatBot.Farmer$")] public ChatBots.Farmer.Configs Farmer { From 148cb34878d606530c4236cadc1c0b396b8560a0 Mon Sep 17 00:00:00 2001 From: Milutinke Date: Fri, 21 Oct 2022 02:43:43 +0200 Subject: [PATCH 04/15] Removed unecessary check and made buttons for teleport request inline which looks way cleaner. --- MinecraftClient/ChatBots/DiscordBridge.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index 9d3d329c..54a364a3 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -31,8 +31,6 @@ namespace MinecraftClient.ChatBots public ulong ChannelId = 1018565295654326364L; public ulong[]? OwnerIds = new[] { 978757810781323276UL }; - - public bool IgnoreMessagesSentByBots = true; } public override void Initialize() @@ -98,9 +96,15 @@ namespace MinecraftClient.ChatBots if (teleportRequest) { var messageBuilder = new DiscordMessageBuilder() - .WithContent(message) - .AddComponents(new DiscordButtonComponent(ButtonStyle.Success, "accept_teleport", "Accept")) - .AddComponents(new DiscordButtonComponent(ButtonStyle.Danger, "deny_teleport", "Deny")); + .WithEmbed(new DiscordEmbedBuilder + { + Description = message, + Color = new DiscordColor(0x3399FF) + }) + .AddComponents(new DiscordComponent[]{ + new DiscordButtonComponent(ButtonStyle.Success, "accept_teleport", "Accept"), + new DiscordButtonComponent(ButtonStyle.Danger, "deny_teleport", "Deny") + }); _client.SendMessageAsync(_channel, messageBuilder).Wait(); return; @@ -159,9 +163,6 @@ namespace MinecraftClient.ChatBots _client.MessageCreated += async (source, e) => { - if (Config.IgnoreMessagesSentByBots && e.Author.IsBot) - return; - if (e.Guild.Id != Config.GuildId) return; @@ -177,9 +178,11 @@ namespace MinecraftClient.ChatBots { await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); message = message[1..]; + string? result = ""; PerformInternalCommand(message, ref result); result = string.IsNullOrEmpty(result) ? "-" : result; + await e.Message.DeleteOwnReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":white_check_mark:")); await e.Message.RespondAsync($"The command was executed with the result:\n```{result}```"); From 12b317644da4efdc2496585fbeb1b5028a61a9a5 Mon Sep 17 00:00:00 2001 From: Milutinke Date: Fri, 21 Oct 2022 12:25:33 +0200 Subject: [PATCH 05/15] Transalted and added detailed comments and instructions and a reference to the documentation article. Fully tested, ready for merge. --- MinecraftClient/ChatBots/DiscordBridge.cs | 25 +++++++++++++++-------- MinecraftClient/Resources/lang/en.ini | 16 +++++++++++++++ MinecraftClient/Settings.cs | 2 +- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index 54a364a3..ca6d425a 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -24,13 +24,22 @@ namespace MinecraftClient.ChatBots public bool Enabled = false; + [TomlInlineComment("$config.ChatBot.DiscordBridge.Token$")] public string Token = "your bot token here"; + [TomlInlineComment("$config.ChatBot.DiscordBridge.GuildId$")] public ulong GuildId = 1018553894831403028L; + [TomlInlineComment("$config.ChatBot.DiscordBridge.ChannelId$")] public ulong ChannelId = 1018565295654326364L; - public ulong[]? OwnerIds = new[] { 978757810781323276UL }; + [TomlInlineComment("$config.ChatBot.DiscordBridge.OwnersIds$")] + public ulong[]? OwnersIds = new[] { 978757810781323276UL }; + + [TomlPrecedingComment("$config.ChatBot.DiscordBridge.Formats$")] + public string PrivateMessageFormat = "**[Private Message]** {username}: {message}"; + public string PublicMessageFormat = "{username}: {message}"; + public string TeleportRequestMessageFormat = "A new Teleport Request from **{username}**!"; } public override void Initialize() @@ -55,7 +64,7 @@ namespace MinecraftClient.ChatBots if (_channel != null) _client.SendMessageAsync(_channel, new DiscordEmbedBuilder { - Description = $"Disconnected from the MCC!", + Description = Translations.TryGet("bot.DiscordBridge.disconnected"), Color = new DiscordColor(0xFF0000) }).Wait(); @@ -75,12 +84,12 @@ namespace MinecraftClient.ChatBots bool teleportRequest = false; if (IsPrivateMessage(text, ref message, ref username)) - message = $"**[Private Message]** {username}: `{message}`"; + message = Config.PrivateMessageFormat.Replace("{username}", username).Replace("{message}", message).Replace("{timestamp}", GetTimestamp()).Trim(); else if (IsChatMessage(text, ref message, ref username)) - message = $"{username}: `{message}`"; + message = Config.PublicMessageFormat.Replace("{username}", username).Replace("{message}", message).Replace("{timestamp}", GetTimestamp()).Trim(); else if (IsTeleportRequest(text, ref username)) { - message = $"A new Teleport Request from **{username}**"; + message = Config.TeleportRequestMessageFormat.Replace("{username}", username).Replace("{timestamp}", GetTimestamp()).Trim(); teleportRequest = true; } else message = text; @@ -169,7 +178,7 @@ namespace MinecraftClient.ChatBots if (e.Channel.Id != Config.ChannelId) return; - if (!Config.OwnerIds!.Contains(e.Author.Id)) + if (!Config.OwnersIds!.Contains(e.Author.Id)) return; string message = e.Message.Content.Trim(); @@ -185,7 +194,7 @@ namespace MinecraftClient.ChatBots await e.Message.DeleteOwnReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":white_check_mark:")); - await e.Message.RespondAsync($"The command was executed with the result:\n```{result}```"); + await e.Message.RespondAsync($"{Translations.TryGet("bot.DiscordBridge.command_executed")}:\n```{result}```"); } else SendText(message); }; @@ -204,7 +213,7 @@ namespace MinecraftClient.ChatBots await _client.SendMessageAsync(_channel, new DiscordEmbedBuilder { - Description = $"Succesfully connected with the MCC!", + Description = Translations.TryGet("bot.DiscordBridge.connected"), Color = new DiscordColor(0x00FF00) }); diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 3543b494..53318238 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -597,6 +597,14 @@ bot.autoRespond.match=match: {0}\nregex: {1}\naction: {2}\nactionPrivate: {3}\na # ChatLog bot.chatLog.invalid_file=Path '{0}' contains invalid characters. +# DiscordBridge +bot.DiscordBridge.command_executed=The command was executed with the result +bot.DiscordBridge.connected=Succesfully connected with MCC! +bot.DiscordBridge.missing_token=Please provide a valid token! +bot.DiscordBridge.guild_not_found=The provided guild/server with an id '{0}' has not been found! +bot.DiscordBridge.channel_not_found=The provided channel with an id '{0}' has not been found! +bot.DiscordBridge.unknown_error=An unknown error has occured! + # Mailer bot.mailer.init=Initializing Mailer with settings: bot.mailer.init.db= - Database File: {0} @@ -904,6 +912,14 @@ config.ChatBot.AutoRespond.Match_Colors=Do not remove colors from text (Note: Yo # ChatBot.ChatLog config.ChatBot.ChatLog=Logs chat messages in a file on disk. +# ChatBot.DiscordBridge +config.ChatBot.DiscordBridge=This bot allows you to send and recieve messages and commands via a Discord channel.\n# For Setup you can either use the documentation or read here (Documentation has images).\n# Documentation: https://mccteam.github.io/guide/chat-bots.html#discord-bridge\n# Setup:\n# First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA .\n# IMPORTANT: When creating a bot, you MUST ENABLE "Message Content Intent" and "Presence Intent" in order for bot to work! Also follow along carefully do not miss any steps!\n# When making a bot, copy the generated token and paste it here in "Token" field (tokens are important, keep them safe).\n# Copy the "Application ID" and go to: https://bit.ly/2Spn2Q3 .\n# Paste the id you have copied and check the "Administrator" field in permissions, then click on the link at the bottom.\n# This will open an invitation menu with your servers, choose the server you want to invite the bot on and invite him.\n# Once you've invited the bot, go to your Discord client and go to Settings -> Advanced and Enable "Developer Mode".\n# Exit the settings and right click on a server you have invited the bot to in the server list, then click "Copy ID", and paste the id here in "GuildId".\n# Then right click on a channel where you want to interact with the bot and again right click -> "Copy ID", pase the copied id here in "ChannelId".\n# And for the end, send a message in the channel, right click on your nick and again right click -> "Copy ID", then paste the id here in "OwnersIds".\n# How to use:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. +config.ChatBot.DiscordBridge.Token=Your Discord Bot token. +config.ChatBot.DiscordBridge.GuildId=The ID of a server/guild where you have invited the bot to. +config.ChatBot.DiscordBridge.ChannelId=The ID of a channel where you want to interact with the MCC using the bot. +config.ChatBot.DiscordBridge.OwnersIds=A list of IDs of people you want to be able to interact with the MCC using the bot. +config.ChatBot.DiscordBridge.Formats=Message formats\n# Words wrapped with { and } are going to be replaced during the code execution, do not change them!\n# For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time.\n# For Discord message formatting, check the following: https://bit.ly/3F8CUCm + # ChatBot.Farmer config.ChatBot.Farmer=Automatically farms crops for you (plants, breaks and bonemeals them).\n# Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat.\n# Usage: "/farmer start" command and "/farmer stop" command.\n# NOTE: This a newly added bot, it is not perfect and was only tested in 1.19.2, there are some minor issues like not being able to bonemeal carrots/potatoes sometimes.\n# or bot jumps onto the farm land and breaks it (this happens rarely but still happens). We are looking forward at improving this.\n# It is recommended to keep the farming area walled off and flat to avoid the bot jumping.\n# Also, if you have your farmland that is one block high, make it 2 or more blocks high so the bot does not fall through, as it can happen sometimes when the bot reconnects.\n# The bot also does not pickup all items if they fly off to the side, we have a plan to implement this option in the future as well as drop off and bonemeal refill chest(s). config.ChatBot.Farmer.Delay_Between_Tasks=Delay between tasks in seconds (Minimum 1 second) diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index f536c53a..fdae982b 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1090,7 +1090,7 @@ namespace MinecraftClient set { ChatBots.ChatLog.Config = value; ChatBots.ChatLog.Config.OnSettingUpdate(); } } - //[TomlPrecedingComment("$config.ChatBot.DiscordBridge")] + [TomlPrecedingComment("$config.ChatBot.DiscordBridge$")] public ChatBots.DiscordBridge.Configs DiscordBridge { get { return ChatBots.DiscordBridge.Config; } From d4f8dd0bfb96927d20dcb72b85b588ae313999e1 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Fri, 21 Oct 2022 19:05:49 +0800 Subject: [PATCH 06/15] Upgrade AutoFishing & Bug fix --- MinecraftClient/ChatBots/AutoFishing.cs | 82 ++++++++++--- MinecraftClient/ChatBots/DiscordBridge.cs | 14 ++- MinecraftClient/Inventory/Item.cs | 13 +- MinecraftClient/Program.cs | 2 +- MinecraftClient/Resources/lang/de.ini | Bin 70464 -> 73092 bytes MinecraftClient/Resources/lang/en.ini | 112 ++++++++++++------ MinecraftClient/Resources/lang/fr.ini | Bin 72428 -> 75050 bytes MinecraftClient/Resources/lang/ru.ini | Bin 69552 -> 72174 bytes MinecraftClient/Resources/lang/vi.ini | Bin 65850 -> 68472 bytes MinecraftClient/Resources/lang/zh-Hans.ini | Bin 73534 -> 76364 bytes MinecraftClient/Resources/lang/zh-Hant.ini | Bin 73614 -> 76446 bytes MinecraftClient/Scripting/AssemblyResolver.cs | 2 +- MinecraftClient/Scripting/ChatBot.cs | 27 +++-- MinecraftClient/Settings.cs | 8 +- README.md | 12 +- 15 files changed, 183 insertions(+), 89 deletions(-) diff --git a/MinecraftClient/ChatBots/AutoFishing.cs b/MinecraftClient/ChatBots/AutoFishing.cs index 3849f161..8e53992a 100644 --- a/MinecraftClient/ChatBots/AutoFishing.cs +++ b/MinecraftClient/ChatBots/AutoFishing.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; using MinecraftClient.Inventory; using MinecraftClient.Mapping; using Tomlet.Attributes; @@ -143,7 +145,8 @@ namespace MinecraftClient.ChatBots private Entity? fishingBobber; private Location LastPos = Location.Zero; private DateTime CaughtTime = DateTime.Now; - private int fishItemCounter = 10; + private int fishItemCounter = 15; + private Dictionary fishItemCnt = new(); private Entity fishItem = new(-1, EntityType.Item, Location.Zero); private int counter = 0; @@ -182,7 +185,7 @@ namespace MinecraftClient.ChatBots public string CommandHandler(string cmd, string[] args) { - if (args.Length > 0) + if (args.Length >= 1) { switch (args[0]) { @@ -206,13 +209,47 @@ namespace MinecraftClient.ChatBots } StopFishing(); return Translations.Get("bot.autoFish.stop"); + case "status": + if (args.Length >= 2) + { + if (args[1] == "clear") + { + fishItemCnt = new(); + return Translations.Get("bot.autoFish.status_clear"); + } + else + { + return GetCommandHelp("status"); + } + } + else + { + if (fishItemCnt.Count == 0) + return Translations.Get("bot.autoFish.status_info"); + + List> orderedList = fishItemCnt.OrderBy(x => x.Value).ToList(); + int maxLen = orderedList[^1].Value.ToString().Length; + StringBuilder sb = new(); + sb.Append(Translations.Get("bot.autoFish.status_info")); + foreach ((ItemType type, uint cnt) in orderedList) + { + sb.Append(Environment.NewLine); + + string cntStr = cnt.ToString(); + sb.Append(' ', maxLen - cntStr.Length).Append(cntStr); + sb.Append(" x "); + sb.Append(Item.GetTypeString(type)); + } + return sb.ToString(); + } case "help": return GetCommandHelp(args.Length >= 2 ? args[1] : ""); default: return GetHelp(); } } - else return GetHelp(); + else + return GetHelp(); } private void StartFishing() @@ -246,7 +283,7 @@ namespace MinecraftClient.ChatBots isFishing = false; state = FishingState.Stopping; } - fishItemCounter = 10; + fishItemCounter = 15; } private void UseFishRod() @@ -259,8 +296,9 @@ namespace MinecraftClient.ChatBots public override void Update() { - if (fishItemCounter < 10) + if (fishItemCounter < 15) ++fishItemCounter; + lock (stateLock) { switch (state) @@ -283,7 +321,7 @@ namespace MinecraftClient.ChatBots { if (castTimeout < 6000) castTimeout *= 2; // Exponential backoff - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.cast_timeout", castTimeout / 10.0)); + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.cast_timeout", castTimeout / 10.0)); counter = Settings.DoubleToTick(Config.Cast_Delay); state = FishingState.WaitingToCast; @@ -292,7 +330,7 @@ namespace MinecraftClient.ChatBots case FishingState.WaitingFishToBite: if (++counter > Settings.DoubleToTick(Config.Fishing_Timeout)) { - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.fishing_timeout")); + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.fishing_timeout")); counter = Settings.DoubleToTick(Config.Cast_Delay); state = FishingState.WaitingToCast; @@ -347,8 +385,8 @@ namespace MinecraftClient.ChatBots public override void OnEntitySpawn(Entity entity) { - if (fishItemCounter < 10 && entity.Type == EntityType.Item && Math.Abs(entity.Location.Y - LastPos.Y) < 2.0 && - Math.Abs(entity.Location.X - LastPos.X) < 0.1 && Math.Abs(entity.Location.Z - LastPos.Z) < 0.1) + if (fishItemCounter < 15 && entity.Type == EntityType.Item && Math.Abs(entity.Location.Y - LastPos.Y) < 2.2 && + Math.Abs(entity.Location.X - LastPos.X) < 0.12 && Math.Abs(entity.Location.Z - LastPos.Z) < 0.12) { if (Config.Log_Fish_Bobber) LogToConsole(string.Format("Item ({0}) spawn at {1}, distance = {2:0.00}", entity.ID, entity.Location, entity.Location.Distance(LastPos))); @@ -359,9 +397,9 @@ namespace MinecraftClient.ChatBots if (Config.Log_Fish_Bobber) LogToConsole(string.Format("FishingBobber spawn at {0}, distance = {1:0.00}", entity.Location, GetCurrentLocation().Distance(entity.Location))); - fishItemCounter = 10; + fishItemCounter = 15; - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.throw")); + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.throw")); lock (stateLock) { fishingBobber = entity; @@ -377,7 +415,7 @@ namespace MinecraftClient.ChatBots public override void OnEntityDespawn(Entity entity) { - if (entity != null && entity.Type == EntityType.FishingBobber && entity.ID == fishingBobber!.ID) + if (entity != null && fishingBobber != null && entity.Type == EntityType.FishingBobber && entity.ID == fishingBobber!.ID) { if (Config.Log_Fish_Bobber) LogToConsole(string.Format("FishingBobber despawn at {0}", entity.Location)); @@ -431,10 +469,15 @@ namespace MinecraftClient.ChatBots public override void OnEntityMetadata(Entity entity, Dictionary metadata) { - if (fishItemCounter < 10 && entity.ID == fishItem.ID && metadata.TryGetValue(8, out object? item)) + if (fishItemCounter < 15 && entity.ID == fishItem.ID && metadata.TryGetValue(8, out object? itemObj)) { - LogToConsole(Translations.Get("bot.autoFish.got", ((Item)item!).ToFullString())); - fishItemCounter = 10; + fishItemCounter = 15; + Item item = (Item)itemObj!; + LogToConsole(Translations.Get("bot.autoFish.got", item.ToFullString())); + if (fishItemCnt.ContainsKey(item.Type)) + fishItemCnt[item.Type] += (uint)item.Count; + else + fishItemCnt.Add(item.Type, (uint)item.Count); } } @@ -471,10 +514,10 @@ namespace MinecraftClient.ChatBots { ++fishCount; if (Config.Enable_Move && Config.Movements.Length > 0) - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.caught_at", + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.caught_at", fishingBobber!.Location.X, fishingBobber!.Location.Y, fishingBobber!.Location.Z, fishCount)); else - LogToConsole(GetTimestamp() + ": " + Translations.Get("bot.autoFish.caught", fishCount)); + LogToConsole(GetShortTimestamp() + ": " + Translations.Get("bot.autoFish.caught", fishCount)); lock (stateLock) { @@ -580,7 +623,7 @@ namespace MinecraftClient.ChatBots private static string GetHelp() { - return Translations.Get("bot.autoFish.available_cmd", "start, stop, help"); + return Translations.Get("bot.autoFish.available_cmd", "start, stop, status, help"); } private string GetCommandHelp(string cmd) @@ -590,8 +633,9 @@ namespace MinecraftClient.ChatBots #pragma warning disable format // @formatter:off "start" => Translations.Get("bot.autoFish.help.start"), "stop" => Translations.Get("bot.autoFish.help.stop"), + "status" => Translations.Get("bot.autoFish.help.status"), "help" => Translations.Get("bot.autoFish.help.help"), - _ => GetHelp(), + _ => GetHelp(), #pragma warning restore format // @formatter:on }; } diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index ca6d425a..ef97820b 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -1,11 +1,11 @@ -using DSharpPlus; -using System.Threading.Tasks; -using System; -using Tomlet.Attributes; +using System; using System.Linq; +using System.Threading.Tasks; +using DSharpPlus; using DSharpPlus.Entities; using DSharpPlus.Exceptions; using Microsoft.Extensions.Logging; +using Tomlet.Attributes; namespace MinecraftClient.ChatBots { @@ -34,12 +34,14 @@ namespace MinecraftClient.ChatBots public ulong ChannelId = 1018565295654326364L; [TomlInlineComment("$config.ChatBot.DiscordBridge.OwnersIds$")] - public ulong[]? OwnersIds = new[] { 978757810781323276UL }; + public ulong[] OwnersIds = new[] { 978757810781323276UL }; [TomlPrecedingComment("$config.ChatBot.DiscordBridge.Formats$")] public string PrivateMessageFormat = "**[Private Message]** {username}: {message}"; public string PublicMessageFormat = "{username}: {message}"; public string TeleportRequestMessageFormat = "A new Teleport Request from **{username}**!"; + + public void OnSettingUpdate() { } } public override void Initialize() @@ -178,7 +180,7 @@ namespace MinecraftClient.ChatBots if (e.Channel.Id != Config.ChannelId) return; - if (!Config.OwnersIds!.Contains(e.Author.Id)) + if (!Config.OwnersIds.Contains(e.Author.Id)) return; string message = e.Message.Content.Trim(); diff --git a/MinecraftClient/Inventory/Item.cs b/MinecraftClient/Inventory/Item.cs index fff591e5..5b4b7941 100644 --- a/MinecraftClient/Inventory/Item.cs +++ b/MinecraftClient/Inventory/Item.cs @@ -113,17 +113,22 @@ namespace MinecraftClient.Inventory } } - public string GetTypeString() + public static string GetTypeString(ItemType type) { - string type = Type.ToString(); - string type_renamed = type.ToUnderscoreCase(); + string type_str = type.ToString(); + string type_renamed = type_str.ToUnderscoreCase(); string? res1 = Protocol.ChatParser.TranslateString("item.minecraft." + type_renamed); if (!string.IsNullOrEmpty(res1)) return res1; string? res2 = Protocol.ChatParser.TranslateString("block.minecraft." + type_renamed); if (!string.IsNullOrEmpty(res2)) return res2; - return type; + return type_str; + } + + public string GetTypeString() + { + return GetTypeString(Type); } public string ToFullString() diff --git a/MinecraftClient/Program.cs b/MinecraftClient/Program.cs index 0eabdae0..5f7c5bf0 100644 --- a/MinecraftClient/Program.cs +++ b/MinecraftClient/Program.cs @@ -691,7 +691,7 @@ namespace MinecraftClient public static void ReloadSettings() { if(Settings.LoadFromFile(settingsIniPath).Item1) - ConsoleIO.WriteLine(Translations.TryGet("config.loading", settingsIniPath)); + ConsoleIO.WriteLine(Translations.TryGet("config.load", settingsIniPath)); } /// diff --git a/MinecraftClient/Resources/lang/de.ini b/MinecraftClient/Resources/lang/de.ini index cc7b0f4eb6d1319868fb050fda57579299a99941..b8eb67895321902a570b848d2cb469205fd4bea2 100644 GIT binary patch delta 1658 zcma)7Ur1A76#qV*Q|C4|b-ON^e>TO`A?~S{M6_jSS%f}BFKTY-a&D#*?L{+s@Fh}C zc`YcgB2uB$s}^K2DtUojcE6$;HQ# zfo2mdXUcVMVk7lS<$_o^VF`>XHls+nrQJ*&Me3 zLk5<5wa_st7cxfMrICI*p>vG$nx4c7Gh@x6I=_h=KmaW55d>64av;xhsnsF^tHhIf zlt4Z{?f9<3dHur;PI6~&Z_8dTgZX740WPGV7LlOnLP#03Ik@Cq13^*BrEWk9QUZGJ z#N;r}Yp@CD`Yd+MkIhvf-*%jX74ptS8Jzp*f z*oIPUssUs3aUd+ixdlB~7H)xbQ9HC)Wa4%hC2v;JRL!@}5fTdTNKS)|mf zOi+K~m~UDz-$On{l0q2&Df$Ohn0LXQs4mpxf5sdW)yV!U8b~2(fBaWkYA{R%h@WY+ Pf_Fty zlW?>Olg_vflOU}clW?>Olg_vblMcBull+?slW@8Yvw*pV29x}o3X>4Q7_(HscmtF0 z!-A7U&K9$L&N>Z~THFe=+}oT1lQ7;SvxMI80h4OR7Lz*15R>HF7Lz>34zt$A!~&BF z$|jTc%NDbA%7O!vs*Mn{rr`VmlU(E?v-sj*1e1K{w38C;60<(&QY^QQ76JAK;Zc0w diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 53318238..0971529e 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -477,20 +477,24 @@ cmd.useitem.use=Used an item # ChatBots. Naming style: bot.. # Alerts +botname.Alerts=Alerts bot.alerts.start_rain=§cWeather change: It is raining now.§r bot.alerts.end_rain=§cWeather change: It is no longer raining.§r bot.alerts.start_thunderstorm=§cWeather change: It is a thunderstorm.§r bot.alerts.end_thunderstorm=§cWeather change: It is no longer a thunderstorm.§r # Anti AFK +botname.AntiAFK=AntiAFK bot.antiafk.not_using_terrain_handling=The terrain handling is not enabled in the settings of the client, enable it if you want to use it with this bot. Using alternative (command) method. bot.antiafk.swapping=The time range begins with a bigger value, swapped them around. bot.antiafk.invalid_walk_range=Invalid walk range provided, must be a positive integer greater than 0, using default value of 5! # AutoAttack +botname.AutoAttack=AutoAttack bot.autoAttack.invalidcooldown=Attack cooldown value cannot be smaller than 0. # AutoCraft +botname.AutoCraft=AutoCraft bot.autoCraft.cmd=Auto-crafting ChatBot command bot.autoCraft.alias=Auto-crafting ChatBot command alias bot.autoCraft.cmd.list=Total {0} recipes loaded: {1} @@ -523,6 +527,7 @@ bot.autocraft.invaild_slots=The number of slots does not match and has been adju bot.autocraft.invaild_invaild_result=Invalid result item! # AutoDig +botname.AutoDig=AutoDig bot.autodig.start_delay=Digging will start in {0:0.0} second(s). bot.autodig.dig_timeout=Digging block timeout, retry. bot.autodig.not_allow=The block currently pointed to is not in the allowed list. @@ -535,6 +540,7 @@ bot.autodig.help.stop=Deactivate the automatic digging bot. bot.autodig.help.help=Get the command description. Usage: /digbot help # AutoDrop +botname.AutoDrop=AutoDrop bot.autoDrop.cmd=AutoDrop ChatBot command bot.autoDrop.alias=AutoDrop ChatBot command alias bot.autoDrop.on=AutoDrop enabled @@ -550,7 +556,11 @@ bot.autoDrop.unknown_mode=Unknwon mode. Available modes: Include, Exclude, Every bot.autoDrop.no_mode=Cannot read drop mode from config. Using include mode. bot.autoDrop.no_inventory=Cannot find inventory {0}! +# AutoEat +botname.AutoEat=AutoEat + # AutoFish +botname.AutoFishing=AutoFishing bot.autoFish.no_inv_handle=Inventory handling is not enabled. Cannot check rod durability and switch rods. bot.autoFish.start_at=Fishing will start in {0:0.0} second(s). bot.autoFish.throw=Casting successfully. @@ -563,15 +573,20 @@ bot.autoFish.fishing_timeout=Fishing timeout, will soon re-cast. bot.autoFish.cast_timeout=Casting timeout and will soon retry. (Timeout increased to {0:0.0} sec). bot.autoFish.update_lookat=Update yaw = {0:0.00}, pitch = {1:0.00}. bot.autoFish.switch=Switch to the rod in slot {0}, durability {1}/64. +bot.autoFish.status_info=All items obtained from fishing (not entirely accurate): +# AutoFish cmd bot.autoFish.cmd=Auto-Fishing ChatBot command bot.autoFish.available_cmd=Available commands: {0}. Use /fish help for more information. bot.autoFish.start=Start auto-fishing. bot.autoFish.stop=Stop auto-fishing. +bot.autoFish.status_clear=The record of the obtained items has been cleared. bot.autoFish.help.start=Start auto-fishing. bot.autoFish.help.stop=Stop auto-fishing. +bot.autoFish.help.status=List all obtained items. Or use "/fish status clear" to clear the list. bot.autoFish.help.help=Get the command description. Usage: /fish help # AutoRelog +botname.AutoRelog=AutoRelog bot.autoRelog.launch=Launching with {0} reconnection attempts bot.autoRelog.no_kick_msg=Initializing without a kick message file bot.autoRelog.loading=Loading messages from file: {0} @@ -586,6 +601,7 @@ bot.autoRelog.reconnect_ignore=Message not containing any defined keywords. Igno bot.autoRelog.wait=Waiting {0:0.000} seconds before reconnecting... # AutoRespond +botname.AutoRespond=AutoRespond bot.autoRespond.loading=Loading matches from '{0}' bot.autoRespond.file_not_found=File not found: '{0}' bot.autoRespond.loaded_match=Loaded match:\n{0} @@ -595,9 +611,11 @@ bot.autoRespond.match_run=Running action: {0} bot.autoRespond.match=match: {0}\nregex: {1}\naction: {2}\nactionPrivate: {3}\nactionOther: {4}\nownersOnly: {5}\ncooldown: {6} # ChatLog +botname.ChatLog=ChatLog bot.chatLog.invalid_file=Path '{0}' contains invalid characters. # DiscordBridge +botname.DiscordBridge=DiscordBridge bot.DiscordBridge.command_executed=The command was executed with the result bot.DiscordBridge.connected=Succesfully connected with MCC! bot.DiscordBridge.missing_token=Please provide a valid token! @@ -605,7 +623,51 @@ bot.DiscordBridge.guild_not_found=The provided guild/server with an id '{0}' has bot.DiscordBridge.channel_not_found=The provided channel with an id '{0}' has not been found! bot.DiscordBridge.unknown_error=An unknown error has occured! +# Farmer +botname.Farmer=Farmer +bot.farmer.desc=Farming bot +bot.farmer.not_implemented=Not implemented bellow 1.13! +bot.farmer.already_stopped=The bot has already stopped farming! +bot.farmer.stopping=Stoping farming, this might take a second... +bot.farmer.stopped=Stopped farming! +bot.farmer.already_running=The bot is already farming! +bot.farmer.invalid_crop_type=Invalid crop type provided (Types which you can use: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat)! +bot.farmer.warining_invalid_parameter=Invalid parameter "{0}" provided (Use format: "key:value")! +bot.farmer.invalid_radius=Invalid radius provided, you must provide a valid integer number greater than 0! +bot.farmer.warining_force_unsafe=You have enabled un-safe movement, the bot might get hurt! +bot.farmer.warining_allow_teleport=You have enabled teleporting, this might get your bot account kicked and in the worst case scenario banned! Use with caution! +bot.farmer.started=Started farming! +bot.farmer.crop_type=Crop type +bot.farmer.radius=Radius +bot.farmer.needs_terrain=The Farmer bot needs Terrain Handling in order to work, please enable it! +bot.farmer.needs_inventory=The Farmer bot needs Inventory Handling in order to work, please enable it! + +# Follow player +botname.FollowPlayer=FollowPlayer +cmd.follow.desc=Makes the bot follow a specified player +cmd.follow.usage=follow [-f] (Use -f to enable un-safe walking) +cmd.follow.already_stopped=Already stopped +cmd.follow.stopping=Stopped following! +cmd.follow.invalid_name=Invalid or empty player name provided! +cmd.follow.invalid_player=The specified player is either not connected out out of the range! +cmd.follow.cant_reach_player=Can not reach the player, he is either in chunks that are not loaded, too far away or not reachable by a bot due to obstacles like gaps or water bodies! +cmd.follow.already_following=Already following {0}! +cmd.follow.switched=Switched to following {0}! +cmd.follow.started=Started following {0}! +cmd.follow.unsafe_enabled=Enabled us-safe walking (NOTE: The bot might die or get hurt!) +cmd.follow.note=NOTE: The bot is quite slow, you need to walk slowly and at a close distance for it to be able to keep up, kinda like when you make animals follow you by holding food in your hand. This is a limitation due to a pathfinding algorithm, we are working to get a better one. +cmd.follow.player_came_to_the_range=The player {0} came back to the range! +cmd.follow.resuming=Resuming to follow! +cmd.follow.player_left_the_range=The player {0} has left the range! +cmd.follow.pausing=Pausing! +cmd.follow.player_left=The player {0} left the server! +cmd.follow.stopping=Stopped! + +# HangmanGame +botname.HangmanGame=HangmanGame + # Mailer +botname.Mailer=Mailer bot.mailer.init=Initializing Mailer with settings: bot.mailer.init.db= - Database File: {0} bot.mailer.init.ignore= - Ignore List: {0} @@ -638,6 +700,7 @@ bot.mailer.cmd.ignore.invalid=Missing or invalid name. Usage: {0} bot.mailer.cmd.help=See usage # Maps +botname.Map=Map bot.map.cmd.desc=Render maps (item maps) bot.map.cmd.not_found=A map with id '{0}' does not exists! bot.map.cmd.invalid_id=Invalid ID provided, must be a number! @@ -649,57 +712,28 @@ bot.map.failed_to_render=Failed to render the map with id: '{0}' bot.map.list_item=- Map id: {0} (Last Updated: {1}) bot.map.scale=The size of the map is reduced from ({0}x{1}) to ({2}x{3}) due to the size limitation of the current terminal. +# PlayerListLogger +botname.PlayerListLogger=PlayerListLogger + +# RemoteControl +botname.RemoteControl=RemoteControl + # ReplayCapture +botname.ReplayCapture=ReplayCapture bot.replayCapture.cmd=replay command bot.replayCapture.created=Replay file created. bot.replayCapture.stopped=Record stopped. bot.replayCapture.restart=Record was stopped. Restart the program to start another record. -# Farmer -bot.farmer.desc=Farming bot -bot.farmer.not_implemented=Not implemented bellow 1.13! -bot.farmer.already_stopped=The bot has already stopped farming! -bot.farmer.stopping=Stoping farming, this might take a second... -bot.farmer.stopped=Stopped farming! -bot.farmer.already_running=The bot is already farming! -bot.farmer.invalid_crop_type=Invalid crop type provided (Types which you can use: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat)! -bot.farmer.warining_invalid_parameter=Invalid parameter "{0}" provided (Use format: "key:value")! -bot.farmer.invalid_radius=Invalid radius provided, you must provide a valid integer number greater than 0! -bot.farmer.warining_force_unsafe=You have enabled un-safe movement, the bot might get hurt! -bot.farmer.warining_allow_teleport=You have enabled teleporting, this might get your bot account kicked and in the worst case scenario banned! Use with caution! -bot.farmer.started=Started farming! -bot.farmer.crop_type=Crop type -bot.farmer.radius=Radius -bot.farmer.needs_terrain=The Farmer bot needs Terrain Handling in order to work, please enable it! -bot.farmer.needs_inventory=The Farmer bot needs Inventory Handling in order to work, please enable it! - -# Follow player -cmd.follow.desc=Makes the bot follow a specified player -cmd.follow.usage=follow [-f] (Use -f to enable un-safe walking) -cmd.follow.already_stopped=Already stopped -cmd.follow.stopping=Stopped following! -cmd.follow.invalid_name=Invalid or empty player name provided! -cmd.follow.invalid_player=The specified player is either not connected out out of the range! -cmd.follow.cant_reach_player=Can not reach the player, he is either in chunks that are not loaded, too far away or not reachable by a bot due to obstacles like gaps or water bodies! -cmd.follow.already_following=Already following {0}! -cmd.follow.switched=Switched to following {0}! -cmd.follow.started=Started following {0}! -cmd.follow.unsafe_enabled=Enabled us-safe walking (NOTE: The bot might die or get hurt!) -cmd.follow.note=NOTE: The bot is quite slow, you need to walk slowly and at a close distance for it to be able to keep up, kinda like when you make animals follow you by holding food in your hand. This is a limitation due to a pathfinding algorithm, we are working to get a better one. -cmd.follow.player_came_to_the_range=The player {0} came back to the range! -cmd.follow.resuming=Resuming to follow! -cmd.follow.player_left_the_range=The player {0} has left the range! -cmd.follow.pausing=Pausing! -cmd.follow.player_left=The player {0} left the server! -cmd.follow.stopping=Stopped! - # Script +botname.Script=Script bot.script.not_found=§8[MCC] [{0}] Cannot find script file: {1} bot.script.file_not_found=File not found: '{0}' bot.script.fail=Script '{0}' failed to run ({1}). bot.script.pm.loaded=Script '{0}' loaded. # ScriptScheduler +botname.ScriptScheduler=ScriptScheduler bot.scriptScheduler.loaded_task=Loaded task:\n{0} bot.scriptScheduler.no_trigger=This task will never trigger:\n{0} bot.scriptScheduler.no_action=No action for task:\n{0} @@ -709,12 +743,12 @@ bot.scriptScheduler.running_login=Login / Running action: {0} bot.scriptScheduler.task=triggeronfirstlogin: {0}\n triggeronlogin: {1}\n triggerontime: {2}\n triggeroninterval: {3}\n timevalue: {4}\n timeinterval: {5}\n action: {6} # TestBot +botname.TestBot=TestBot bot.testBot.told=Bot: {0} told me : {1} bot.testBot.said=Bot: {0} said : {1} [config] - config.load=Settings have been loaded from {0} config.load.fail=§cFailed to load settings:§r config.write.fail=§cFailed to write to settings file {0}§r diff --git a/MinecraftClient/Resources/lang/fr.ini b/MinecraftClient/Resources/lang/fr.ini index 3ab0cab983214dd5238fa908a84f24142dbce169..307b70fd060c8c6c634238cdad249f9dd304862c 100644 GIT binary patch delta 1648 zcma)6T}YE*6n@{iHrZx=bM8+Y+Y(#Wl-6CuM6C?1&`P8`bvl1ax3NvSOZ1|vF3e+n zr9^dAbfK%4bP7^NNO)zDnXp15R4NL)5WC6F_ib+e;F7Vw_nhaP=REIoz8}w}PbQ_I z-h5cHt29C4CVqU?krv`1HTd?zxKj&X?HmM)GPKPY@sI}6D#n`<8LT`1+A(lNbnI_q zGli8#!yd$v8b~iSQYS1onqvQ#OZDt>bG1Y6!BTC+4Rvc;(Dm(y$lZ_n*cmAx#n>i@ zO^Qh=IRk(C%rMb6wT|I`NvR%s!aDY#znsQTi{(_yTjW#*hXy(5zOQM_X+I0l4|&uO2pn6<-qq;!3?27nf?@#43adWATFFHh@?c)e3^=x zc`lWvLiJEayoDp38sT6+2Xolw^RSF{4XIM#=ZMS^B<&=CY6+?~VkM_oGly7kz;>+b z#W7njqZv=?QI-aH8@59CNJv1d*U|N;yBit!;U-3lMt#ZvVg-g+J$9ym(CE=PBd?lf zi$hnAso3ghraT@=*HAM|4scL8Chd8xVh3KWq!<(WpFk=?;j>7si8w($@u=gZNU(+x4j zcY|#c#|+@72ZyUcyq8?T8lGqkd&~;SnK$8;w2)SK(PeV%rfI#Xca(A~jCW;Q|BC{L zOas3?1~AO(;@4`;q*}N>t7C<;OA_G+-lAm!?%*pgH`Mfy(4UEQeTlr;(bs^gV8*CYbt=aQq-1-~;FBOdhk$ ztWxlGbEZuLp*1g}D+KfUP!K0{&Sx{loJ11Wa6F6JnC#0t>4tr%LJfswD?u{|AnT$D zzAZdvw-;;e#vQE=#cMhG9in`vl-hE42x7MaQnhY8zD`1OZOKwkN9~3_FXJCdsO@mF y%AsE@!8eOKVD{AsN-5u(FHM4T~ZIf2`?DS2}f@)sG^JendfK zPjP~R6q-^bZGAy2gb1R*WOT9qB-O77f^I~_y1wVkbeRn~oO9UsyqD*F-{(CGU&weT zGQvlS@U<{o39=gIr_0T}%*R}GH?klao~SyC<9QNx)@Q4Ag!`C>)f-l|$#&vM(Y!^t zUTMP%w^c;l+c>sX<)ph<4K}S%cmNeu&hakttYN$9t{0irmBkZ$6tIDQ7PB$~^5f1$ z1v}b4sL6ia9kX{lZ)TNc{Rh(n+M2V~v z46{m+1!e7%7VA^lMRoWFr!|HQRCfziSP%Dp2g1FkIsdO$uOqQYK7A1od8*<{Z+1~C zj~3#xi|$6Yhn_Cn_S#Vxvnj(9DyOdaja~AiE#?p#V{g)MHfEz0tD(zdh|t(_>~B-i z9@^90?`f{hH}m`vft$^IPNqIDC(F~gh3AgIf0?;M+h(D} zQeXbi8eX+^75wQpoi`;-DoEU=HnLy{Y#PlX{i{eL0A)-VrBKF=yh#i^Y5>k@QpFUN7` Kjaw%EMfwfDVYtEo delta 258 zcmV+d0sa2&vjni81h9xJvkWXSHj~sh5VLY(NCC4ZWKu4Znv(RB1e6k!#FNmIY?lg? z$dvSxY?mCemgo?(45#n`lWdm?lBx=`HmOVolfbVMlWejSlU}g~vly_>0+T$mvXhLo z5R-7I8k118Mw8B-3X|5fD3ffL3bQb^oCcF@mkN_GwFHw;y$q8COc0Y?widHEyo>{r z=)a1SM9LPke9CqWlf2jplRnr8vmn@%0h4UnB$Mph9<$!s;sKLt!WNS{!w|Ew!sr5% z3dSar_Qw{pbjE@MlRAnJlhE7-lN{d;vj*Mb0ka(6o&=LN&#vS?P~ML|V0YF74-gM`2=vu#aFH+AdhrXRY>x+$M9 z$coY$5mA$`E19_{GKEATDk37ID1CR*vx|Msa6> zW--cK*{H8bLui}GcW+-WU{6K%RLaFF(O@@YSH+xk`HgaT*a2F#{7S{oh0}bLx{i5p z`<)SwPGsTMiMOWFJtF6zvOy~9U?pr5o;6u;wP`@bbPSttdRT`;?IMnxe68*eeKf-N zQHQ=(vAM;-gQr%dC*<5uIsI%6<@KVW-PC+RkIvQmNw=SHVYFbMq zgKP#%VY>+!BvK0LbDu4!59zV2Ekgx5zMAp8NraQYJKJ7pmIeqQ!S}NO2`7oXhmzRk zbXJx}cT@P3&{=?0(~gT2al_G`kLB0GrbN6;7O;vn67zgGLn-JxV`vVQr;ZC7JzAl1 z>iU`^i{!jv;BBELhKzLBWx~s-e*Wai6_AfK_7FUX!D!~b zWYKl9Baog=1DOS z3Nus3M%E5n)T(ySAES!*vUqv)g9g>nY?FuRyJf2-Og5^bmg(vlI+kZ*#r?t=iGKbR z%R%J9Y<}ea@hNbQWX=4iwb(LZ!$7|dFTIr=ku1kmjwJe zCPM7djT`c<)PBjK_0&>3?)94a+@6R!-RIH;!G?#u1zgj+Ks(O(Wh9<7NX6s>37G%5 z8}s_pkk+r~vBw(=lQhZk!D^hag%}((7Vnxt4;ve{7P^6)g03>zRkm6<0+l*nHMX-b1EfT6gBuZexp{RY7o&e;F} delta 241 zcmVj1zK< diff --git a/MinecraftClient/Resources/lang/zh-Hans.ini b/MinecraftClient/Resources/lang/zh-Hans.ini index fbef68787d016992e84f86661f58ff9f80526fe2..a4ac3b53747f4fc05f43fbdbe61659e3cac4b9e1 100644 GIT binary patch delta 2132 zcma)7ZERCz6n<|zW>;D_p=;NUZp~$bQLr{jB>rfua}-4#t~67&v3lE%wqa{$OD9{F ztkW7u+y~pil^8=DMUjLp>u#b6F(~{n{*X8l6Z}XH!mJvZ)#h(J7~7ytQ) zjld^bEnIe~o(bS~2)BBfgTI7}nkWffoiJtGEJlpCw*xO|q2Ey@y7s=wLBLV-7==SH z5O!5_Q4k#Um6;t1JAkWKOgS#>+(NnZ;iHATz!*i4V-#5}d8emDdt{i?OD#h9Hi(uE z@(Qtu>FC!S;;;bl*B?6g^PUo3AJo85EK!kngWqDCl{Rv3^mC`eRpmhg6Wkvt7Yzeu zW#0SeziYwzu1V~O*U205u80&v#4u@Ls`eu<8isF`yCI&agR%D%iu^|Rs2LXCS84<% zpun<_ywpN@!cwqCcWsy)!OKP-93CBPisfv@c%oVU^j=Ks7!%pU>h>t7j75gguamdV zfGMtlYbQ44a!OXpIDIy8I6eF^ES;qLV#TmR3%-?@@|rl%GfD!u z!2&cu92DX&(+^~PA?S-J2$lBLY{32T9f$7p+;qdW54%TN7;!JYilAQ2cn{*QAMvTS zYwKLx+9t8LU~f`6{x7P9?e<}H*uhq@!+#v%yd(zoscL;uS4i*CK~qZQFKU4YwvPcv z@gBk?+?aKk9L4A%6ta*|X@Y)i!XW8|bhm!bdVRc1;}GJ|!nq;+Ty@KOePif#6jzwM zgLgU%a|06x;o>>oR4Ao`wGy38v~%g{ygsN7g7v&X{QcZr8GLnKi?gW*FCRNctL81Z zQ^t*ww=nChhVMQ#t5;Py`SufBEgsQZi%K)<_w=|ol$5D`B5V}(X4MgY=@<@Tg11{ zmdlg8o@-b`QqXga8!0qhTZt)YG;sB6Y-$ahk@D!6wPM=2k?w>aX7!v+9GQKdgNZp6 z?7yUx&^(BZNY6k&wvLS$MtAwaK4%bpmzY9KzZ5r3NdNG|y*xG+sR z2%52|7zZw3q3kS$;z0fp)@AIo3pZ{XzpSa` HwpINHX*(vX delta 367 zcmX?eg=OD9mJLy|oA1f~Qs4Z~HG*;TA@_OOlch>zHZLj;W85rLx`lBv&lIl7JmoE5 z`a=cJutT-U-i*t3oTHaY_?ErbISrVw#i=GcqTvC!Zn#~o4{s;twD^Nmu=g_Jo&^f ep~*9LwM<{p!HazW diff --git a/MinecraftClient/Resources/lang/zh-Hant.ini b/MinecraftClient/Resources/lang/zh-Hant.ini index 29b3bf2910a787902715eb5a7f72529b8098a038..9ac26a7c318f311c8b5e4201634b37fe71a70ea8 100644 GIT binary patch delta 2038 zcma)6ZA_b06uuWaD6Bx!NlOc*uPxI7(h>Yj%u5_@HGZ+tHOt%tEQOT@Xq$c@A;tk| zgdr;}XKRTXB32h&mI=IxCdOo%_-l#L{n)Q1pxHELKZq>R7=8@Tz5Rq(ByaB9zW01R z=bY!9@D16*ZP`*l|5P9G;p4$$CIOQCvJ)3^lWsg7yz9nWH?fl*GKl|riAcb$R>ReL zMSVYByYXs}8#wcuNFM!xLvX^bh6RgKnmBfvLd;TC%mSc$xYbxm{V;1WR3vX$pkX|N z(k)A(v4%w-!cQ|f!ZG*|l^=2Z?3jzfL*HXk#CM8nbG6*4;X-W-d6kIL9sdoAvA7f1 zSl5(_+^j#dm$2>&xq^~DpMl4P1yr)4GU81a7$@MZuq+zJ|3@;CdQ0~naie! zTA88!hko#eYLyPe$tuL$^ULY+2KX+dFtG~RIq0-VATJVDzW{0DWfq_*^rSp3MQ=~! ze$+}gLv@8M$GAZmDaCHE-sI*S#BUEtY9tY~MAqiP8+-)TCsk>@mp7<3mE_&%nGeA` zRSUv&k))fNkwJK>s5wA_h_jn_^c*@A$qPLt&b!fOYiQWz``#wsDr=uP+usp$e=}0D zrQ3ql$*N+3@XUcu8>A~U&rs&6F73&|n`TFfiR3}&KKJNo)*A>DI#6CHh1zgA+`stl zn|{oK6@`HiiN?CwkyS2eR`={1JAeF8P#{Kfn4Ck9ek`JZ@2nNW!U)6y8d1gq`ldD3 z{;9e51?UQUOEOi1CwTA#dZGt+MpG!!>sA6QhuA zq=hqv?HNW?Hi+iYG8YI5oup3uQ2^_os&U8k;nT|pWZSlcXhM&?N=adPQUjZxHI!|e z(s+fERwLckE`A>S!vED-Nw{Jyim$4uZin6-71b6XzP6Y#XZO91_^@y(e1ApOJ3;tJ z2d#0r^iE7BS7kF{nqJHSt}Y1D+(J=dd__xzctlHOJ0g9lr3d%3TC!szLVQ3+4LfvP zI;v-AW)_<>gYjq$HApLqtrY$lP}XhB5@~W0>BgAuMS_T=*m1A@+Y$KXnogR(cADiP$TIV;?TX|kbC?nYEuUWDh?$|c)Md8Z~1MdKSPdayc=5dDeq z@k(lXJimgO*pMhGLUZND-IQNSG8yUcGM=RO!@C(I7k-Mydo?=FnafD&=O(xo|3424 U-#Baw1mYVN)bw?41#M9N3tS)@MgRZ+ delta 380 zcmZXNze@sf7{8oEJq>;cfr}w$y zF#Pd8dfEwc3M6VAp_GKqr;PBfjK#f($yzn(k-GWXkx0=)r?m@hL((_c7TaV#Hq8<& z&bFDK#mpIH4hALZM%%DKH>838=UEnE>!vbezQ^G+#_~!pw&N)vU3zl8h%O&bxIuzrpj(?UTIV ScriptAssemblies = new(); diff --git a/MinecraftClient/Scripting/ChatBot.cs b/MinecraftClient/Scripting/ChatBot.cs index 14fefcde..1c8272b1 100644 --- a/MinecraftClient/Scripting/ChatBot.cs +++ b/MinecraftClient/Scripting/ChatBot.cs @@ -835,10 +835,11 @@ namespace MinecraftClient /// Log text to write protected void LogToConsole(object? text) { + string botName = Translations.GetOrNull("botname." + GetType().Name) ?? GetType().Name; if (_handler == null || master == null) - ConsoleIO.WriteLogLine(String.Format("[{0}] {1}", GetType().Name, text)); + ConsoleIO.WriteLogLine(String.Format("[{0}] {1}", botName, text)); else - Handler.Log.Info(String.Format("[{0}] {1}", GetType().Name, text)); + Handler.Log.Info(String.Format("[{0}] {1}", botName, text)); string logfile = Settings.Config.AppVar.ExpandVars(Config.Main.Advanced.ChatbotLogFile); if (!String.IsNullOrEmpty(logfile)) @@ -913,7 +914,10 @@ namespace MinecraftClient protected void ReconnectToTheServer(int ExtraAttempts = 3, int delaySeconds = 0) { if (Settings.Config.Logging.DebugMessages) - ConsoleIO.WriteLogLine(Translations.Get("chatbot.reconnect", GetType().Name)); + { + string botName = Translations.GetOrNull("botname." + GetType().Name) ?? GetType().Name; + ConsoleIO.WriteLogLine(Translations.Get("chatbot.reconnect", botName)); + } McClient.ReconnectionAttemptsLeft = ExtraAttempts; Program.Restart(delaySeconds); } @@ -1129,14 +1133,15 @@ namespace MinecraftClient /// protected static string GetTimestamp() { - DateTime time = DateTime.Now; - return String.Format("{0}-{1}-{2} {3}:{4}:{5}", - time.Year.ToString("0000"), - time.Month.ToString("00"), - time.Day.ToString("00"), - time.Hour.ToString("00"), - time.Minute.ToString("00"), - time.Second.ToString("00")); + return DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + } + + /// + /// Get a h:m:s timestamp representing the current system time + /// + protected static string GetShortTimestamp() + { + return DateTime.Now.ToString("HH:mm:ss"); } /// diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index fdae982b..c0115a29 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -387,6 +387,7 @@ namespace MinecraftClient { string[] sip = General.Server.Host.Split(new[] { ":", ":" }, StringSplitOptions.None); General.Server.Host = sip[0]; + InternalConfig.ServerIP = General.Server.Host; if (sip.Length > 1) { @@ -395,7 +396,10 @@ namespace MinecraftClient } } - SetServerIP(General.Server, true); + if (General.Server.Port.HasValue) + InternalConfig.ServerPort = General.Server.Port.Value; + else + SetServerIP(General.Server, true); for (int i = 0; i < Advanced.BotOwners.Count; ++i) Advanced.BotOwners[i] = ToLowerIfNeed(Advanced.BotOwners[i]); @@ -1094,7 +1098,7 @@ namespace MinecraftClient public ChatBots.DiscordBridge.Configs DiscordBridge { get { return ChatBots.DiscordBridge.Config; } - set { ChatBots.DiscordBridge.Config = value; } + set { ChatBots.DiscordBridge.Config = value; ChatBots.DiscordBridge.Config.OnSettingUpdate(); } } [TomlPrecedingComment("$config.ChatBot.Farmer$")] diff --git a/README.md b/README.md index 7d49547e..0456d50b 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,13 @@ If you'd like to contribute to Minecraft Console Client, great, just fork the re Check out: [How to update or add translations for MCC](https://mccteam.github.io/guide/contibuting.html#translations). MCC now supports the following languages (Alphabetical order) : - * `de.ini` (51.34% translated) : Deutsch - German + * `de.ini` (48.28% translated) : Deutsch - German * `en.ini` : English - English - * `fr.ini` (51.34% translated) : Français (France) - French - * `ru.ini` (50.49% translated) : Русский (Russkiy) - Russian - * `vi.ini` (50.49% translated) : Tiếng Việt (Việt Nam) - Vietnamese - * `zh-Hans.ini` (95.50% translated) : 简体中文 - Chinese Simplified - * `zh-Hant.ini` (95.50% translated) : 繁體中文 - Chinese Traditional + * `fr.ini` (48.28% translated) : Français (France) - French + * `ru.ini` (47.49% translated) : Русский (Russkiy) - Russian + * `vi.ini` (47.49% translated) : Tiếng Việt (Việt Nam) - Vietnamese + * `zh-Hans.ini` (92.20% translated) : 简体中文 - Chinese Simplified + * `zh-Hant.ini` (92.20% translated) : 繁體中文 - Chinese Traditional ## Building from the source 🏗️ From 8fb74c63a6bcaa5c3b895c64cbe26d9b7e806723 Mon Sep 17 00:00:00 2001 From: BruceChen Date: Fri, 21 Oct 2022 19:24:57 +0800 Subject: [PATCH 07/15] [README] Calculates the translation status in characters. --- MinecraftClient/Translations.cs | 12 ++++++------ README.md | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/MinecraftClient/Translations.cs b/MinecraftClient/Translations.cs index c66554ac..16decad2 100644 --- a/MinecraftClient/Translations.cs +++ b/MinecraftClient/Translations.cs @@ -683,6 +683,7 @@ namespace MinecraftClient } StringBuilder sb = new(); int total = 0, translated = 0; + int total_char = 0, translated_char = 0; for (int i = 0; i < transEn.Length; ++i) { string line = transEn[i].Trim(); @@ -693,23 +694,22 @@ namespace MinecraftClient } else { + int en_value_len = line.Length - index; string key = line[..index]; sb.Append(key).Append('='); if (trans.TryGetValue(key, out string? value)) { sb.Append(value.Replace("\n", "\\n")); - ++total; ++translated; + translated_char += en_value_len; } - else - { - ++total; - } + ++total; + total_char += en_value_len; } sb.AppendLine(); } File.WriteAllText(fileName, sb.ToString(), Encoding.Unicode); - ConsoleIO.WriteLine(string.Format("Language {0}: Translated {1} of {2}, {3:0.00}%", lang, translated, total, 100.0 * (double)translated / total)); + ConsoleIO.WriteLine(string.Format("Language {0}: Translated {1} of {2}, {3:0.00}%", lang, translated, total, 100.0 * translated_char / total_char)); } } diff --git a/README.md b/README.md index 0456d50b..222b40a1 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,13 @@ If you'd like to contribute to Minecraft Console Client, great, just fork the re Check out: [How to update or add translations for MCC](https://mccteam.github.io/guide/contibuting.html#translations). MCC now supports the following languages (Alphabetical order) : - * `de.ini` (48.28% translated) : Deutsch - German + * `de.ini` (30.21% translated) : Deutsch - German * `en.ini` : English - English - * `fr.ini` (48.28% translated) : Français (France) - French - * `ru.ini` (47.49% translated) : Русский (Russkiy) - Russian - * `vi.ini` (47.49% translated) : Tiếng Việt (Việt Nam) - Vietnamese - * `zh-Hans.ini` (92.20% translated) : 简体中文 - Chinese Simplified - * `zh-Hant.ini` (92.20% translated) : 繁體中文 - Chinese Traditional + * `fr.ini` (30.21% translated) : Français (France) - French + * `ru.ini` (29.65% translated) : Русский (Russkiy) - Russian + * `vi.ini` (29.65% translated) : Tiếng Việt (Việt Nam) - Vietnamese + * `zh-Hans.ini` (87.08% translated) : 简体中文 - Chinese Simplified + * `zh-Hant.ini` (87.08% translated) : 繁體中文 - Chinese Traditional ## Building from the source 🏗️ From 614e98d424514b79ff6ae42063dd9f5370641f2b Mon Sep 17 00:00:00 2001 From: Milutinke Date: Fri, 21 Oct 2022 15:08:11 +0200 Subject: [PATCH 08/15] Added debug option to the Discord Bridge chat bot. --- MinecraftClient/ChatBots/DiscordBridge.cs | 9 ++++++++- MinecraftClient/Resources/lang/en.ini | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index ef97820b..29c36ec4 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -141,7 +141,8 @@ namespace MinecraftClient.ChatBots TokenType = TokenType.Bot, AutoReconnect = true, Intents = DiscordIntents.All, - MinimumLogLevel = LogLevel.None + MinimumLogLevel = Settings.Config.Logging.DebugMessages ? + (LogLevel.Trace | LogLevel.Information | LogLevel.Debug | LogLevel.Critical | LogLevel.Error | LogLevel.Warning) : LogLevel.None }); try @@ -156,6 +157,9 @@ namespace MinecraftClient.ChatBots UnloadBot(); return; } + + LogDebugToConsole("Exception when trying to find the guild:"); + LogDebugToConsole(e); } try @@ -170,6 +174,9 @@ namespace MinecraftClient.ChatBots UnloadBot(); return; } + + LogDebugToConsole("Exception when trying to find the channel:"); + LogDebugToConsole(e); } _client.MessageCreated += async (source, e) => diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 0971529e..6ed147ee 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -947,7 +947,7 @@ config.ChatBot.AutoRespond.Match_Colors=Do not remove colors from text (Note: Yo config.ChatBot.ChatLog=Logs chat messages in a file on disk. # ChatBot.DiscordBridge -config.ChatBot.DiscordBridge=This bot allows you to send and recieve messages and commands via a Discord channel.\n# For Setup you can either use the documentation or read here (Documentation has images).\n# Documentation: https://mccteam.github.io/guide/chat-bots.html#discord-bridge\n# Setup:\n# First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA .\n# IMPORTANT: When creating a bot, you MUST ENABLE "Message Content Intent" and "Presence Intent" in order for bot to work! Also follow along carefully do not miss any steps!\n# When making a bot, copy the generated token and paste it here in "Token" field (tokens are important, keep them safe).\n# Copy the "Application ID" and go to: https://bit.ly/2Spn2Q3 .\n# Paste the id you have copied and check the "Administrator" field in permissions, then click on the link at the bottom.\n# This will open an invitation menu with your servers, choose the server you want to invite the bot on and invite him.\n# Once you've invited the bot, go to your Discord client and go to Settings -> Advanced and Enable "Developer Mode".\n# Exit the settings and right click on a server you have invited the bot to in the server list, then click "Copy ID", and paste the id here in "GuildId".\n# Then right click on a channel where you want to interact with the bot and again right click -> "Copy ID", pase the copied id here in "ChannelId".\n# And for the end, send a message in the channel, right click on your nick and again right click -> "Copy ID", then paste the id here in "OwnersIds".\n# How to use:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. +config.ChatBot.DiscordBridge=This bot allows you to send and recieve messages and commands via a Discord channel.\n# For Setup you can either use the documentation or read here (Documentation has images).\n# Documentation: https://mccteam.github.io/guide/chat-bots.html#discord-bridge\n# Setup:\n# First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA .\n# IMPORTANT: When creating a bot, you MUST ENABLE "Message Content Intent", "Server Members Intent" and "Presence Intent" in order for bot to work! Also follow along carefully do not miss any steps!\n# When making a bot, copy the generated token and paste it here in "Token" field (tokens are important, keep them safe).\n# Copy the "Application ID" and go to: https://bit.ly/2Spn2Q3 .\n# Paste the id you have copied and check the "Administrator" field in permissions, then click on the link at the bottom.\n# This will open an invitation menu with your servers, choose the server you want to invite the bot on and invite him.\n# Once you've invited the bot, go to your Discord client and go to Settings -> Advanced and Enable "Developer Mode".\n# Exit the settings and right click on a server you have invited the bot to in the server list, then click "Copy ID", and paste the id here in "GuildId".\n# Then right click on a channel where you want to interact with the bot and again right click -> "Copy ID", pase the copied id here in "ChannelId".\n# And for the end, send a message in the channel, right click on your nick and again right click -> "Copy ID", then paste the id here in "OwnersIds".\n# How to use:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. config.ChatBot.DiscordBridge.Token=Your Discord Bot token. config.ChatBot.DiscordBridge.GuildId=The ID of a server/guild where you have invited the bot to. config.ChatBot.DiscordBridge.ChannelId=The ID of a channel where you want to interact with the MCC using the bot. From d8c9e1587b10486361e48945c834ef6b510f196a Mon Sep 17 00:00:00 2001 From: Milutinke Date: Fri, 21 Oct 2022 20:25:48 +0200 Subject: [PATCH 09/15] Added Resizing back to the Map Chat bot and added an option to send rendered images to Discord via Discord Bridge chat bot. --- MinecraftClient/ChatBots/DiscordBridge.cs | 72 ++++++++++++++++++++--- MinecraftClient/ChatBots/Map.cs | 68 ++++++++++++++++++++- MinecraftClient/MinecraftClient.csproj | 1 + MinecraftClient/Resources/lang/en.ini | 13 ++-- 4 files changed, 138 insertions(+), 16 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index 29c36ec4..77e9df99 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using DSharpPlus; @@ -11,6 +13,8 @@ namespace MinecraftClient.ChatBots { public class DiscordBridge : ChatBot { + private static DiscordBridge? instance = null; + private DiscordClient? _client; private DiscordChannel? _channel; @@ -44,6 +48,11 @@ namespace MinecraftClient.ChatBots public void OnSettingUpdate() { } } + public DiscordBridge() + { + instance = this; + } + public override void Initialize() { Task.Run(async () => await MainAsync()); @@ -74,6 +83,11 @@ namespace MinecraftClient.ChatBots } } + public static DiscordBridge? GetInstance() + { + return instance; + } + public override void GetText(string text) { if (_client == null || _channel == null) @@ -96,14 +110,6 @@ namespace MinecraftClient.ChatBots } else message = text; - SendMessageToDiscord(message, teleportRequest); - } - - private void SendMessageToDiscord(string message, bool teleportRequest = false) - { - if (_client == null || _channel == null) - return; - if (teleportRequest) { var messageBuilder = new DiscordMessageBuilder() @@ -117,13 +123,61 @@ namespace MinecraftClient.ChatBots new DiscordButtonComponent(ButtonStyle.Danger, "deny_teleport", "Deny") }); - _client.SendMessageAsync(_channel, messageBuilder).Wait(); + SendMessage(messageBuilder); return; } + else SendMessage(message); + } + + public void SendMessage(string message) + { + if (_client == null || _channel == null) + return; _client.SendMessageAsync(_channel, message).Wait(); } + public void SendMessage(DiscordMessageBuilder builder) + { + if (_client == null || _channel == null) + return; + + _client.SendMessageAsync(_channel, builder).Wait(); + } + + public void SendMessage(DiscordEmbedBuilder embedBuilder) + { + if (_client == null || _channel == null) + return; + + _client.SendMessageAsync(_channel, embedBuilder).Wait(); + + } + public void SendImage(string filePath, string? text = null) + { + if (_client == null || _channel == null) + return; + + using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + filePath = filePath[(filePath.IndexOf(Path.DirectorySeparatorChar) + 1)..]; + var messageBuilder = new DiscordMessageBuilder().WithContent(text); + + messageBuilder.WithFiles(new Dictionary() { { $"attachment://{filePath}", fs } }); + + _client.SendMessageAsync(_channel, messageBuilder).Wait(); + } + } + + public void SendFile(FileStream fileStream) + { + if (_client == null || _channel == null) + return; + + var messageBuilder = new DiscordMessageBuilder().WithFile(fileStream); + SendMessage(messageBuilder); + } + async Task MainAsync() { try diff --git a/MinecraftClient/ChatBots/Map.cs b/MinecraftClient/ChatBots/Map.cs index 2b994c16..1c2b8786 100644 --- a/MinecraftClient/ChatBots/Map.cs +++ b/MinecraftClient/ChatBots/Map.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; +using ImageMagick; using MinecraftClient.Mapping; using Tomlet.Attributes; @@ -35,7 +36,20 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$config.ChatBot.Map.Notify_On_First_Update$")] public bool Notify_On_First_Update = true; - public void OnSettingUpdate() { } + [TomlInlineComment("$config.ChatBot.Map.Rasize_Rendered_Image$")] + public bool Rasize_Rendered_Image = false; + + [TomlInlineComment("$config.ChatBot.Map.Resize_To$")] + public int Resize_To = 512; + + [TomlPrecedingComment("$config.ChatBot.Map.Send_Rendered_To_Discord$")] + public bool Send_Rendered_To_Discord = false; + + public void OnSettingUpdate() + { + if (Resize_To <= 0) + Resize_To = 128; + } } private readonly string baseDirectory = @"Rendered_Maps"; @@ -47,10 +61,17 @@ namespace MinecraftClient.ChatBots if (!Directory.Exists(baseDirectory)) Directory.CreateDirectory(baseDirectory); + DeleteRenderedMaps(); + RegisterChatBotCommand("maps", "bot.map.cmd.desc", "maps list|render or maps l|r ", OnMapCommand); } public override void OnUnload() + { + DeleteRenderedMaps(); + } + + private void DeleteRenderedMaps() { if (Config.Delete_All_On_Unload) { @@ -199,7 +220,48 @@ namespace MinecraftClient.ChatBots } } file.Close(); + LogToConsole(Translations.TryGet("bot.map.rendered", map.MapId, fileName)); + + if (Config.Rasize_Rendered_Image) + { + using (var image = new MagickImage(fileName)) + { + var size = new MagickGeometry(Config.Resize_To, Config.Resize_To); + size.IgnoreAspectRatio = true; + + image.Resize(size); + image.Write(fileName); + LogToConsole(Translations.TryGet("bot.map.resized_rendered_image", map.MapId, Config.Resize_To)); + } + } + + if (Config.Send_Rendered_To_Discord) + { + if (DiscordBridge.Config.Enabled) + { + DiscordBridge? discordBridge = DiscordBridge.GetInstance(); + + if (discordBridge == null) + return; + + // We must convert to a PNG in order to send to Discord, BMP does not work + string newFileName = fileName.Replace(".bmp", ".png"); + using (var image = new MagickImage(fileName)) + { + image.Write(newFileName); + discordBridge.SendImage(newFileName, $"> A render of the map with an id: **{map.MapId}**"); + + newFileName = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + newFileName; + + // Delete the temporary file + if (File.Exists(newFileName)) + File.Delete(newFileName); + + LogToConsole(Translations.TryGet("bot.map.sent_to_discord", map.MapId)); + } + } + } } private void RenderInConsole(McMap map) @@ -389,8 +451,8 @@ namespace MinecraftClient.ChatBots return new( r: (byte)((Colors[baseColorId][0] * shadeMultiplier) / 255), - g: (byte)((Colors[baseColorId][1] * shadeMultiplier) / 255), - b: (byte)((Colors[baseColorId][2] * shadeMultiplier) / 255), + g: (byte)((Colors[baseColorId][1] * shadeMultiplier) / 255), + b: (byte)((Colors[baseColorId][2] * shadeMultiplier) / 255), a: 255 ); } diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 67e98941..4387bd2a 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -37,6 +37,7 @@ + diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 6ed147ee..4d6a7068 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -711,6 +711,8 @@ bot.map.rendered=Succesfully rendered a map with id '{0}' to: '{1}' bot.map.failed_to_render=Failed to render the map with id: '{0}' bot.map.list_item=- Map id: {0} (Last Updated: {1}) bot.map.scale=The size of the map is reduced from ({0}x{1}) to ({2}x{3}) due to the size limitation of the current terminal. +bot.map.resized_rendered_image=Resized the rendered image of the map with id: '{0}' to {1}x{1}. +bot.map.sent_to_discord=Sent a rendered image of a map with an id '{0}' to the discord! # PlayerListLogger botname.PlayerListLogger=PlayerListLogger @@ -947,7 +949,7 @@ config.ChatBot.AutoRespond.Match_Colors=Do not remove colors from text (Note: Yo config.ChatBot.ChatLog=Logs chat messages in a file on disk. # ChatBot.DiscordBridge -config.ChatBot.DiscordBridge=This bot allows you to send and recieve messages and commands via a Discord channel.\n# For Setup you can either use the documentation or read here (Documentation has images).\n# Documentation: https://mccteam.github.io/guide/chat-bots.html#discord-bridge\n# Setup:\n# First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA .\n# IMPORTANT: When creating a bot, you MUST ENABLE "Message Content Intent", "Server Members Intent" and "Presence Intent" in order for bot to work! Also follow along carefully do not miss any steps!\n# When making a bot, copy the generated token and paste it here in "Token" field (tokens are important, keep them safe).\n# Copy the "Application ID" and go to: https://bit.ly/2Spn2Q3 .\n# Paste the id you have copied and check the "Administrator" field in permissions, then click on the link at the bottom.\n# This will open an invitation menu with your servers, choose the server you want to invite the bot on and invite him.\n# Once you've invited the bot, go to your Discord client and go to Settings -> Advanced and Enable "Developer Mode".\n# Exit the settings and right click on a server you have invited the bot to in the server list, then click "Copy ID", and paste the id here in "GuildId".\n# Then right click on a channel where you want to interact with the bot and again right click -> "Copy ID", pase the copied id here in "ChannelId".\n# And for the end, send a message in the channel, right click on your nick and again right click -> "Copy ID", then paste the id here in "OwnersIds".\n# How to use:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. +config.ChatBot.DiscordBridge=This bot allows you to send and recieve messages and commands via a Discord channel.\n# For Setup you can either use the documentation or read here (Documentation has images).\n# Documentation: https://mccteam.github.io/guide/chat-bots.html#discord-bridge\n# Setup:\n# First you need to create a Bot on the Discord Developers Portal, here is a video tutorial: https://www.youtube.com/watch?v=2FgMnZViNPA .\n# /!\ IMPORTANT /!\: When creating a bot, you MUST ENABLE "Message Content Intent", "Server Members Intent" and "Presence Intent" in order for bot to work! Also follow along carefully do not miss any steps!\n# When making a bot, copy the generated token and paste it here in "Token" field (tokens are important, keep them safe).\n# Copy the "Application ID" and go to: https://bit.ly/2Spn2Q3 .\n# Paste the id you have copied and check the "Administrator" field in permissions, then click on the link at the bottom.\n# This will open an invitation menu with your servers, choose the server you want to invite the bot on and invite him.\n# Once you've invited the bot, go to your Discord client and go to Settings -> Advanced and Enable "Developer Mode".\n# Exit the settings and right click on a server you have invited the bot to in the server list, then click "Copy ID", and paste the id here in "GuildId".\n# Then right click on a channel where you want to interact with the bot and again right click -> "Copy ID", pase the copied id here in "ChannelId".\n# And for the end, send a message in the channel, right click on your nick and again right click -> "Copy ID", then paste the id here in "OwnersIds".\n# How to use:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. config.ChatBot.DiscordBridge.Token=Your Discord Bot token. config.ChatBot.DiscordBridge.GuildId=The ID of a server/guild where you have invited the bot to. config.ChatBot.DiscordBridge.ChannelId=The ID of a channel where you want to interact with the MCC using the bot. @@ -970,12 +972,15 @@ config.ChatBot.HangmanGame=A small game to demonstrate chat interactions. Player config.ChatBot.Mailer=Relay messages between players and servers, like a mail plugin\n# This bot can store messages when the recipients are offline, and send them when they join the server\n# /!\ Server admins can spoof PMs (/tellraw, /nick) so enable this bot only if you trust server admins # ChatBot.Map -config.ChatBot.Map=Allows you to render maps into .jpg images\n# This is useful for solving captchas which use maps\n# The maps are rendered into Rendered_Maps folder.\n# NOTE:\n# This feature is currently only useful for solving captchas which use maps.\n# If some servers have a very short time for solving captchas, enabe Auto_Render_On_Update and prepare to open the file quickly.\n# On linux you can use FTP to access generated files.\n# In the future it might will be possible to display maps directly in the console with a separate command.\n# /!\ Make sure server rules allow bots to be used on the server, or you risk being punished. +config.ChatBot.Map=Allows you to render maps in the console and into images (which can be then sent to Discord using Discord Bridge Chat Bot)\n# This is useful for solving captchas which use maps\n# The maps are rendered into Rendered_Maps folder if the Save_To_File is enabled.\n# NOTE:\n# If some servers have a very short time for solving captchas, enabe Auto_Render_On_Update to see them immediatelly in the console.\n# /!\ Make sure server rules allow bots to be used on the server, or you risk being punished. config.ChatBot.Map.Render_In_Console=Whether to render the map in the console. -config.ChatBot.Map.Save_To_File=Whether to store the rendered map as a file. +config.ChatBot.Map.Save_To_File=Whether to store the rendered map as a file (You need this setting if you want to get a map on Discord using Discord Bridge). config.ChatBot.Map.Auto_Render_On_Update=Automatically render the map once it is received or updated from/by the server -config.ChatBot.Map.Delete_All_On_Unload=Delete all rendered maps on unload/reload (Does not delete the images if you exit the client) +config.ChatBot.Map.Delete_All_On_Unload=Delete all rendered maps on unload/reload or when you launch the MCC again. config.ChatBot.Map.Notify_On_First_Update=Get a notification when you have gotten a map from the server for the first time +config.ChatBot.Map.Resize_To=The size that a rendered image should be resized to, in pixels (eg. 512). +config.ChatBot.Map.Rasize_Rendered_Image=Resize an rendered image, this is useful when images that are rendered are small and when are being sent to Discord. +config.ChatBot.Map.Send_Rendered_To_Discord=Send a rendered map (saved to a file) to a Discord channel via the Discord Bride chat bot (The Discord Bridge chat bot must be enabled and configured!)\n# You need to enable Save_To_File in order for this to work.\n# We also recommend turning on resizing. # ChatBot.PlayerListLogger config.ChatBot.PlayerListLogger=Log the list of players periodically into a textual file. From cbfc592e2714974ae4d4cf6ab4a43641acc825b0 Mon Sep 17 00:00:00 2001 From: Milutinke Date: Fri, 21 Oct 2022 21:41:45 +0200 Subject: [PATCH 10/15] Fixed Map Chat Bot not sending maps in case the Discord Bridge was not ready and connected. --- MinecraftClient/ChatBots/DiscordBridge.cs | 3 ++ MinecraftClient/ChatBots/Map.cs | 59 ++++++++++++++++------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index 77e9df99..ae8f38bf 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -14,6 +14,7 @@ namespace MinecraftClient.ChatBots public class DiscordBridge : ChatBot { private static DiscordBridge? instance = null; + public bool IsConnected { get; private set; } private DiscordClient? _client; private DiscordChannel? _channel; @@ -80,6 +81,7 @@ namespace MinecraftClient.ChatBots }).Wait(); _client.DisconnectAsync().Wait(); + IsConnected = false; } } @@ -280,6 +282,7 @@ namespace MinecraftClient.ChatBots Color = new DiscordColor(0x00FF00) }); + IsConnected = true; await Task.Delay(-1); } catch (Exception e) diff --git a/MinecraftClient/ChatBots/Map.cs b/MinecraftClient/ChatBots/Map.cs index 1c2b8786..ae612e42 100644 --- a/MinecraftClient/ChatBots/Map.cs +++ b/MinecraftClient/ChatBots/Map.cs @@ -13,6 +13,12 @@ namespace MinecraftClient.ChatBots { public static Configs Config = new(); + public struct DiscordMap + { + public string FileName; + public int MapId; + } + [TomlDoNotInlineObject] public class Configs { @@ -56,6 +62,8 @@ namespace MinecraftClient.ChatBots private readonly Dictionary cachedMaps = new(); + private readonly Queue discordQueue = new(); + public override void Initialize() { if (!Directory.Exists(baseDirectory)) @@ -238,28 +246,43 @@ namespace MinecraftClient.ChatBots if (Config.Send_Rendered_To_Discord) { + // We need to queue up images because Discord Bridge is not ready immediatelly if (DiscordBridge.Config.Enabled) + discordQueue.Enqueue(new DiscordMap { FileName = fileName, MapId = map.MapId }); + } + } + + public override void Update() + { + if (!DiscordBridge.Config.Enabled) + return; + + DiscordBridge? discordBridge = DiscordBridge.GetInstance(); + + if (discordBridge == null) + return; + + if (!discordBridge.IsConnected) + return; + + if (discordQueue.Count > 0) + { + DiscordMap discordMap = discordQueue.Dequeue(); + string fileName = discordMap.FileName; + + // We must convert to a PNG in order to send to Discord, BMP does not work + string newFileName = fileName.Replace(".bmp", ".png"); + using (var image = new MagickImage(fileName)) { - DiscordBridge? discordBridge = DiscordBridge.GetInstance(); + image.Write(newFileName); + discordBridge.SendImage(newFileName, $"> A render of the map with an id: **{discordMap.MapId}**"); + newFileName = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + newFileName; - if (discordBridge == null) - return; + // Delete the temporary file + if (File.Exists(newFileName)) + File.Delete(newFileName); - // We must convert to a PNG in order to send to Discord, BMP does not work - string newFileName = fileName.Replace(".bmp", ".png"); - using (var image = new MagickImage(fileName)) - { - image.Write(newFileName); - discordBridge.SendImage(newFileName, $"> A render of the map with an id: **{map.MapId}**"); - - newFileName = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + newFileName; - - // Delete the temporary file - if (File.Exists(newFileName)) - File.Delete(newFileName); - - LogToConsole(Translations.TryGet("bot.map.sent_to_discord", map.MapId)); - } + LogToConsole(Translations.TryGet("bot.map.sent_to_discord", discordMap.MapId)); } } } From 6851b8a96c1b17fa479c1ee6362a06d7f6866701 Mon Sep 17 00:00:00 2001 From: Milutinke Date: Sat, 22 Oct 2022 12:15:44 +0200 Subject: [PATCH 11/15] Added a command for switching a direction of the Discord Bridge. --- MinecraftClient/ChatBots/DiscordBridge.cs | 85 ++++++++++++++++++++--- MinecraftClient/Resources/lang/en.ini | 6 ++ 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index ae8f38bf..d5afe5e6 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -11,6 +11,13 @@ using Tomlet.Attributes; namespace MinecraftClient.ChatBots { + internal enum BridgeDirection + { + Both = 0, + Minecraft, + Discord + } + public class DiscordBridge : ChatBot { private static DiscordBridge? instance = null; @@ -18,6 +25,7 @@ namespace MinecraftClient.ChatBots private DiscordClient? _client; private DiscordChannel? _channel; + private BridgeDirection bridgeDirection = BridgeDirection.Both; public static Configs Config = new(); @@ -56,6 +64,8 @@ namespace MinecraftClient.ChatBots public override void Initialize() { + RegisterChatBotCommand("dscbridge", "bot.DiscordBridge.desc", "dscbridge direction ", OnDscCommand); + Task.Run(async () => await MainAsync()); } @@ -90,9 +100,51 @@ namespace MinecraftClient.ChatBots return instance; } + private string OnDscCommand(string cmd, string[] args) + { + if (args.Length == 2) + { + if (args[0].ToLower().Equals("direction")) + { + string direction = args[1].ToLower().Trim(); + + string? bridgeName = ""; + + switch (direction) + { + case "b": + case "both": + bridgeName = "bot.DiscordBridge.direction.both"; + bridgeDirection = BridgeDirection.Both; + break; + + case "mc": + case "minecraft": + bridgeName = "bot.DiscordBridge.direction.minecraft"; + bridgeDirection = BridgeDirection.Minecraft; + break; + + case "d": + case "dcs": + case "discord": + bridgeName = "bot.DiscordBridge.direction.discord"; + bridgeDirection = BridgeDirection.Discord; + break; + + default: + return Translations.TryGet("bot.DiscordBridge.invalid_direction"); + } + + return Translations.TryGet("bot.DiscordBridge.direction", Translations.TryGet(bridgeName)); + }; + } + + return "dscbridge direction "; + } + public override void GetText(string text) { - if (_client == null || _channel == null) + if (!CanSendMessages()) return; text = GetVerbatim(text).Trim(); @@ -133,31 +185,31 @@ namespace MinecraftClient.ChatBots public void SendMessage(string message) { - if (_client == null || _channel == null) + if (!CanSendMessages()) return; - _client.SendMessageAsync(_channel, message).Wait(); + _client!.SendMessageAsync(_channel, message).Wait(); } public void SendMessage(DiscordMessageBuilder builder) { - if (_client == null || _channel == null) + if (!CanSendMessages()) return; - _client.SendMessageAsync(_channel, builder).Wait(); + _client!.SendMessageAsync(_channel, builder).Wait(); } public void SendMessage(DiscordEmbedBuilder embedBuilder) { - if (_client == null || _channel == null) + if (!CanSendMessages()) return; - _client.SendMessageAsync(_channel, embedBuilder).Wait(); + _client!.SendMessageAsync(_channel, embedBuilder).Wait(); } public void SendImage(string filePath, string? text = null) { - if (_client == null || _channel == null) + if (!CanSendMessages()) return; using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) @@ -167,19 +219,24 @@ namespace MinecraftClient.ChatBots messageBuilder.WithFiles(new Dictionary() { { $"attachment://{filePath}", fs } }); - _client.SendMessageAsync(_channel, messageBuilder).Wait(); + _client!.SendMessageAsync(_channel, messageBuilder).Wait(); } } public void SendFile(FileStream fileStream) { - if (_client == null || _channel == null) + if (!CanSendMessages()) return; var messageBuilder = new DiscordMessageBuilder().WithFile(fileStream); SendMessage(messageBuilder); } + private bool CanSendMessages() + { + return _client == null || _channel == null || bridgeDirection == BridgeDirection.Minecraft ? false : true; + } + async Task MainAsync() { try @@ -248,10 +305,16 @@ namespace MinecraftClient.ChatBots string message = e.Message.Content.Trim(); + if (bridgeDirection == BridgeDirection.Discord) + { + if (!message.StartsWith(".dscbridge")) + return; + } + if (message.StartsWith(".")) { - await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); message = message[1..]; + await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); string? result = ""; PerformInternalCommand(message, ref result); diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 4d6a7068..e9ef41f6 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -622,6 +622,12 @@ bot.DiscordBridge.missing_token=Please provide a valid token! bot.DiscordBridge.guild_not_found=The provided guild/server with an id '{0}' has not been found! bot.DiscordBridge.channel_not_found=The provided channel with an id '{0}' has not been found! bot.DiscordBridge.unknown_error=An unknown error has occured! +bot.DiscordBridge.desc=This command allows you to specify in the which direction the messages will be relayed via the Discord Bridge chat bot. +bot.DiscordBridge.invalid_direction=Invalid direction provided! Avaliable directions: both|b, minecraft|mc, discord|dsc. Example: "dscbridge direction mc" +bot.DiscordBridge.direction=Direction of the Discord Brdige has been switched to '{0}'! +bot.DiscordBridge.direction.both=Both +bot.DiscordBridge.direction.minecraft=Minecraft +bot.DiscordBridge.direction.discord=Discord # Farmer botname.Farmer=Farmer From b89aeda198891d4f4a4e17672574eb2905e62eb7 Mon Sep 17 00:00:00 2001 From: Milutinke Date: Sat, 22 Oct 2022 14:21:35 +0200 Subject: [PATCH 12/15] Fix a crash with Discord Bridge when getting an empty message --- MinecraftClient/ChatBots/DiscordBridge.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index d5afe5e6..1d83cf5d 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -149,6 +149,10 @@ namespace MinecraftClient.ChatBots text = GetVerbatim(text).Trim(); + // Stop the crash when an empty text is recived somehow + if (string.IsNullOrEmpty(text)) + return; + string message = ""; string username = ""; bool teleportRequest = false; @@ -185,7 +189,7 @@ namespace MinecraftClient.ChatBots public void SendMessage(string message) { - if (!CanSendMessages()) + if (!CanSendMessages() || string.IsNullOrEmpty(message)) return; _client!.SendMessageAsync(_channel, message).Wait(); @@ -215,7 +219,10 @@ namespace MinecraftClient.ChatBots using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { filePath = filePath[(filePath.IndexOf(Path.DirectorySeparatorChar) + 1)..]; - var messageBuilder = new DiscordMessageBuilder().WithContent(text); + var messageBuilder = new DiscordMessageBuilder(); + + if (text != null) + messageBuilder.WithContent(text); messageBuilder.WithFiles(new Dictionary() { { $"attachment://{filePath}", fs } }); @@ -305,6 +312,9 @@ namespace MinecraftClient.ChatBots string message = e.Message.Content.Trim(); + if (string.IsNullOrEmpty(message) || string.IsNullOrWhiteSpace(message)) + return; + if (bridgeDirection == BridgeDirection.Discord) { if (!message.StartsWith(".dscbridge")) From 9f5b7d50df103e8be4e82a3e9a8c04b9b1242306 Mon Sep 17 00:00:00 2001 From: Milutinke Date: Sat, 22 Oct 2022 16:34:26 +0200 Subject: [PATCH 13/15] Another bug fix and added safeguards to the Discord Bridge chat bot. --- MinecraftClient/ChatBots/DiscordBridge.cs | 87 +++++++++++++++++------ MinecraftClient/Resources/lang/en.ini | 2 + 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index 1d83cf5d..3140af98 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -49,12 +49,18 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$config.ChatBot.DiscordBridge.OwnersIds$")] public ulong[] OwnersIds = new[] { 978757810781323276UL }; + [TomlInlineComment("$config.ChatBot.DiscordBridge.MessageSendTimeout$")] + public int Message_Send_Timeout = 3; + [TomlPrecedingComment("$config.ChatBot.DiscordBridge.Formats$")] public string PrivateMessageFormat = "**[Private Message]** {username}: {message}"; public string PublicMessageFormat = "{username}: {message}"; public string TeleportRequestMessageFormat = "A new Teleport Request from **{username}**!"; - public void OnSettingUpdate() { } + public void OnSettingUpdate() + { + Message_Send_Timeout = Message_Send_Timeout <= 0 ? 3 : Message_Send_Timeout; + } } public DiscordBridge() @@ -83,12 +89,20 @@ namespace MinecraftClient.ChatBots { if (_client != null) { - if (_channel != null) - _client.SendMessageAsync(_channel, new DiscordEmbedBuilder - { - Description = Translations.TryGet("bot.DiscordBridge.disconnected"), - Color = new DiscordColor(0xFF0000) - }).Wait(); + try + { + if (_channel != null) + _client.SendMessageAsync(_channel, new DiscordEmbedBuilder + { + Description = Translations.TryGet("bot.DiscordBridge.disconnected"), + Color = new DiscordColor(0xFF0000) + }).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } _client.DisconnectAsync().Wait(); IsConnected = false; @@ -192,7 +206,15 @@ namespace MinecraftClient.ChatBots if (!CanSendMessages() || string.IsNullOrEmpty(message)) return; - _client!.SendMessageAsync(_channel, message).Wait(); + try + { + _client!.SendMessageAsync(_channel, message).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } } public void SendMessage(DiscordMessageBuilder builder) @@ -200,7 +222,15 @@ namespace MinecraftClient.ChatBots if (!CanSendMessages()) return; - _client!.SendMessageAsync(_channel, builder).Wait(); + try + { + _client!.SendMessageAsync(_channel, builder).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } } public void SendMessage(DiscordEmbedBuilder embedBuilder) @@ -208,25 +238,40 @@ namespace MinecraftClient.ChatBots if (!CanSendMessages()) return; - _client!.SendMessageAsync(_channel, embedBuilder).Wait(); - + try + { + _client!.SendMessageAsync(_channel, embedBuilder).Wait(Config.Message_Send_Timeout * 1000); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); + } } public void SendImage(string filePath, string? text = null) { if (!CanSendMessages()) return; - using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + try { - filePath = filePath[(filePath.IndexOf(Path.DirectorySeparatorChar) + 1)..]; - var messageBuilder = new DiscordMessageBuilder(); + using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + filePath = filePath[(filePath.IndexOf(Path.DirectorySeparatorChar) + 1)..]; + var messageBuilder = new DiscordMessageBuilder(); - if (text != null) - messageBuilder.WithContent(text); + if (text != null) + messageBuilder.WithContent(text); - messageBuilder.WithFiles(new Dictionary() { { $"attachment://{filePath}", fs } }); + messageBuilder.WithFiles(new Dictionary() { { $"attachment://{filePath}", fs } }); - _client!.SendMessageAsync(_channel, messageBuilder).Wait(); + _client!.SendMessageAsync(_channel, messageBuilder).Wait(Config.Message_Send_Timeout * 1000); + } + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.canceled_sending")); + LogDebugToConsole(e); } } @@ -235,8 +280,7 @@ namespace MinecraftClient.ChatBots if (!CanSendMessages()) return; - var messageBuilder = new DiscordMessageBuilder().WithFile(fileStream); - SendMessage(messageBuilder); + SendMessage(new DiscordMessageBuilder().WithFile(fileStream)); } private bool CanSendMessages() @@ -356,11 +400,12 @@ namespace MinecraftClient.ChatBots }); IsConnected = true; + LogToConsole("§y§l§f" + Translations.TryGet("bot.DiscordBridge.connected")); await Task.Delay(-1); } catch (Exception e) { - LogToConsole(Translations.TryGet("bot.DiscordBridge.unknown_error")); + LogToConsole("§w§l§f" + Translations.TryGet("bot.DiscordBridge.unknown_error")); LogToConsole(e); return; } diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index e9ef41f6..305e9ef1 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -622,6 +622,7 @@ bot.DiscordBridge.missing_token=Please provide a valid token! bot.DiscordBridge.guild_not_found=The provided guild/server with an id '{0}' has not been found! bot.DiscordBridge.channel_not_found=The provided channel with an id '{0}' has not been found! bot.DiscordBridge.unknown_error=An unknown error has occured! +bot.DiscordBridge.canceled_sending=Sending message to Discord was canceled due an error occuring. For more info enable Debug. bot.DiscordBridge.desc=This command allows you to specify in the which direction the messages will be relayed via the Discord Bridge chat bot. bot.DiscordBridge.invalid_direction=Invalid direction provided! Avaliable directions: both|b, minecraft|mc, discord|dsc. Example: "dscbridge direction mc" bot.DiscordBridge.direction=Direction of the Discord Brdige has been switched to '{0}'! @@ -960,6 +961,7 @@ config.ChatBot.DiscordBridge.Token=Your Discord Bot token. config.ChatBot.DiscordBridge.GuildId=The ID of a server/guild where you have invited the bot to. config.ChatBot.DiscordBridge.ChannelId=The ID of a channel where you want to interact with the MCC using the bot. config.ChatBot.DiscordBridge.OwnersIds=A list of IDs of people you want to be able to interact with the MCC using the bot. +config.ChatBot.DiscordBridge.MessageSendTimeout=How long to wait (in seconds) if a message can not be sent to discord before canceling the task (minimum 1 second). config.ChatBot.DiscordBridge.Formats=Message formats\n# Words wrapped with { and } are going to be replaced during the code execution, do not change them!\n# For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time.\n# For Discord message formatting, check the following: https://bit.ly/3F8CUCm # ChatBot.Farmer From 1272ffda0b70b875bdc01ff1c8e5876d2b9fa21a Mon Sep 17 00:00:00 2001 From: Milutinke Date: Mon, 24 Oct 2022 15:35:24 +0200 Subject: [PATCH 14/15] Added a Telegram Bridge chat bot. --- MinecraftClient/ChatBots/DiscordBridge.cs | 56 ++-- MinecraftClient/ChatBots/TelegramBridge.cs | 291 +++++++++++++++++++++ MinecraftClient/McClient.cs | 1 + MinecraftClient/MinecraftClient.csproj | 1 + MinecraftClient/Resources/lang/en.ini | 24 +- MinecraftClient/Settings.cs | 7 + 6 files changed, 351 insertions(+), 29 deletions(-) create mode 100644 MinecraftClient/ChatBots/TelegramBridge.cs diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs index 3140af98..c7d327b9 100644 --- a/MinecraftClient/ChatBots/DiscordBridge.cs +++ b/MinecraftClient/ChatBots/DiscordBridge.cs @@ -11,20 +11,20 @@ using Tomlet.Attributes; namespace MinecraftClient.ChatBots { - internal enum BridgeDirection - { - Both = 0, - Minecraft, - Discord - } - public class DiscordBridge : ChatBot { + private enum BridgeDirection + { + Both = 0, + Minecraft, + Discord + } + private static DiscordBridge? instance = null; public bool IsConnected { get; private set; } - private DiscordClient? _client; - private DiscordChannel? _channel; + private DiscordClient? discordBotClient; + private DiscordChannel? discordChannel; private BridgeDirection bridgeDirection = BridgeDirection.Both; public static Configs Config = new(); @@ -87,12 +87,12 @@ namespace MinecraftClient.ChatBots private void Disconnect() { - if (_client != null) + if (discordBotClient != null) { try { - if (_channel != null) - _client.SendMessageAsync(_channel, new DiscordEmbedBuilder + if (discordChannel != null) + discordBotClient.SendMessageAsync(discordChannel, new DiscordEmbedBuilder { Description = Translations.TryGet("bot.DiscordBridge.disconnected"), Color = new DiscordColor(0xFF0000) @@ -104,7 +104,7 @@ namespace MinecraftClient.ChatBots LogDebugToConsole(e); } - _client.DisconnectAsync().Wait(); + discordBotClient.DisconnectAsync().Wait(); IsConnected = false; } } @@ -208,7 +208,7 @@ namespace MinecraftClient.ChatBots try { - _client!.SendMessageAsync(_channel, message).Wait(Config.Message_Send_Timeout * 1000); + discordBotClient!.SendMessageAsync(discordChannel, message).Wait(Config.Message_Send_Timeout * 1000); } catch (Exception e) { @@ -224,7 +224,7 @@ namespace MinecraftClient.ChatBots try { - _client!.SendMessageAsync(_channel, builder).Wait(Config.Message_Send_Timeout * 1000); + discordBotClient!.SendMessageAsync(discordChannel, builder).Wait(Config.Message_Send_Timeout * 1000); } catch (Exception e) { @@ -240,7 +240,7 @@ namespace MinecraftClient.ChatBots try { - _client!.SendMessageAsync(_channel, embedBuilder).Wait(Config.Message_Send_Timeout * 1000); + discordBotClient!.SendMessageAsync(discordChannel, embedBuilder).Wait(Config.Message_Send_Timeout * 1000); } catch (Exception e) { @@ -265,7 +265,7 @@ namespace MinecraftClient.ChatBots messageBuilder.WithFiles(new Dictionary() { { $"attachment://{filePath}", fs } }); - _client!.SendMessageAsync(_channel, messageBuilder).Wait(Config.Message_Send_Timeout * 1000); + discordBotClient!.SendMessageAsync(discordChannel, messageBuilder).Wait(Config.Message_Send_Timeout * 1000); } } catch (Exception e) @@ -285,7 +285,7 @@ namespace MinecraftClient.ChatBots private bool CanSendMessages() { - return _client == null || _channel == null || bridgeDirection == BridgeDirection.Minecraft ? false : true; + return discordBotClient == null || discordChannel == null || bridgeDirection == BridgeDirection.Minecraft ? false : true; } async Task MainAsync() @@ -299,7 +299,7 @@ namespace MinecraftClient.ChatBots return; } - _client = new DiscordClient(new DiscordConfiguration() + discordBotClient = new DiscordClient(new DiscordConfiguration() { Token = Config.Token.Trim(), TokenType = TokenType.Bot, @@ -311,7 +311,7 @@ namespace MinecraftClient.ChatBots try { - await _client.GetGuildAsync(Config.GuildId); + await discordBotClient.GetGuildAsync(Config.GuildId); } catch (Exception e) { @@ -328,7 +328,7 @@ namespace MinecraftClient.ChatBots try { - _channel = await _client.GetChannelAsync(Config.ChannelId); + discordChannel = await discordBotClient.GetChannelAsync(Config.ChannelId); } catch (Exception e) { @@ -343,7 +343,7 @@ namespace MinecraftClient.ChatBots LogDebugToConsole(e); } - _client.MessageCreated += async (source, e) => + discordBotClient.MessageCreated += async (source, e) => { if (e.Guild.Id != Config.GuildId) return; @@ -368,20 +368,20 @@ namespace MinecraftClient.ChatBots if (message.StartsWith(".")) { message = message[1..]; - await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); + await e.Message.CreateReactionAsync(DiscordEmoji.FromName(discordBotClient, ":gear:")); string? result = ""; PerformInternalCommand(message, ref result); result = string.IsNullOrEmpty(result) ? "-" : result; - await e.Message.DeleteOwnReactionAsync(DiscordEmoji.FromName(_client, ":gear:")); - await e.Message.CreateReactionAsync(DiscordEmoji.FromName(_client, ":white_check_mark:")); + await e.Message.DeleteOwnReactionAsync(DiscordEmoji.FromName(discordBotClient, ":gear:")); + await e.Message.CreateReactionAsync(DiscordEmoji.FromName(discordBotClient, ":white_check_mark:")); await e.Message.RespondAsync($"{Translations.TryGet("bot.DiscordBridge.command_executed")}:\n```{result}```"); } else SendText(message); }; - _client.ComponentInteractionCreated += async (s, e) => + discordBotClient.ComponentInteractionCreated += async (s, e) => { if (!(e.Id.Equals("accept_teleport") || e.Id.Equals("deny_teleport"))) return; @@ -391,9 +391,9 @@ namespace MinecraftClient.ChatBots await e.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().WithContent(result)); }; - await _client.ConnectAsync(); + await discordBotClient.ConnectAsync(); - await _client.SendMessageAsync(_channel, new DiscordEmbedBuilder + await discordBotClient.SendMessageAsync(discordChannel, new DiscordEmbedBuilder { Description = Translations.TryGet("bot.DiscordBridge.connected"), Color = new DiscordColor(0x00FF00) diff --git a/MinecraftClient/ChatBots/TelegramBridge.cs b/MinecraftClient/ChatBots/TelegramBridge.cs new file mode 100644 index 00000000..dd4ea4ab --- /dev/null +++ b/MinecraftClient/ChatBots/TelegramBridge.cs @@ -0,0 +1,291 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Telegram.Bot; +using Telegram.Bot.Exceptions; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Tomlet.Attributes; + +namespace MinecraftClient.ChatBots +{ + public class TelegramBridge : ChatBot + { + private enum BridgeDirection + { + Both = 0, + Minecraft, + Telegram + } + + private static TelegramBridge? instance = null; + public bool IsConnected { get; private set; } + + private TelegramBotClient? botClient; + private CancellationTokenSource? cancellationToken; + private BridgeDirection bridgeDirection = BridgeDirection.Both; + + public static Configs Config = new(); + + [TomlDoNotInlineObject] + public class Configs + { + [NonSerialized] + private const string BotName = "TelegramBridge"; + + public bool Enabled = false; + + [TomlInlineComment("$config.ChatBot.TelegramBridge.Token$")] + public string Token = "your bot token here"; + + [TomlInlineComment("$config.ChatBot.TelegramBridge.ChannelId$")] + public string ChannelId = ""; + + [TomlInlineComment("$config.ChatBot.TelegramBridge.MessageSendTimeout$")] + public int Message_Send_Timeout = 3; + + [TomlPrecedingComment("$config.ChatBot.TelegramBridge.Formats$")] + public string PrivateMessageFormat = "**[Private Message]** {username}: {message}"; + public string PublicMessageFormat = "{username}: {message}"; + public string TeleportRequestMessageFormat = "A new Teleport Request from **{username}**!"; + + public void OnSettingUpdate() + { + Message_Send_Timeout = Message_Send_Timeout <= 0 ? 3 : Message_Send_Timeout; + } + } + + public TelegramBridge() + { + instance = this; + } + + public override void Initialize() + { + RegisterChatBotCommand("tgbridge", "bot.TelegramBridge.desc", "tgbridge direction ", OnTgCommand); + + Task.Run(async () => await MainAsync()); + } + + ~TelegramBridge() + { + Disconnect(); + } + + public override void OnUnload() + { + Disconnect(); + } + + private void Disconnect() + { + if (botClient != null) + { + try + { + SendMessage(Translations.TryGet("bot.TelegramBridge.disconnected")); + cancellationToken?.Cancel(); + botClient = null; + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.canceled_sending")); + LogDebugToConsole(e); + } + + IsConnected = false; + } + } + + public static TelegramBridge? GetInstance() + { + return instance; + } + + private string OnTgCommand(string cmd, string[] args) + { + if (args.Length == 2) + { + if (args[0].ToLower().Equals("direction")) + { + string direction = args[1].ToLower().Trim(); + + string? bridgeName = ""; + + switch (direction) + { + case "b": + case "both": + bridgeName = "bot.TelegramBridge.direction.both"; + bridgeDirection = BridgeDirection.Both; + break; + + case "mc": + case "minecraft": + bridgeName = "bot.TelegramBridge.direction.minecraft"; + bridgeDirection = BridgeDirection.Minecraft; + break; + + case "t": + case "tg": + case "telegram": + bridgeName = "bot.TelegramBridge.direction.discord"; + bridgeDirection = BridgeDirection.Telegram; + break; + + default: + return Translations.TryGet("bot.TelegramBridge.invalid_direction"); + } + + return Translations.TryGet("bot.TelegramBridge.direction", Translations.TryGet(bridgeName)); + }; + } + + return "dscbridge direction "; + } + + public override void GetText(string text) + { + if (!CanSendMessages()) + return; + + text = GetVerbatim(text).Trim(); + + // Stop the crash when an empty text is recived somehow + if (string.IsNullOrEmpty(text)) + return; + + string message = ""; + string username = ""; + + if (IsPrivateMessage(text, ref message, ref username)) + message = Config.PrivateMessageFormat.Replace("{username}", username).Replace("{message}", message).Replace("{timestamp}", GetTimestamp()).Trim(); + else if (IsChatMessage(text, ref message, ref username)) + message = Config.PublicMessageFormat.Replace("{username}", username).Replace("{message}", message).Replace("{timestamp}", GetTimestamp()).Trim(); + else if (IsTeleportRequest(text, ref username)) + message = Config.TeleportRequestMessageFormat.Replace("{username}", username).Replace("{timestamp}", GetTimestamp()).Trim(); + + else message = text; + + SendMessage(message); + } + + public void SendMessage(string message) + { + if (!CanSendMessages() || string.IsNullOrEmpty(message)) + return; + + try + { + botClient!.SendTextMessageAsync(Config.ChannelId.Trim(), message).Wait(Config.Message_Send_Timeout); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.canceled_sending")); + LogDebugToConsole(e); + } + } + + private bool CanSendMessages() + { + return botClient != null && !string.IsNullOrEmpty(Config.ChannelId.Trim()) && bridgeDirection != BridgeDirection.Minecraft; + } + + async Task MainAsync() + { + try + { + if (string.IsNullOrEmpty(Config.Token.Trim())) + { + LogToConsole(Translations.TryGet("bot.TelegramBridge.missing_token")); + UnloadBot(); + return; + } + + if (string.IsNullOrEmpty(Config.ChannelId.Trim())) + LogToConsole("§x§l§4" + Translations.TryGet("bot.TelegramBridge.missing_channel_id")); + + botClient = new TelegramBotClient(Config.Token.Trim()); + cancellationToken = new CancellationTokenSource(); + + botClient.StartReceiving( + updateHandler: HandleUpdateAsync, + pollingErrorHandler: HandlePollingErrorAsync, + receiverOptions: new ReceiverOptions + { + // receive all update types + AllowedUpdates = Array.Empty() + }, + cancellationToken: cancellationToken.Token + ); + + var me = await botClient.GetMeAsync(); + IsConnected = true; + + SendMessage(Translations.TryGet("bot.TelegramBridge.connected")); + LogToConsole("§y§l§f" + Translations.TryGet("bot.TelegramBridge.connected", me.Username)); + await Task.Delay(-1); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.unknown_error")); + LogToConsole(e); + return; + } + } + + private async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken _cancellationToken) + { + // Only process Message updates: https://core.telegram.org/bots/api#message + if (update.Message is not { } message) + return; + + // Only process text messages + if (message.Text is not { } messageText) + return; + + var chatId = message.Chat.Id; + var text = message.Text; + + if (string.IsNullOrEmpty(text) || string.IsNullOrWhiteSpace(text)) + return; + + if (text.ToLower().Contains("/start")) + return; + + LogDebugToConsole($"Received a '{messageText}' message in chat {chatId}."); + + if (bridgeDirection == BridgeDirection.Telegram) + { + if (!text.StartsWith(".dscbridge")) + return; + } + + if (text.StartsWith(".")) + { + var command = text[1..]; + + string? result = ""; + PerformInternalCommand(command, ref result); + result = string.IsNullOrEmpty(result) ? "-" : result; + + await botClient.SendTextMessageAsync(chatId: chatId, replyToMessageId: message.MessageId, text: $"{Translations.TryGet("bot.TelegramBridge.command_executed")}:\n\n{result}", cancellationToken: _cancellationToken); + } + else SendText(text); + } + + private Task HandlePollingErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken _cancellationToken) + { + var ErrorMessage = exception switch + { + ApiRequestException apiRequestException + => $"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}", + _ => exception.ToString() + }; + + LogToConsole("§w§l§f" + ErrorMessage); + return Task.CompletedTask; + } + } +} diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs index f829745d..16bb2e9d 100644 --- a/MinecraftClient/McClient.cs +++ b/MinecraftClient/McClient.cs @@ -274,6 +274,7 @@ namespace MinecraftClient if (Config.ChatBot.RemoteControl.Enabled) { BotLoad(new RemoteControl()); } if (Config.ChatBot.ReplayCapture.Enabled && reload) { BotLoad(new ReplayCapture()); } if (Config.ChatBot.ScriptScheduler.Enabled) { BotLoad(new ScriptScheduler()); } + if (Config.ChatBot.TelegramBridge.Enabled) { BotLoad(new TelegramBridge()); } //Add your ChatBot here by uncommenting and adapting //BotLoad(new ChatBots.YourBot()); } diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj index 4387bd2a..013e6801 100644 --- a/MinecraftClient/MinecraftClient.csproj +++ b/MinecraftClient/MinecraftClient.csproj @@ -47,6 +47,7 @@ NU1701 + diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index 305e9ef1..b04ef367 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -624,7 +624,7 @@ bot.DiscordBridge.channel_not_found=The provided channel with an id '{0}' has no bot.DiscordBridge.unknown_error=An unknown error has occured! bot.DiscordBridge.canceled_sending=Sending message to Discord was canceled due an error occuring. For more info enable Debug. bot.DiscordBridge.desc=This command allows you to specify in the which direction the messages will be relayed via the Discord Bridge chat bot. -bot.DiscordBridge.invalid_direction=Invalid direction provided! Avaliable directions: both|b, minecraft|mc, discord|dsc. Example: "dscbridge direction mc" +bot.DiscordBridge.invalid_direction=Invalid direction provided! Available directions: both|b, minecraft|mc, discord|dsc. Example: "dscbridge direction mc" bot.DiscordBridge.direction=Direction of the Discord Brdige has been switched to '{0}'! bot.DiscordBridge.direction.both=Both bot.DiscordBridge.direction.minecraft=Minecraft @@ -751,6 +751,21 @@ bot.scriptScheduler.running_inverval=Interval / Running action: {0} bot.scriptScheduler.running_login=Login / Running action: {0} bot.scriptScheduler.task=triggeronfirstlogin: {0}\n triggeronlogin: {1}\n triggerontime: {2}\n triggeroninterval: {3}\n timevalue: {4}\n timeinterval: {5}\n action: {6} +# TelegramBridge +botname.TelegramBridge=TelegramBridge +bot.TelegramBridge.command_executed=The command was executed with the result +bot.TelegramBridge.connected=Succesfully connected with MCC! +bot.TelegramBridge.missing_token=Please provide a valid bot token! +bot.TelegramBridge.missing_channel_id=[WARNING] You have not provided a Channel ID, you will ONLY get replies to commands sent from Telegram! +bot.TelegramBridge.unknown_error=An unknown error has occured! +bot.TelegramBridge.canceled_sending=Sending message to Telegram was canceled due an error occuring. For more info enable Debug. +bot.TelegramBridge.desc=This command allows you to specify in the which direction the messages will be relayed via the Telegram Bridge chat bot. +bot.TelegramBridge.invalid_direction=Invalid direction provided! Available directions: both|b, minecraft|mc, telegram|tg|t. Example: "tgbridge direction mc" +bot.TelegramBridge.direction=Direction of the Telegram Brdige has been switched to '{0}'! +bot.TelegramBridge.direction.both=Both +bot.TelegramBridge.direction.minecraft=Minecraft +bot.TelegramBridge.direction.Telegram=Telegram + # TestBot botname.TestBot=TestBot bot.testBot.told=Bot: {0} told me : {1} @@ -1003,3 +1018,10 @@ config.ChatBot.ReplayCapture.Backup_Interval=How long should replay file be auto # ChatBot.ScriptScheduler config.ChatBot.ScriptScheduler=Schedule commands and scripts to launch on various events such as server join, date/time or time interval\n# See https://mccteam.github.io/guide/chat-bots.html#script-scheduler for more info + +# ChatBot.TelegramBridge +config.ChatBot.TelegramBridge=This bot allows you to send and receive messages and commands via a Telegram Bot DM or to receive messages in a Telegram channel.\n# /!\ NOTE: You can't send messages and commands from a group channel, you can only send them in the bot DM, but you can get the messages from the client in a group channel.\n#-----------------------------------------------------------\n# Setup:\n# First you need to create a Telegram bot and obtain an API key, to do so, go to Telegram and find @botfather\n# Click on "Start" button and read the bot reply, then type "/newbot", the Botfather will guide you through the bot creation.\n# Once you create the bot, copy the API key that you have gotten, and put it into the "Token" field of "ChatBot.TelegramBridge" section (this section).\n# /!\ Do not share this token with anyone else as it will give them the control over your bot. Save it securely.\n# Set the "Enabled" field to true (in this section), then enable "DebugMessages" by setting it to true, in the "Logging" section and save the file.\n# Then launch the client and go to Telegram, find your newly created bot by searching for it with its username, and open a DM with it.\n# Click on "Start" button and type and send any message to the bot, then in the client console you should see something like: "[MCC] [TelegramBridge] Received a 'e' message in chat 2127848600."\n# Copy the chat number (eg. 2127848600) and paste it in the "ChannelId" field (in this section), disable "DebugMessages" if you want and save the file.\n# Now you can use the bot using it's DM.\n#-----------------------------------------------------------\n# NOTE: If you want to recieve messages to a group channel instead, make the channel temporarely public, invite the bot to it and make it an administrator, then set the channel to private if you want.\n# Then set the "ChannelId" field to the @ of your channel (you must include the @ in the settings, eg. "@mysupersecretchannel"), this is the username you can see in the invite link of the channel.\n# /!\ Only include the username with @ prefix, do not include the rest of the link. Example if you have "https://t.me/mysupersecretchannel", the "ChannelId" will be "@mysupersecretchannel".\n# /!\ Note that you will not be able to send messages to the client from a group channel!\n#-----------------------------------------------------------\n# How to use the bot:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. +config.ChatBot.TelegramBridge.Token=Your Telegram Bot token. +config.ChatBot.TelegramBridge.ChannelId=The ID of a channel where you want to interact with the MCC using the bot. +config.ChatBot.TelegramBridge.MessageSendTimeout=How long to wait (in seconds) if a message can not be sent to Telegram before canceling the task (minimum 1 second). +config.ChatBot.TelegramBridge.Formats=Message formats\n# Words wrapped with { and } are going to be replaced during the code execution, do not change them!\n# For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time.\n# For Telegram message formatting, check the following: https://sendpulse.com/blog/telegram-text-formatting \ No newline at end of file diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs index c0115a29..39e4795a 100644 --- a/MinecraftClient/Settings.cs +++ b/MinecraftClient/Settings.cs @@ -1163,6 +1163,13 @@ namespace MinecraftClient get { return ChatBots.ScriptScheduler.Config; } set { ChatBots.ScriptScheduler.Config = value; ChatBots.ScriptScheduler.Config.OnSettingUpdate(); } } + + [TomlPrecedingComment("$config.ChatBot.TelegramBridge$")] + public ChatBots.TelegramBridge.Configs TelegramBridge + { + get { return ChatBots.TelegramBridge.Config; } + set { ChatBots.TelegramBridge.Config = value; ChatBots.TelegramBridge.Config.OnSettingUpdate(); } + } } } From 3c6de23d614a7e9c2af087bc012411fa99de91d9 Mon Sep 17 00:00:00 2001 From: Milutinke Date: Mon, 24 Oct 2022 18:39:07 +0200 Subject: [PATCH 15/15] Added authorization/security and option to send rendered maps to Telegram via Telegram Bridge chat bot. --- MinecraftClient/ChatBots/Map.cs | 75 +++++++++++++------ MinecraftClient/ChatBots/TelegramBridge.cs | 83 +++++++++++++++++++--- MinecraftClient/Resources/lang/en.ini | 15 ++-- 3 files changed, 139 insertions(+), 34 deletions(-) diff --git a/MinecraftClient/ChatBots/Map.cs b/MinecraftClient/ChatBots/Map.cs index ae612e42..a004b82f 100644 --- a/MinecraftClient/ChatBots/Map.cs +++ b/MinecraftClient/ChatBots/Map.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Text; +using System.Threading.Tasks; using ImageMagick; using MinecraftClient.Mapping; using Tomlet.Attributes; @@ -13,7 +15,7 @@ namespace MinecraftClient.ChatBots { public static Configs Config = new(); - public struct DiscordMap + public struct QueuedMap { public string FileName; public int MapId; @@ -48,8 +50,9 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$config.ChatBot.Map.Resize_To$")] public int Resize_To = 512; - [TomlPrecedingComment("$config.ChatBot.Map.Send_Rendered_To_Discord$")] + [TomlPrecedingComment("$config.ChatBot.Map.Send_Rendered_To_Bridges$")] public bool Send_Rendered_To_Discord = false; + public bool Send_Rendered_To_Telegram = false; public void OnSettingUpdate() { @@ -62,7 +65,7 @@ namespace MinecraftClient.ChatBots private readonly Dictionary cachedMaps = new(); - private readonly Queue discordQueue = new(); + private readonly Queue discordQueue = new(); public override void Initialize() { @@ -244,45 +247,75 @@ namespace MinecraftClient.ChatBots } } - if (Config.Send_Rendered_To_Discord) + if (Config.Send_Rendered_To_Discord || Config.Send_Rendered_To_Telegram) { - // We need to queue up images because Discord Bridge is not ready immediatelly - if (DiscordBridge.Config.Enabled) - discordQueue.Enqueue(new DiscordMap { FileName = fileName, MapId = map.MapId }); + // We need to queue up images because Discord/Telegram Bridge is not ready immediatelly + if (DiscordBridge.Config.Enabled || TelegramBridge.Config.Enabled) + discordQueue.Enqueue(new QueuedMap { FileName = fileName, MapId = map.MapId }); } } public override void Update() { - if (!DiscordBridge.Config.Enabled) - return; - DiscordBridge? discordBridge = DiscordBridge.GetInstance(); + TelegramBridge? telegramBridge = TelegramBridge.GetInstance(); - if (discordBridge == null) - return; + if (Config.Send_Rendered_To_Discord) + { + if (discordBridge == null || (discordBridge != null && !discordBridge.IsConnected)) + return; + } - if (!discordBridge.IsConnected) - return; + if (Config.Send_Rendered_To_Telegram) + { + if (telegramBridge == null || (telegramBridge != null && !telegramBridge.IsConnected)) + return; + } if (discordQueue.Count > 0) { - DiscordMap discordMap = discordQueue.Dequeue(); - string fileName = discordMap.FileName; + QueuedMap map = discordQueue.Dequeue(); + string fileName = map.FileName; // We must convert to a PNG in order to send to Discord, BMP does not work string newFileName = fileName.Replace(".bmp", ".png"); using (var image = new MagickImage(fileName)) { image.Write(newFileName); - discordBridge.SendImage(newFileName, $"> A render of the map with an id: **{discordMap.MapId}**"); + + if (Config.Send_Rendered_To_Discord) + discordBridge!.SendImage(newFileName, $"> A render of the map with an id: **{map.MapId}**"); + + if (Config.Send_Rendered_To_Telegram) + telegramBridge!.SendImage(newFileName, $"A render of the map with an id: *{map.MapId}*"); + newFileName = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + newFileName; - // Delete the temporary file - if (File.Exists(newFileName)) - File.Delete(newFileName); + if (Config.Send_Rendered_To_Discord) + LogToConsole(Translations.TryGet("bot.map.sent_to_discord", map.MapId)); - LogToConsole(Translations.TryGet("bot.map.sent_to_discord", discordMap.MapId)); + if (Config.Send_Rendered_To_Telegram) + LogToConsole(Translations.TryGet("bot.map.sent_to_telegram", map.MapId)); + + // Wait for 2 seconds and then try until file is free for deletion + // 10 seconds timeout + Task.Run(async () => + { + await Task.Delay(2000); + + var time = Stopwatch.StartNew(); + + while (time.ElapsedMilliseconds < 10000) // 10 seconds + { + try + { + // Delete the temporary file + if (File.Exists(newFileName)) + File.Delete(newFileName); + } + catch (IOException e) { } + } + }); } } } diff --git a/MinecraftClient/ChatBots/TelegramBridge.cs b/MinecraftClient/ChatBots/TelegramBridge.cs index dd4ea4ab..7b9f56d9 100644 --- a/MinecraftClient/ChatBots/TelegramBridge.cs +++ b/MinecraftClient/ChatBots/TelegramBridge.cs @@ -1,12 +1,18 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using DSharpPlus.Entities; using Telegram.Bot; using Telegram.Bot.Exceptions; using Telegram.Bot.Polling; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.InputFiles; using Tomlet.Attributes; +using File = System.IO.File; namespace MinecraftClient.ChatBots { @@ -42,11 +48,14 @@ namespace MinecraftClient.ChatBots [TomlInlineComment("$config.ChatBot.TelegramBridge.ChannelId$")] public string ChannelId = ""; + [TomlInlineComment("$config.ChatBot.TelegramBridge.Authorized_Chat_Ids$")] + public long[] Authorized_Chat_Ids = Array.Empty(); + [TomlInlineComment("$config.ChatBot.TelegramBridge.MessageSendTimeout$")] public int Message_Send_Timeout = 3; [TomlPrecedingComment("$config.ChatBot.TelegramBridge.Formats$")] - public string PrivateMessageFormat = "**[Private Message]** {username}: {message}"; + public string PrivateMessageFormat = "*(Private Message)* {username}: {message}"; public string PublicMessageFormat = "{username}: {message}"; public string TeleportRequestMessageFormat = "A new Teleport Request from **{username}**!"; @@ -178,7 +187,30 @@ namespace MinecraftClient.ChatBots try { - botClient!.SendTextMessageAsync(Config.ChannelId.Trim(), message).Wait(Config.Message_Send_Timeout); + botClient!.SendTextMessageAsync(Config.ChannelId.Trim(), message, ParseMode.Markdown).Wait(Config.Message_Send_Timeout); + } + catch (Exception e) + { + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.canceled_sending")); + LogDebugToConsole(e); + } + } + + public void SendImage(string filePath, string? text = null) + { + if (!CanSendMessages()) + return; + + try + { + string fileName = filePath[(filePath.IndexOf(Path.DirectorySeparatorChar) + 1)..]; + + Stream stream = File.OpenRead(filePath); + botClient!.SendDocumentAsync( + Config.ChannelId.Trim(), + document: new InputOnlineFile(content: stream, fileName), + caption: text, + parseMode: ParseMode.Markdown).Wait(Config.Message_Send_Timeout * 1000); } catch (Exception e) { @@ -204,7 +236,7 @@ namespace MinecraftClient.ChatBots } if (string.IsNullOrEmpty(Config.ChannelId.Trim())) - LogToConsole("§x§l§4" + Translations.TryGet("bot.TelegramBridge.missing_channel_id")); + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.missing_channel_id")); botClient = new TelegramBotClient(Config.Token.Trim()); cancellationToken = new CancellationTokenSource(); @@ -220,11 +252,18 @@ namespace MinecraftClient.ChatBots cancellationToken: cancellationToken.Token ); - var me = await botClient.GetMeAsync(); IsConnected = true; - SendMessage(Translations.TryGet("bot.TelegramBridge.connected")); - LogToConsole("§y§l§f" + Translations.TryGet("bot.TelegramBridge.connected", me.Username)); + SendMessage("✅ " + Translations.TryGet("bot.TelegramBridge.connected")); + LogToConsole("§y§l§f" + Translations.TryGet("bot.TelegramBridge.connected")); + + if (Config.Authorized_Chat_Ids.Length == 0) + { + SendMessage("⚠️ *" + Translations.TryGet("bot.TelegramBridge.missing_authorized_channels") + "* ⚠️"); + LogToConsole("§w§l§f" + Translations.TryGet("bot.TelegramBridge.missing_authorized_channels")); + return; + } + await Task.Delay(-1); } catch (Exception e) @@ -254,7 +293,29 @@ namespace MinecraftClient.ChatBots if (text.ToLower().Contains("/start")) return; - LogDebugToConsole($"Received a '{messageText}' message in chat {chatId}."); + if (text.ToLower().Contains(".chatid")) + { + await botClient.SendTextMessageAsync(chatId: chatId, + replyToMessageId: message.MessageId, + text: $"Chat ID: {chatId}", + cancellationToken: _cancellationToken, + parseMode: ParseMode.Markdown); + return; + } + + if (Config.Authorized_Chat_Ids.Length > 0 && !Config.Authorized_Chat_Ids.Contains(chatId)) + { + LogDebugToConsole($"Unauthorized message '{messageText}' received in a chat with with an ID: {chatId} !"); + await botClient.SendTextMessageAsync( + chatId: chatId, + replyToMessageId: message.MessageId, + text: Translations.TryGet("bot.TelegramBridge.unauthorized"), + cancellationToken: _cancellationToken, + parseMode: ParseMode.Markdown); + return; + } + + LogDebugToConsole($"Received a '{messageText}' message in a chat with with an ID: {chatId} ."); if (bridgeDirection == BridgeDirection.Telegram) { @@ -270,7 +331,13 @@ namespace MinecraftClient.ChatBots PerformInternalCommand(command, ref result); result = string.IsNullOrEmpty(result) ? "-" : result; - await botClient.SendTextMessageAsync(chatId: chatId, replyToMessageId: message.MessageId, text: $"{Translations.TryGet("bot.TelegramBridge.command_executed")}:\n\n{result}", cancellationToken: _cancellationToken); + await botClient.SendTextMessageAsync( + chatId: chatId, + replyToMessageId: + message.MessageId, + text: $"{Translations.TryGet("bot.TelegramBridge.command_executed")}:\n\n{result}", + cancellationToken: _cancellationToken, + parseMode: ParseMode.Markdown); } else SendText(text); } diff --git a/MinecraftClient/Resources/lang/en.ini b/MinecraftClient/Resources/lang/en.ini index b04ef367..693fb469 100644 --- a/MinecraftClient/Resources/lang/en.ini +++ b/MinecraftClient/Resources/lang/en.ini @@ -719,7 +719,8 @@ bot.map.failed_to_render=Failed to render the map with id: '{0}' bot.map.list_item=- Map id: {0} (Last Updated: {1}) bot.map.scale=The size of the map is reduced from ({0}x{1}) to ({2}x{3}) due to the size limitation of the current terminal. bot.map.resized_rendered_image=Resized the rendered image of the map with id: '{0}' to {1}x{1}. -bot.map.sent_to_discord=Sent a rendered image of a map with an id '{0}' to the discord! +bot.map.sent_to_discord=Sent a rendered image of a map with an id '{0}' to the Discord via Discord Brdige chat bot! +bot.map.sent_to_telegram=Sent a rendered image of a map with an id '{0}' to the Telegram via Telegram Bridge chat bot! # PlayerListLogger botname.PlayerListLogger=PlayerListLogger @@ -754,9 +755,12 @@ bot.scriptScheduler.task=triggeronfirstlogin: {0}\n triggeronlogin: {1}\n trigge # TelegramBridge botname.TelegramBridge=TelegramBridge bot.TelegramBridge.command_executed=The command was executed with the result -bot.TelegramBridge.connected=Succesfully connected with MCC! +bot.TelegramBridge.connected=Succesfully connected with the MCC! +bot.TelegramBridge.disconnected=Disconnected from from the MCC! bot.TelegramBridge.missing_token=Please provide a valid bot token! bot.TelegramBridge.missing_channel_id=[WARNING] You have not provided a Channel ID, you will ONLY get replies to commands sent from Telegram! +bot.TelegramBridge.missing_authorized_channels=[WARNING] You have not provided any Channel IDs, for "Authorized_Chat_Ids" field, anyone who finds your bot will be able to send messages and commands to it! +bot.TelegramBridge.unauthorized=**🛑 Unauthorized access! 🛑\n\nAdd the ID of this chat to "Authorized_Chat_Ids" field in the configuration file to gain access!** bot.TelegramBridge.unknown_error=An unknown error has occured! bot.TelegramBridge.canceled_sending=Sending message to Telegram was canceled due an error occuring. For more info enable Debug. bot.TelegramBridge.desc=This command allows you to specify in the which direction the messages will be relayed via the Telegram Bridge chat bot. @@ -1003,7 +1007,7 @@ config.ChatBot.Map.Delete_All_On_Unload=Delete all rendered maps on unload/reloa config.ChatBot.Map.Notify_On_First_Update=Get a notification when you have gotten a map from the server for the first time config.ChatBot.Map.Resize_To=The size that a rendered image should be resized to, in pixels (eg. 512). config.ChatBot.Map.Rasize_Rendered_Image=Resize an rendered image, this is useful when images that are rendered are small and when are being sent to Discord. -config.ChatBot.Map.Send_Rendered_To_Discord=Send a rendered map (saved to a file) to a Discord channel via the Discord Bride chat bot (The Discord Bridge chat bot must be enabled and configured!)\n# You need to enable Save_To_File in order for this to work.\n# We also recommend turning on resizing. +config.ChatBot.Map.Send_Rendered_To_Bridges=Send a rendered map (saved to a file) to a Discord or a Telegram channel via the Discord or Telegram Bride chat bot (The Discord/Telegram Bridge chat bot must be enabled and configured!)\n# You need to enable Save_To_File in order for this to work.\n# We also recommend turning on resizing. # ChatBot.PlayerListLogger config.ChatBot.PlayerListLogger=Log the list of players periodically into a textual file. @@ -1020,8 +1024,9 @@ config.ChatBot.ReplayCapture.Backup_Interval=How long should replay file be auto config.ChatBot.ScriptScheduler=Schedule commands and scripts to launch on various events such as server join, date/time or time interval\n# See https://mccteam.github.io/guide/chat-bots.html#script-scheduler for more info # ChatBot.TelegramBridge -config.ChatBot.TelegramBridge=This bot allows you to send and receive messages and commands via a Telegram Bot DM or to receive messages in a Telegram channel.\n# /!\ NOTE: You can't send messages and commands from a group channel, you can only send them in the bot DM, but you can get the messages from the client in a group channel.\n#-----------------------------------------------------------\n# Setup:\n# First you need to create a Telegram bot and obtain an API key, to do so, go to Telegram and find @botfather\n# Click on "Start" button and read the bot reply, then type "/newbot", the Botfather will guide you through the bot creation.\n# Once you create the bot, copy the API key that you have gotten, and put it into the "Token" field of "ChatBot.TelegramBridge" section (this section).\n# /!\ Do not share this token with anyone else as it will give them the control over your bot. Save it securely.\n# Set the "Enabled" field to true (in this section), then enable "DebugMessages" by setting it to true, in the "Logging" section and save the file.\n# Then launch the client and go to Telegram, find your newly created bot by searching for it with its username, and open a DM with it.\n# Click on "Start" button and type and send any message to the bot, then in the client console you should see something like: "[MCC] [TelegramBridge] Received a 'e' message in chat 2127848600."\n# Copy the chat number (eg. 2127848600) and paste it in the "ChannelId" field (in this section), disable "DebugMessages" if you want and save the file.\n# Now you can use the bot using it's DM.\n#-----------------------------------------------------------\n# NOTE: If you want to recieve messages to a group channel instead, make the channel temporarely public, invite the bot to it and make it an administrator, then set the channel to private if you want.\n# Then set the "ChannelId" field to the @ of your channel (you must include the @ in the settings, eg. "@mysupersecretchannel"), this is the username you can see in the invite link of the channel.\n# /!\ Only include the username with @ prefix, do not include the rest of the link. Example if you have "https://t.me/mysupersecretchannel", the "ChannelId" will be "@mysupersecretchannel".\n# /!\ Note that you will not be able to send messages to the client from a group channel!\n#-----------------------------------------------------------\n# How to use the bot:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. +config.ChatBot.TelegramBridge=This bot allows you to send and receive messages and commands via a Telegram Bot DM or to receive messages in a Telegram channel.\n# /!\ NOTE: You can't send messages and commands from a group channel, you can only send them in the bot DM, but you can get the messages from the client in a group channel.\n#-----------------------------------------------------------\n# Setup:\n# First you need to create a Telegram bot and obtain an API key, to do so, go to Telegram and find @botfather\n# Click on "Start" button and read the bot reply, then type "/newbot", the Botfather will guide you through the bot creation.\n# Once you create the bot, copy the API key that you have gotten, and put it into the "Token" field of "ChatBot.TelegramBridge" section (this section).\n# /!\ Do not share this token with anyone else as it will give them the control over your bot. Save it securely.\n# Then launch the client and go to Telegram, find your newly created bot by searching for it with its username, and open a DM with it.\n# Click on "Start" button and type and send the following command ".chatid" to obtain the chat id. \n# Copy the chat id number (eg. 2627844670) and paste it in the "ChannelId" field and add it to the "Authorized_Chat_Ids" field (in this section) (an id in "Authorized_Chat_Ids" field is a number/long, not a string!), then save the file.\n# Now you can use the bot using it's DM.\n# /!\ If you do not add the id of your chat DM with the bot to the "Authorized_Chat_Ids" field, ayone who finds your bot via search will be able to execute commands and send messages!\n# /!\ An id pasted in to the "Authorized_Chat_Ids" should be a number/long, not a string!\n#-----------------------------------------------------------\n# NOTE: If you want to recieve messages to a group channel instead, make the channel temporarely public, invite the bot to it and make it an administrator, then set the channel to private if you want.\n# Then set the "ChannelId" field to the @ of your channel (you must include the @ in the settings, eg. "@mysupersecretchannel"), this is the username you can see in the invite link of the channel.\n# /!\ Only include the username with @ prefix, do not include the rest of the link. Example if you have "https://t.me/mysupersecretchannel", the "ChannelId" will be "@mysupersecretchannel".\n# /!\ Note that you will not be able to send messages to the client from a group channel!\n#-----------------------------------------------------------\n# How to use the bot:\n# To execute an MCC command, prefix it with a dot ".", example: ".move 143 64 735" .\n# To send a message, simply type it out and hit enter. config.ChatBot.TelegramBridge.Token=Your Telegram Bot token. -config.ChatBot.TelegramBridge.ChannelId=The ID of a channel where you want to interact with the MCC using the bot. +config.ChatBot.TelegramBridge.ChannelId=An ID of a channel where you want to interact with the MCC using the bot. +config.ChatBot.TelegramBridge.Authorized_Chat_Ids=A list of Chat IDs that are allowed to send messages and execute commands. To get an id of your chat DM with the bot use ".chatid" bot command in Telegram. config.ChatBot.TelegramBridge.MessageSendTimeout=How long to wait (in seconds) if a message can not be sent to Telegram before canceling the task (minimum 1 second). config.ChatBot.TelegramBridge.Formats=Message formats\n# Words wrapped with { and } are going to be replaced during the code execution, do not change them!\n# For example. {message} is going to be replace with an actual message, {username} will be replaced with an username, {timestamp} with the current time.\n# For Telegram message formatting, check the following: https://sendpulse.com/blog/telegram-text-formatting \ No newline at end of file