Telegram bot: Стандартная обработка сообщений пользователя
Поговорим не о функциональности телеграм бота, а о сценариях обработки сообщения пользователя.
Давайте выберем простой пример и разберем его, калькулятор.
static void Main(string[] args)
{
Console.WriteLine("Simple Calculator");
double firstNumber = GetNumberFromUser("Please write the first number: ");
double secondNumber = GetNumberFromUser("Please write the second number: ");
char operation = GetOperationFromUser("Please write the operation (+, -, *, /): ");
double result = PerformOperation(firstNumber, secondNumber, operation);
Console.WriteLine($"Result: {firstNumber} {operation} {secondNumber} = {result}");
Console.Read();
}
static double GetNumberFromUser(string prompt)
{
double number;
Console.Write(prompt);
while (!double.TryParse(Console.ReadLine(), out number))
{
Console.WriteLine("Invalid input. Please enter a valid number.");
Console.Write(prompt);
}
return number;
}
static char GetOperationFromUser(string prompt)
{
Console.Write(prompt);
string input = Console.ReadLine();
while (string.IsNullOrEmpty(input) || "+-*/".IndexOf(input[0]) == -1)
{
Console.WriteLine("Invalid operation. Please enter a valid operation (+, -, *, /).");
Console.Write(prompt);
input = Console.ReadLine();
}
return input[0];
}
static double PerformOperation(double firstNumber, double secondNumber, char operation)
{
switch (operation)
{
case '+':
return firstNumber + secondNumber;
case '-':
return firstNumber - secondNumber;
case '*':
return firstNumber * secondNumber;
case '/':
if (secondNumber == 0)
{
Console.WriteLine("Division by zero is not allowed.");
return 0;
}
return firstNumber / secondNumber;
default:
Console.WriteLine("Invalid operation.");
return 0;
}
}
Абсолютно банально, но как нам перенести эту логику в нашего телеграм бота? Как нам двигаться дальше по сценарию, от чего нам отталкиваться? Добавим на данный момент состояния.
using System;
using System.Threading.Tasks;
using System.Threading;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
namespace TestBot
{
internal class Program
{
static async Task Main(string[] args)
{
using var cancellationToken = new CancellationTokenSource();
// Инициализируем нашего бота, передав в конструктор токен.
var myBot = new TelegramBotClient("Your_Token");
UserMessageHandler handleService = new UserMessageHandler();
myBot.StartReceiving(
handleService.HandleUpdateAsync,
handleService.HandleErrorAsync,
new ReceiverOptions(),
cancellationToken.Token);
var myBotUser = await myBot.GetMeAsync();
Console.WriteLine($"Start listening for @{myBotUser.Username}");
Console.ReadLine();
cancellationToken.Cancel();
}
}
public class UserMessageHandler
{
public CalculationStates currentState { get; set; }
public double firstNumber { get; set; }
public double secondNumber { get; set; }
public UserMessageHandler()
{
this.currentState = CalculationStates.Greetings;
}
public async Task HandleUpdateAsync(
ITelegramBotClient botClient,
Update update,
CancellationToken cancellationToken)
{
if (update.Message == null)
{
Console.WriteLine($"Message is null...");
return;
}
var username = update.Message.Chat.Username;
var messageText = update.Message.Text;
if (username == null)
{
Console.WriteLine($"Username is null...");
return;
}
if (messageText == null)
{
Console.WriteLine($"MessageText is null...");
return;
}
if (update.Message.From == null)
{
Console.WriteLine($"User is null...");
return;
}
if (currentState.Equals(CalculationStates.Greetings))
{
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the first number");
currentState = CalculationStates.SetFirstNumber;
return;
}
// Ввод первого числа.
if (currentState.Equals(CalculationStates.SetFirstNumber) && Double.TryParse(messageText,out double parsedFirstNumber))
{
firstNumber = parsedFirstNumber;
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the second number");
currentState = CalculationStates.SetSecondNumber;
return;
}
if (currentState.Equals(CalculationStates.SetFirstNumber))
{
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the correct number");
return;
}
// Ввод второго числа.
if (currentState.Equals(CalculationStates.SetSecondNumber) && Double.TryParse(messageText, out double parsedSecondNumber))
{
secondNumber = parsedSecondNumber;
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the operation");
currentState = CalculationStates.SetOperation;
return;
}
if (currentState.Equals(CalculationStates.SetSecondNumber))
{
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the correct number");
return;
}
// Ввод второго числа.
if (currentState.Equals(CalculationStates.SetOperation))
{
switch (messageText[0])
{
case '+':
await botClient.SendTextMessageAsync(update.Message.From.Id, $"The answer is {firstNumber + secondNumber}");
return;
case '-':
await botClient.SendTextMessageAsync(update.Message.From.Id, $"The answer is {firstNumber - secondNumber}");
return;
case '*':
await botClient.SendTextMessageAsync(update.Message.From.Id, $"The answer is {firstNumber * secondNumber}");
return;
case '/':
if (secondNumber == 0)
{
await botClient.SendTextMessageAsync(update.Message.From.Id, "Division by zero is not allowed.");
return;
}
await botClient.SendTextMessageAsync(update.Message.From.Id, $"The answer is {firstNumber / secondNumber}");
return;
default:
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Enter the correct operation like + - * /");
return;
}
}
currentState = CalculationStates.Greetings;
}
public async Task HandleErrorAsync(
ITelegramBotClient botClient,
Exception exception,
CancellationToken cancellationToken)
{
Console.WriteLine($"Exception: '{exception}'");
}
}
public enum CalculationStates
{
Greetings,
SetFirstNumber,
SetSecondNumber,
SetOperation
}
}
Результат
Конечно лучше, однако наше приложение может расширяться в функционале, допустим телеграм бот может узнавать погоду, высчитывать индекс массы тела… Добавим Кейсы и состояние пользователя на определенное меню. Предыдущие состояния теперь будут принадлежать непосредственно к конкретному кейсу. Пропустим моменты наподобие внедрения зависимости, эта тема достойна отдельного поста.
using System;
using System.Threading.Tasks;
using System.Threading;
using Telegram.Bot;
using Telegram.Bot.Polling;
using Telegram.Bot.Types;
using Telegram.Bots.Http;
using Telegram.Bots.Types;
namespace TestBot
{
internal class Program
{
static async Task Main(string[] args)
{
using var cancellationToken = new CancellationTokenSource();
var myBot = new TelegramBotClient("Your token");
User user = new User();
CalculatorUseCase CalculatorUseCase = new CalculatorUseCase(user);
WeatherUseCase WeatherUseCase = new WeatherUseCase(user);
MainMenuUseCase mainMenuUseCase = new MainMenuUseCase(user);
UserMessageHandler handleService = new UserMessageHandler(CalculatorUseCase,WeatherUseCase,mainMenuUseCase, user);
myBot.StartReceiving(
handleService.HandleUpdateAsync,
handleService.HandleErrorAsync,
new ReceiverOptions(),
cancellationToken.Token);
var myBotUser = await myBot.GetMeAsync();
Console.WriteLine($"Start listening for @{myBotUser.Username}");
Console.ReadLine();
cancellationToken.Cancel();
}
}
public class User
{
public User()
{
this.userState = UserState.MainMenu;
}
public UserState userState{ get; set; }
}
public enum UserState
{
MainMenu,
CalculatorPipeLine,
WeatherPipeLine
}
public class UserMessageHandler
{
CalculatorUseCase calculatorUseCase;
WeatherUseCase weatherUseCase;
MainMenuUseCase mainMenuUseCase;
User user;
public UserMessageHandler(CalculatorUseCase calculatorUseCase, WeatherUseCase weatherUseCase,MainMenuUseCase mainMenuUseCase, User user)
{
this.calculatorUseCase = calculatorUseCase;
this.weatherUseCase = weatherUseCase;
this.user = user;
this.mainMenuUseCase = mainMenuUseCase;
}
public async Task HandleUpdateAsync(
ITelegramBotClient botClient,
Telegram.Bot.Types.Update update,
CancellationToken cancellationToken)
{
if (update.Message == null)
{
Console.WriteLine($"Message is null...");
return;
}
var username = update.Message.Chat.Username;
var messageText = update.Message.Text;
if (username == null)
{
Console.WriteLine($"Username is null...");
return;
}
if (messageText == null)
{
Console.WriteLine($"MessageText is null...");
return;
}
if (update.Message.From == null)
{
Console.WriteLine($"User is null...");
return;
}
switch (user.userState)
{
case UserState.MainMenu:
await mainMenuUseCase.processMessage(update, botClient);
return;
case UserState.CalculatorPipeLine:
await calculatorUseCase.processMessage(update, botClient);
return;
case UserState.WeatherPipeLine:
await weatherUseCase.processMessage(update, botClient);
return;
default:
break;
}
}
public async Task HandleErrorAsync(
ITelegramBotClient botClient,
Exception exception,
CancellationToken cancellationToken)
{
Console.WriteLine($"Exception: '{exception}'");
}
}
public enum CalculationStates
{
SetFirstNumber,
SetSecondNumber,
SetOperation
}
public class CalculatorUseCase
{
public CalculationStates currentState { get; set; }
public double firstNumber { get; set; }
public double secondNumber { get; set; }
public User user { get; set; }
public CalculatorUseCase(User user)
{
this.currentState = CalculationStates.SetFirstNumber;
this.user = user;
}
public async Task processMessage(Telegram.Bot.Types.Update update, ITelegramBotClient botClient)
{
var messageText = update.Message.Text;
if (currentState.Equals(CalculationStates.SetFirstNumber) && Double.TryParse(messageText, out double parsedFirstNumber))
{
firstNumber = parsedFirstNumber;
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the second number");
currentState = CalculationStates.SetSecondNumber;
return;
}
if (currentState.Equals(CalculationStates.SetFirstNumber))
{
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the correct number");
return;
}
if (currentState.Equals(CalculationStates.SetSecondNumber) && Double.TryParse(messageText, out double parsedSecondNumber))
{
secondNumber = parsedSecondNumber;
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the operation");
currentState = CalculationStates.SetOperation;
return;
}
if (currentState.Equals(CalculationStates.SetSecondNumber))
{
await botClient.SendTextMessageAsync(update.Message.From.Id, "Enter the correct number");
return;
}
if (currentState.Equals(CalculationStates.SetOperation))
{
switch (messageText[0])
{
case '+':
await botClient.SendTextMessageAsync(update.Message.From.Id, $"The answer is {firstNumber + secondNumber}");
await botClient.SendTextMessageAsync(update.Message.From.Id, $"1 - go to calculate menu or 2 - weather menu");
user.userState = UserState.MainMenu;
currentState = CalculationStates.SetFirstNumber;
return;
case '-':
await botClient.SendTextMessageAsync(update.Message.From.Id, $"The answer is {firstNumber - secondNumber}");
await botClient.SendTextMessageAsync(update.Message.From.Id, $"1 - go to calculate menu or 2 - weather menu");
user.userState = UserState.MainMenu;
currentState = CalculationStates.SetFirstNumber;
return;
case '*':
await botClient.SendTextMessageAsync(update.Message.From.Id, $"The answer is {firstNumber * secondNumber}");
await botClient.SendTextMessageAsync(update.Message.From.Id, $"1 - go to calculate menu or 2 - weather menu");
user.userState = UserState.MainMenu;
currentState = CalculationStates.SetFirstNumber;
return;
case '/':
if (secondNumber == 0)
{
await botClient.SendTextMessageAsync(update.Message.From.Id, "Division by zero is not allowed.");
await botClient.SendTextMessageAsync(update.Message.From.Id, $"1 - go to calculate menu or 2 - weather menu");
user.userState = UserState.MainMenu;
currentState = CalculationStates.SetFirstNumber;
return;
}
await botClient.SendTextMessageAsync(update.Message.From.Id, $"The answer is {firstNumber / secondNumber}");
await botClient.SendTextMessageAsync(update.Message.From.Id, $"1 - go to calculate menu or 2 - weather menu");
user.userState = UserState.MainMenu;
currentState = CalculationStates.SetFirstNumber;
return;
default:
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Enter the correct operation like + - * /");
user.userState = UserState.MainMenu;
currentState = CalculationStates.SetFirstNumber;
return;
}
}
currentState = CalculationStates.SetFirstNumber;
}
}
public enum WeatherStates
{
SelectCelsiusOrFahrenheit,
SelectCity
}
public class WeatherUseCase
{
public User user { get; set; }
public WeatherStates currentState { get; set; }
public bool isCelsium{ get; set; }
public WeatherUseCase(User user)
{
this.currentState = WeatherStates.SelectCelsiusOrFahrenheit;
this.user = user;
}
public async Task processMessage(Telegram.Bot.Types.Update update, ITelegramBotClient botClient)
{
var messageText = update.Message.Text;
if (currentState.Equals(WeatherStates.SelectCelsiusOrFahrenheit) && Int32.TryParse(messageText, out int parsedAnswer))
{
switch (parsedAnswer)
{
case 1:
isCelsium = true;
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Select the city");
currentState = WeatherStates.SelectCity;
return;
case 2:
isCelsium = false;
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Select the city");
currentState = WeatherStates.SelectCity;
return;
default:
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Enter the correct number 1 (Celsius) or 2 (Fahrenheit)");
return;
}
}
if (currentState.Equals(WeatherStates.SelectCity))
{
switch (messageText.ToLower())
{
case "moscow":
isCelsium = true;
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Cloudy, generally as always");
await botClient.SendTextMessageAsync(update.Message.From.Id, $"1 - go to calculate menu or 2 - weather menu");
currentState = WeatherStates.SelectCelsiusOrFahrenheit;
user.userState = UserState.MainMenu;
return;
case "philadelphia":
isCelsium = false;
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Where is always sunny in philadelphia");
await botClient.SendTextMessageAsync(update.Message.From.Id, $"1 - go to calculate menu or 2 - weather menu");
currentState = WeatherStates.SelectCelsiusOrFahrenheit;
user.userState = UserState.MainMenu;
return;
default:
await botClient.SendTextMessageAsync(update.Message.From.Id, $"City is not founded.");
await botClient.SendTextMessageAsync(update.Message.From.Id, $"1 - go to calculate menu or 2 - weather menu");
currentState = WeatherStates.SelectCelsiusOrFahrenheit;
user.userState = UserState.MainMenu;
return;
}
}
}
}
public enum MainMenuState
{
Greetings,
ChooseMenu
}
public class MainMenuUseCase
{
public User user { get; set; }
public MainMenuState currentState { get; set; }
public MainMenuUseCase(User user)
{
this.currentState = MainMenuState.Greetings;
this.user = user;
}
public async Task processMessage(Telegram.Bot.Types.Update update, ITelegramBotClient botClient)
{
var messageText = update.Message.Text;
if (currentState.Equals(MainMenuState.Greetings))
{
await botClient.SendTextMessageAsync(update.Message.From.Id, "1 - go to calculate menu or 2 - weather menu");
currentState = MainMenuState.ChooseMenu;
return;
}
if (currentState.Equals(MainMenuState.ChooseMenu) && Int32.TryParse(messageText, out int parsedAnswer))
{
switch (parsedAnswer)
{
case 1:
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Enter the first number");
user.userState = UserState.CalculatorPipeLine;
return;
case 2:
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Select 1-Celsius or 2-Fahrenheit");
user.userState = UserState.WeatherPipeLine;
return;
default:
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Enter the correct number 1 (calculate menu) or 2 (weather menu)");
return;
}
}
if (currentState.Equals(MainMenuState.ChooseMenu))
{
await botClient.SendTextMessageAsync(update.Message.From.Id, $"Enter the correct number 1 (calculate menu) or 2 (weather menu)");
return;
}
}
}
}
Итог
В будущем мы более подробно рассмотрим этот кейс, выделим абстракции для удобного добавления новых сценариев. Конечно есть и другие возможные реализации, например через FSM, и F#, но эти темы мы обсудим в будущем.
Итог
Мы познакомились с не очень элегантным способом обработки сообщений от пользователя, однако он может покрыть большую половину требований к функционалу бота поскольку прост в исполнении.
Надеюсь пост был для вас полезен, до новых встреч!