From 1272ffda0b70b875bdc01ff1c8e5876d2b9fa21a Mon Sep 17 00:00:00 2001 From: Milutinke Date: Mon, 24 Oct 2022 15:35:24 +0200 Subject: [PATCH 1/2] 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 2/2] 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