From df6268323c5c21f5fa91bca0574e5413be76e063 Mon Sep 17 00:00:00 2001 From: Slavasil Date: Sun, 20 Apr 2025 16:41:30 +0300 Subject: [PATCH] add part of the dialog and implement logging to console --- SimpleTGBot/DialogData.cs | 16 ++++ SimpleTGBot/Interactions.cs | 34 +++++++++ SimpleTGBot/Logging/LogLevel.cs | 19 +++++ SimpleTGBot/Logging/LogSink.cs | 6 ++ SimpleTGBot/Logging/Logger.cs | 34 +++++++++ SimpleTGBot/Logging/StdoutSink.cs | 23 ++++++ SimpleTGBot/Program.cs | 6 +- SimpleTGBot/TelegramBot.cs | 123 ++++++++++++++++++++++++------ 8 files changed, 238 insertions(+), 23 deletions(-) create mode 100644 SimpleTGBot/DialogData.cs create mode 100644 SimpleTGBot/Interactions.cs create mode 100644 SimpleTGBot/Logging/LogLevel.cs create mode 100644 SimpleTGBot/Logging/LogSink.cs create mode 100644 SimpleTGBot/Logging/Logger.cs create mode 100644 SimpleTGBot/Logging/StdoutSink.cs diff --git a/SimpleTGBot/DialogData.cs b/SimpleTGBot/DialogData.cs new file mode 100644 index 0000000..ecdf46f --- /dev/null +++ b/SimpleTGBot/DialogData.cs @@ -0,0 +1,16 @@ +namespace SimpleTGBot; + +internal class DialogData +{ + public DialogState state; + public string? inputPictureFilename; +} + +enum DialogState +{ + Initial, + AwaitingPicture, + AwaitingTitle, + AwaitingSubtitle, + ShowingResult, +} diff --git a/SimpleTGBot/Interactions.cs b/SimpleTGBot/Interactions.cs new file mode 100644 index 0000000..f4d3e90 --- /dev/null +++ b/SimpleTGBot/Interactions.cs @@ -0,0 +1,34 @@ +using Telegram.Bot.Types.ReplyMarkups; + +namespace SimpleTGBot; + +internal static class Interactions +{ + public const string awaitingPictureMessage = "Привет. Я - бот, который умеет делать демотиваторы. Присылай картинку, а я скажу, что делать дальше. Или можешь нажать на кнопку в меню."; + public const string sayHelloMessage = "Напиши \"привет\" или нажми на кнопку в меню, чтобы начать."; + public const string sendPictureOrQuitMessage = "Пришли мне картинку для демотиватора, чтобы продолжить. Чтобы отменить, напиши \"назад\" или \"\""; + + public static readonly IReplyMarkup initialReplyMarkup = new ReplyKeyboardMarkup([[new KeyboardButton("▶️Начать")]]); + public static readonly IReplyMarkup backButtonReplyMarkup = new ReplyKeyboardMarkup(new KeyboardButton("↩️Назад")); + public static readonly IReplyMarkup quickActionReplyMarkup = new ReplyKeyboardRemove(); + + static readonly string[] helloWords = ["прив","привет","▶️начать","ку","хай","приветик","превед","привки","хаюхай","здравствуй","здравствуйте","здорово","дарова","дороу","здарова","здорова"]; + static readonly string[] cancelWords = ["↩️назад", "назад", "выйти", "отмена", "отменить", "отменяй", "галя", "галина", "стоп"]; + + public static bool IsStartCommand(string message) + { + return message.Split(' ').FirstOrDefault() == "/start"; + } + + public static bool IsHello(string message) + { + string[] messageWords = message.ToLower().Split(new char[] { ' ', ',', '.', ';', '(', ')' }); + return helloWords.Any(word => messageWords.Contains(word)); + } + + public static bool IsCancellation(string message) + { + string[] messageWords = message.ToLower().Split(new char[] { ' ', ',', '.', ';', '(', ')' }); + return cancelWords.Any(word => messageWords.Contains(word)); + } +} diff --git a/SimpleTGBot/Logging/LogLevel.cs b/SimpleTGBot/Logging/LogLevel.cs new file mode 100644 index 0000000..671d16b --- /dev/null +++ b/SimpleTGBot/Logging/LogLevel.cs @@ -0,0 +1,19 @@ +namespace SimpleTGBot.Logging; + +internal enum LogLevel : int +{ + Debug = 0, Info, Warning, Error, Fatal +} +static class LogLevelExt +{ + public static string GetName(this LogLevel level) + { + return level switch { + LogLevel.Debug => "DEBUG", + LogLevel.Info => "INFO", + LogLevel.Warning => "WARN", + LogLevel.Error => "ERROR", + LogLevel.Fatal => "FATAL" + }; + } +} diff --git a/SimpleTGBot/Logging/LogSink.cs b/SimpleTGBot/Logging/LogSink.cs new file mode 100644 index 0000000..6aea239 --- /dev/null +++ b/SimpleTGBot/Logging/LogSink.cs @@ -0,0 +1,6 @@ +namespace SimpleTGBot.Logging; + +internal interface ILogSink : IDisposable +{ + public void Log(DateTime time, LogLevel level, string message); +} diff --git a/SimpleTGBot/Logging/Logger.cs b/SimpleTGBot/Logging/Logger.cs new file mode 100644 index 0000000..9973b35 --- /dev/null +++ b/SimpleTGBot/Logging/Logger.cs @@ -0,0 +1,34 @@ +namespace SimpleTGBot.Logging; + +internal class Logger : IDisposable +{ + public List Sinks; + + public Logger(params ILogSink[] sinks) + { + Sinks = new List(sinks); + } + + public void Log(LogLevel level, string message) + { + DateTime now = DateTime.Now; + foreach (var sink in Sinks) + { + sink.Log(now, level, message); + } + } + + public void Debug(string message) => Log(LogLevel.Debug, message); + public void Info(string message) => Log(LogLevel.Info, message); + public void Warn(string message) => Log(LogLevel.Warning, message); + public void Error(string message) => Log(LogLevel.Error, message); + public void Fatal(string message) => Log(LogLevel.Fatal, message); + + public void Dispose() + { + foreach (var sink in Sinks) + { + sink.Dispose(); + } + } +} diff --git a/SimpleTGBot/Logging/StdoutSink.cs b/SimpleTGBot/Logging/StdoutSink.cs new file mode 100644 index 0000000..0cfd71b --- /dev/null +++ b/SimpleTGBot/Logging/StdoutSink.cs @@ -0,0 +1,23 @@ +namespace SimpleTGBot.Logging; + +internal class StdoutSink : ILogSink +{ + private readonly ConsoleColor[] colors = [ConsoleColor.White, ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.DarkRed, ConsoleColor.Red]; + private ConsoleColor originalConsoleColor; + + public StdoutSink() + { + originalConsoleColor = Console.ForegroundColor; + } + + public void Log(DateTime time, LogLevel level, string message) + { + Console.ForegroundColor = colors[(int)level]; + foreach (string line in message.Split(Environment.NewLine)) + Console.WriteLine($"({time:u}) [{level.GetName()}] {line}"); + } + + public void Dispose() { + Console.ForegroundColor = originalConsoleColor; + } +} diff --git a/SimpleTGBot/Program.cs b/SimpleTGBot/Program.cs index dcd4aa1..9e873ea 100644 --- a/SimpleTGBot/Program.cs +++ b/SimpleTGBot/Program.cs @@ -1,4 +1,5 @@ using System.Text; +using SimpleTGBot.Logging; namespace SimpleTGBot; @@ -29,8 +30,11 @@ public static class Program return 1; } - TelegramBot telegramBot = new TelegramBot(botToken); + Logger logger = new Logger(); + logger.Sinks.Add(new StdoutSink()); + TelegramBot telegramBot = new TelegramBot(botToken, logger); await telegramBot.Run(); + logger.Dispose(); return 0; } diff --git a/SimpleTGBot/TelegramBot.cs b/SimpleTGBot/TelegramBot.cs index 588495e..ff33220 100644 --- a/SimpleTGBot/TelegramBot.cs +++ b/SimpleTGBot/TelegramBot.cs @@ -1,4 +1,5 @@ -using SimpleTGBot.MemeGen; +using SimpleTGBot.Logging; +using SimpleTGBot.MemeGen; using Telegram.Bot; using Telegram.Bot.Exceptions; using Telegram.Bot.Polling; @@ -7,15 +8,21 @@ using Telegram.Bot.Types.Enums; namespace SimpleTGBot; -public class TelegramBot +internal class TelegramBot { private string token; + private Logger logger; + private Dictionary dialogs; private TempStorage temp; + private HttpClient httpClient; - public TelegramBot(string token) + public TelegramBot(string token, Logger logger) { this.token = token; + this.logger = logger; + dialogs = new Dictionary(); temp = new TempStorage(); + httpClient = new HttpClient(); } /// @@ -40,11 +47,11 @@ public class TelegramBot try { var me = await botClient.GetMeAsync(cancellationToken: cts.Token); - Console.WriteLine($"Бот @{me.Username} запущен.\nДля остановки нажмите клавишу Esc..."); + logger.Info($"Бот @{me.Username} запущен.\r\nДля остановки нажмите клавишу Esc..."); } catch (ApiRequestException) { - Console.WriteLine("Указан неправильный токен"); + logger.Fatal("Указан неправильный токен"); goto botQuit; } @@ -63,27 +70,94 @@ public class TelegramBot /// Служебный токен для работы с многопоточностью async Task OnMessageReceived(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) { - var message = update.Message; - if (message is null) + if (update.Message is not { } message) return; + if (message.Chat.Type != ChatType.Private) return; + + DialogData dialogData; + if (!dialogs.ContainsKey(message.Chat.Id)) { - return; - } - if (message.Text is not { } messageText) + dialogData = new DialogData() { state = DialogState.Initial }; + dialogs[message.Chat.Id] = dialogData; + } else { - return; + dialogData = dialogs[message.Chat.Id]; } - var chatId = message.Chat.Id; - Console.WriteLine($"Получено сообщение в чате {chatId}: '{messageText}'"); + switch (dialogData.state) + { + case DialogState.Initial: + { + bool replied = false; + if (message.Photo is { } picture) + { + replied = true; + await DialogHandleDemotivatorPicture(botClient, dialogData, message, picture, cancellationToken); + } + else if (message.Text is { } messageText) + { + if (Interactions.IsStartCommand(messageText) || Interactions.IsHello(messageText)) + { + _ = botClient.SendTextMessageAsync(message.Chat.Id, Interactions.awaitingPictureMessage, replyMarkup: Interactions.backButtonReplyMarkup); + dialogData.state = DialogState.AwaitingPicture; + replied = true; + } + } + if (!replied) + { + _ = botClient.SendTextMessageAsync(message.Chat.Id, Interactions.sayHelloMessage, replyMarkup: Interactions.initialReplyMarkup); + } + break; + } + case DialogState.AwaitingPicture: + { + if (message.Photo is { } picture) + { + await DialogHandleDemotivatorPicture(botClient, dialogData, message, picture, cancellationToken); + } + else + { + bool reacted = false; + if (message.Text is { } messageText) + { + if (Interactions.IsCancellation(messageText)) + { + dialogData.state = DialogState.Initial; + _ = botClient.SendTextMessageAsync(message.Chat.Id, Interactions.awaitingPictureMessage, replyMarkup: Interactions.quickActionReplyMarkup); + reacted = true; + } + } + if (!reacted) + _ = botClient.SendTextMessageAsync(message.Chat.Id, Interactions.sendPictureOrQuitMessage, replyMarkup: Interactions.backButtonReplyMarkup); + } + break; + } + } + } - Message sentMessage = await botClient.SendTextMessageAsync( - chatId: chatId, - text: "Ты написал:\n" + messageText, - cancellationToken: cancellationToken); - - // грязный тест - MemoryStream demotivatorPng = DemotivatorGen.MakePictureDemotivator("pic.png", [new DemotivatorText() {Title=messageText, Subtitle=messageText}], DemotivatorGen.DefaultStyle()); - await botClient.SendPhotoAsync(message.Chat.Id, new InputFile(demotivatorPng, "dem.png")); + async Task DialogHandleDemotivatorPicture(ITelegramBotClient botClient, DialogData dialogData, Message message, PhotoSize[] picture, CancellationToken cancellationToken) + { + string largestSizeId = picture[picture.Length - 1].FileId; + Telegram.Bot.Types.File pictureFile = await botClient.GetFileAsync(largestSizeId, cancellationToken); + string pictureExtension = pictureFile.FilePath.Substring(pictureFile.FilePath.LastIndexOf('.') + 1); + try + { + using (HttpResponseMessage response = await httpClient.GetAsync(FilePathToUrl(pictureFile.FilePath), cancellationToken)) + { + response.EnsureSuccessStatusCode(); + (string tempFileName, FileStream tempFile) = temp.newTemporaryFile("pic", pictureExtension); + await response.Content.CopyToAsync(tempFile); + tempFile.Close(); + logger.Info($"Файл картинки {tempFileName} загружен от пользователя {message.From.FirstName}[{message.From.Id}]"); + dialogData.inputPictureFilename = tempFileName; + } + } + catch (Exception e) + { + logger.Error("Ошибка при скачивании картинки от пользователя: " + e.GetType().Name + ": " + e.Message); + logger.Error(e.StackTrace ?? ""); + _ = botClient.SendTextMessageAsync(message.Chat.Id, "Ошибка :("); + dialogData.state = DialogState.Initial; + } } /// @@ -103,8 +177,13 @@ public class TelegramBot _ => exception.ToString() }; - Console.WriteLine(errorMessage); + logger.Error(errorMessage); return Task.CompletedTask; } + + private string FilePathToUrl(string filePath) + { + return $"https://api.telegram.org/file/bot{token}/{filePath}"; + } }