Приветствую всех в своем уроке по написанию Telegram бота на языке программирования C#. В этом уроке я постараюсь максимально просто и понятно объяснить как написать своего бота с нуля. Конечно, здесь не будет какого-то трудного проекта, я дам вам необходимую базу, с помощью которой вы сможете писать своих ботов.

Начало

Для начала нам требуется зарегистрировать нашего бота. Делается это через "Ботобатю" (кликабельно) в самом Telegram.

  1. Переходим по ссылке и попадаем в диалог с BotFather и жмем "Запустить". BotFather - это официальный бот Telegram`а, через которого проходит регистрация и настройка ваших ботов.

  2. От вашего лица должна отправиться команда /start , на которую бот ответит большим списком команд с описаниями. Подробно изучить каждую Вы можете сами, а мы двигаемся к шагу 3.

  3. Используем команду /newbot. На этом шаге нужно отправить боту имя Вашего будущего бота. Например, пусть это будет Александр Николаевич.

  4. Далее нас попросят ввести username бота, учтите, что он должен быть написан строго на латинице, а также содержать bot или _bot в конце. Username - это уникальный идентификатор аккаунта (вашего или бота), по которому люди могут найти аккаунт в поиске. В случае, если username уже занят, вас попросят использовать другой.

  5. После всех выполнений действий, Ботобатя присылает нам небольшое сообщение, в котором говорится об успешном создании бота. Из этого сообщения нам нужен token (уникальный ключ), с помощью которого мы будем авторизировать нашего бота в дальнейшем, он имеет примерно такой вид: 13523457:AFAgd_SDFG6sd7f6asdf67asdf78. Учтите, что абсолютно никому нельзя присылать этот token! Это ваш секретный ключ для доступа к боту.

Пример того, как все должно выглядеть.
Пример того, как все должно выглядеть.

Создание проекта в IDE

После того, как мы создали бота, перейдем к созданию проекта в вашей IDE. У меня это Rider от компании JetBrains. Вы можете использовать эту же IDE, либо Visual Studio от компании Microsoft, либо все от той же компании Visual Studio Code.

По сути, вы можете создать любой тип проекта, будь то консольное приложение или же какой-нибудь WinForms. Я всегда создаю консольное приложение, так как в будущем делаю деплой на Linux, да и как-то не вижу смысла в создании бота с программным интерфейсом.

Если же вы до сих пор не знаете, что такое IDE, как создавать проекты, то вам явно рано писать ботов, займитесь для начала изучением языка!

После того, как мы создали проект, нам нужно установить библиотеку Telegram.Bot (GitHub библиотеки, Nuget пакет). Сделать это можно либо через терминал в IDE, написав команду

dotnet add Telegram.Bot

либо же использовать графический интерфейс. На момент написания статьи была установлена самая последняя и самая актуальная версия пакета (19.0.0).

У библиотеки есть своя документация, можете посмотреть ее здесь.

Написание бота

Теперь приступим к написанию бота, для начала напишем стандартный класс Program. Добавим туда объект интерфейса ITelegramBotClient и в методе Main создадим стандартные переменные и присвоим им соответствующие значения.

class Program
{
    // Это клиент для работы с Telegram Bot API, который позволяет отправлять сообщения, управлять ботом, подписываться на обновления и многое другое.
    private static ITelegramBotClient _botClient;
    
    // Это объект с настройками работы бота. Здесь мы будем указывать, какие типы Update мы будем получать, Timeout бота и так далее.
    private static ReceiverOptions _receiverOptions;
    
    static async Task Main()
    {
        
        _botClient = new TelegramBotClient("<token>"); // Присваиваем нашей переменной значение, в параметре передаем Token, полученный от BotFather
        _receiverOptions = new ReceiverOptions // Также присваем значение настройкам бота
        {
            AllowedUpdates = new[] // Тут указываем типы получаемых Update`ов, о них подробнее расказано тут https://core.telegram.org/bots/api#update
            {
                UpdateType.Message, // Сообщения (текст, фото/видео, голосовые/видео сообщения и т.д.)
            },
            // Параметр, отвечающий за обработку сообщений, пришедших за то время, когда ваш бот был оффлайн
            // True - не обрабатывать, False (стоит по умолчанию) - обрабаывать
            ThrowPendingUpdates = true, 
        };
        
        using var cts = new CancellationTokenSource();
        
        // UpdateHander - обработчик приходящих Update`ов
        // ErrorHandler - обработчик ошибок, связанных с Bot API
        _botClient.StartReceiving(UpdateHandler, ErrorHandler, _receiverOptions, cts.Token); // Запускаем бота
        
        var me = await _botClient.GetMeAsync(); // Создаем переменную, в которую помещаем информацию о нашем боте.
        Console.WriteLine($"{me.FirstName} запущен!");
        
        await Task.Delay(-1); // Устанавливаем бесконечную задержку, чтобы наш бот работал постоянно
    }
  }

Теперь давайте в этом же классе (можно и в другом) напишем методы UpdateHandler и ErrorHandler.

private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
    {
        // Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
        try
        {
            // Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
            switch (update.Type)
            {
                case UpdateType.Message:
                {
                    Console.WriteLine("Пришло сообщение!");
                    return;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

private static Task ErrorHandler(ITelegramBotClient botClient, Exception error, CancellationToken cancellationToken)
    {
        // Тут создадим переменную, в которую поместим код ошибки и её сообщение 
        var ErrorMessage = error switch
        {
            ApiRequestException apiRequestException
                => $"Telegram API Error:\n[{apiRequestException.ErrorCode}]\n{apiRequestException.Message}",
            _ => error.ToString()
        };

        Console.WriteLine(ErrorMessage);
        return Task.CompletedTask;
    }

Теперь давайте посмотрим, работает ли наш код ? Жмем на кнопку Debug и проверяем :)

It works!
It works!

Как мы видим, бот успешно запустился, теперь давайте напишем пару сообщений, чтобы понять, получает ли он сообщения

И тут тоже все прекрасно, теперь давайте дополним наш метод UpdateHandler и напишем эхо бота. Также чуть-чуть попозже расскажу немного о типах Update.

private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
    {
        // Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
        try
        {
            // Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
            switch (update.Type)
            {
                case UpdateType.Message:
                {
                    // эта переменная будет содержать в себе все связанное с сообщениями
                    var message = update.Message;
                    
                    // From - это от кого пришло сообщение (или любой другой Update)
                    var user = message.From;
                    
                    // Выводим на экран то, что пишут нашему боту, а также небольшую информацию об отправителе
                    Console.WriteLine($"{user.FirstName} ({user.Id}) написал сообщение: {message.Text}");

                    // Chat - содержит всю информацию о чате
                    var chat = message.Chat;
                    await botClient.SendTextMessageAsync(
                        chat.Id,
                        message.Text, // отправляем то, что написал пользователь
                        replyToMessageId: message.MessageId // по желанию можем поставить этот параметр, отвечающий за "ответ" на сообщение
                        );
                    
                    return;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

Теперь проверяем.

Как мы видим, все работает идеально. Теперь расскажу о типах Update, так как дальше мы напишем кое-что посложнее. Итак, типы Update:

  • Message - принимает в себя все сообщения. Обычные текстовые, фото или видео, аудио или видео сообщения (кружочки), стикеры, контакты, геопозицию, голосование и так далее. Всё, что мы отправляем в чат - это все Message.

  • EditedMessage - тут все просто: этот тип принимает в себя любое обновление сообщения. Схож с Message.

  • ChannelPost - как и Message, но направлен на каналы.

  • EditedChannelPost - аналогичен EditedMessage, но также направлен на каналы.

  • CallbackQuery - отвечает за Inline кнопки, они висят под сообщением, возможно, вы их уже видели в других ботах.

  • Poll - получает все связанное с голосованием.

  • PollAnswner - а этот тип работает только тогда, когда пользователь изменил свой ответ в голосовании.

  • ChatMember - всё, что касается людей в чате/канале: зашел, вышел, повысили, понизили, замьютили и т.д.

  • MyChatMember - всё, что касается бота в диалоге между пользователем и ботом, т.е. изменения в личных сообщениях.

  • ChatJoinRequest - получение информации о поданной заявки на вступление в чат/канал.

  • InlineQuery - получение входящих inline запросов. Inline запрос - это, когда вы в чате используете @ и username бота и вводите какой-то запрос, а результат выполнения отправляется в чат от вашего лица с надписью "сделано с помощью....".

  • ChosenInlineResult - а это уже то, что как раз таки выбрал пользователь. Т.е. InlineQuery это просто как разрешение использовать эту функцию, а ChosenInlineResult получает выбор пользователя и обрабатывает его. Знаю, что вы думаете "Они что, совсем идиоты ? Не могли сделать нормально ?", но привыкайте, такого будет полно)

  • PreCheckoutQuery - сюда приходит информация о платеже, который начал оплачивать пользователь.

  • ShippingQuery - а это срабатывает тогда, когда успешно сработал PreCheckoutQuery , т.е. этот update уже подтверждает успешную оплату пользователем.

Фух, ну вроде все, если желаете посмотреть оригинал, то он находится здесь.

Теперь давайте напишем что-нибудь посложнее. Добавим в AllowedUpdates тип CallbackQuery:

AllowedUpdates = new[] // Тут указываем типы получаемых Update`ов, о них подробнее расказано тут https://core.telegram.org/bots/api#update
            {
                UpdateType.Message, // Сообщения (текст, фото/видео, голосовые/видео сообщения и т.д.)
                UpdateType.CallbackQuery // Inline кнопки
            },

Теперь в нашем UpdateHandler добавим обработку команды /start и сделаем там несколько клавиатур, чтобы вы поняли, как работать с разными типами update, а также увидели еще одну клавиатуру, которая называется Reply клавиатура.

    private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
    {
        // Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
        try
        {
            // Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
            switch (update.Type)
            {
                case UpdateType.Message:
                {
                    // эта переменная будет содержать в себе все связанное с сообщениями
                    var message = update.Message;

                    // From - это от кого пришло сообщение
                    var user = message.From;
                    
                    // Выводим на экран то, что пишут нашему боту, а также небольшую информацию об отправителе
                    Console.WriteLine($"{user.FirstName} ({user.Id}) написал сообщение: {message.Text}");

                    // Chat - содержит всю информацию о чате
                    var chat = message.Chat;

                    // Добавляем проверку на тип Message
                    switch (message.Type)
                    {
                        // Тут понятно, текстовый тип
                        case MessageType.Text:
                        {
                            // тут обрабатываем команду /start, остальные аналогично
                            if (message.Text == "/start")
                            {
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "Выбери клавиатуру:\n" +
                                    "/inline\n" +
                                    "/reply\n");
                                return;
                            }

                            if (message.Text == "/inline")
                            {
                                // Тут создаем нашу клавиатуру
                                var inlineKeyboard = new InlineKeyboardMarkup(
                                    new List<InlineKeyboardButton[]>() // здесь создаем лист (массив), который содрежит в себе массив из класса кнопок
                                    {
                                        // Каждый новый массив - это дополнительные строки,
                                        // а каждая дополнительная строка (кнопка) в массиве - это добавление ряда

                                        new InlineKeyboardButton[] // тут создаем массив кнопок
                                        {
                                            InlineKeyboardButton.WithUrl("Это кнопка с сайтом", "https://habr.com/"),
                                            InlineKeyboardButton.WithCallbackData("А это просто кнопка", "button1"), 
                                        },
                                        new InlineKeyboardButton[]
                                        {
                                            InlineKeyboardButton.WithCallbackData("Тут еще одна", "button2"), 
                                            InlineKeyboardButton.WithCallbackData("И здесь", "button3"), 
                                        },
                                    });
                                
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "Это inline клавиатура!",
                                    replyMarkup: inlineKeyboard); // Все клавиатуры передаются в параметр replyMarkup
                                
                                return;
                            }

                            if (message.Text == "/reply")
                            {
                                // Тут все аналогично Inline клавиатуре, только меняются классы
                                // НО! Тут потребуется дополнительно указать один параметр, чтобы
                                // клавиатура выглядела нормально, а не как абы что
                                
                                var replyKeyboard = new ReplyKeyboardMarkup(
                                    new List<KeyboardButton[]>()
                                    {
                                        new KeyboardButton[]
                                        {
                                            new KeyboardButton("Привет!"),
                                            new KeyboardButton("Пока!"),
                                        },
                                        new KeyboardButton[]
                                        {
                                            new KeyboardButton("Позвони мне!")
                                        },
                                        new KeyboardButton[]
                                        {
                                            new KeyboardButton("Напиши моему соседу!")
                                        }
                                    })
                                {
                                    // автоматическое изменение размера клавиатуры, если не стоит true,
                                    // тогда клавиатура растягивается чуть ли не до луны,
                                    // проверить можете сами
                                    ResizeKeyboard = true,
                                };
                                
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "Это reply клавиатура!",
                                    replyMarkup: replyKeyboard); // опять передаем клавиатуру в параметр replyMarkup

                                return;
                            }
                            
                            return;
                        }

                        // Добавил default , чтобы показать вам разницу типов Message
                        default:
                        {
                            await botClient.SendTextMessageAsync(
                                chat.Id,
                                "Используй только текст!");
                            return;
                        }
                    }
                    
                    return;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

Как мы видим, бот теперь реагирует на /start:

Вот так выглядит inline клавиатура:

А вот так reply клавиатура:

Причем, если вы понажимаете на кнопки, то как вы уже поняли, ничего не произойдет:)


Конечно, это логично, ведь мы не добавили обработчики этих кнопок. Как вы могли заметить, reply клавиатура - это просто как заготовленный текст для пользователя, поэтому с обработкой этих кнопок у вас не должно возникнуть проблем. Так как это просто дополнительные if в блоке case MessageType.Text. Но я все же покажу, как это сделать, после перейдем к кейсу с Inline клавиатурой.

private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
    {
        // Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
        try
        {
            // Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
            switch (update.Type)
            {
                case UpdateType.Message:
                {
                    // тут все переменные

                    // Добавляем проверку на тип Message
                    switch (message.Type)
                    {
                        // Тут понятно, текстовый тип
                        case MessageType.Text:
                        {
                            // а тут обработчики команд

                            if (message.Text == "Позвони мне!")
                            {
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "Хорошо, присылай номер!",
                                    replyToMessageId: message.MessageId);

                                return;
                            }

                            if (message.Text == "Напиши моему соседу!")
                            {
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "А самому что, трудно что-ли ?",
                                    replyToMessageId: message.MessageId);

                                return;
                            }
                            
                            return;
                        }
                        // тут остальной код  
                    }        
                    return;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

Ну, вот так как-то вышло:

Теперь перейдем к блоку с Inline клавиатурами. Для обработки этой клавиатуры нам потребуется добавить в

switch(update.Type)
{
  case UpdateType.Message:
  {
    // тут весь код из примеров выше
  }
}

следующий код:

case UpdateType.CallbackQuery:
                {
                    // Переменная, которая будет содержать в себе всю информацию о кнопке, которую нажали
                    var callbackQuery = update.CallbackQuery;
                    
                    // Аналогично и с Message мы можем получить информацию о чате, о пользователе и т.д.
                    var user = callbackQuery.From;

                    // Выводим на экран нажатие кнопки
                    Console.WriteLine($"{user.FirstName} ({user.Id}) нажал на кнопку: {callbackQuery.Data}");
                  
                    // Вот тут нужно уже быть немножко внимательным и не путаться!
                    // Мы пишем не callbackQuery.Chat , а callbackQuery.Message.Chat , так как
                    // кнопка привязана к сообщению, то мы берем информацию от сообщения.
                    var chat = callbackQuery.Message.Chat; 
                    
                    // Добавляем блок switch для проверки кнопок
                    switch (callbackQuery.Data)
                    {
                        // Data - это придуманный нами id кнопки, мы его указывали в параметре
                        // callbackData при создании кнопок. У меня это button1, button2 и button3

                        case "button1":
                        {
                            // В этом типе клавиатуры обязательно нужно использовать следующий метод
                            await botClient.AnswerCallbackQueryAsync(callbackQuery.Id);
                            // Для того, чтобы отправить телеграмму запрос, что мы нажали на кнопку
                            
                            await botClient.SendTextMessageAsync(
                                chat.Id,
                                $"Вы нажали на {callbackQuery.Data}");
                            return;
                        }
                        
                        case "button2":
                        {
                            // А здесь мы добавляем наш сообственный текст, который заменит слово "загрузка", когда мы нажмем на кнопку
                            await botClient.AnswerCallbackQueryAsync(callbackQuery.Id, "Тут может быть ваш текст!");
                            
                            await botClient.SendTextMessageAsync(
                                chat.Id,
                                $"Вы нажали на {callbackQuery.Data}");
                            return;
                        }
                        
                        case "button3":
                        {
                            // А тут мы добавили еще showAlert, чтобы отобразить пользователю полноценное окно
                            await botClient.AnswerCallbackQueryAsync(callbackQuery.Id, "А это полноэкранный текст!", showAlert: true);
                            
                            await botClient.SendTextMessageAsync(
                                chat.Id,
                                $"Вы нажали на {callbackQuery.Data}");
                            return;
                        }
                    }
                    
                    return;
                }

В конечном счете UpdateHandler должен выглядеть вот так:

private static async Task UpdateHandler(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
    {
        // Обязательно ставим блок try-catch, чтобы наш бот не "падал" в случае каких-либо ошибок
        try
        {
            // Сразу же ставим конструкцию switch, чтобы обрабатывать приходящие Update
            switch (update.Type)
            {
                case UpdateType.Message:
                {
                    // Эта переменная будет содержать в себе все связанное с сообщениями
                    var message = update.Message;

                    // From - это от кого пришло сообщение (или любой другой Update)
                    var user = message.From;
                    
                    // Выводим на экран то, что пишут нашему боту, а также небольшую информацию об отправителе
                    Console.WriteLine($"{user.FirstName} ({user.Id}) написал сообщение: {message.Text}");

                    // Chat - содержит всю информацию о чате
                    var chat = message.Chat;

                    // Добавляем проверку на тип Message
                    switch (message.Type)
                    {
                        // Тут понятно, текстовый тип
                        case MessageType.Text:
                        {
                            // тут обрабатываем команду /start, остальные аналогично
                            if (message.Text == "/start")
                            {
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "Выбери клавиатуру:\n" +
                                    "/inline\n" +
                                    "/reply\n");
                                return;
                            }

                            if (message.Text == "/inline")
                            {
                                // Тут создаем нашу клавиатуру
                                var inlineKeyboard = new InlineKeyboardMarkup(
                                    new List<InlineKeyboardButton[]>() // здесь создаем лист (массив), который содрежит в себе массив из класса кнопок
                                    {
                                        // Каждый новый массив - это дополнительные строки,
                                        // а каждая дополнительная кнопка в массиве - это добавление ряда

                                        new InlineKeyboardButton[] // тут создаем массив кнопок
                                        {
                                            InlineKeyboardButton.WithUrl("Это кнопка с сайтом", "https://habr.com/"),
                                            InlineKeyboardButton.WithCallbackData("А это просто кнопка", "button1"), 
                                        },
                                        new InlineKeyboardButton[]
                                        {
                                            InlineKeyboardButton.WithCallbackData("Тут еще одна", "button2"), 
                                            InlineKeyboardButton.WithCallbackData("И здесь", "button3"), 
                                        },
                                    });
                                
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "Это inline клавиатура!",
                                    replyMarkup: inlineKeyboard); // Все клавиатуры передаются в параметр replyMarkup
                                
                                return;
                            }

                            if (message.Text == "/reply")
                            {
                                // Тут все аналогично Inline клавиатуре, только меняются классы
                                // НО! Тут потребуется дополнительно указать один параметр, чтобы
                                // клавиатура выглядела нормально, а не как абы что
                                
                                var replyKeyboard = new ReplyKeyboardMarkup(
                                    new List<KeyboardButton[]>()
                                    {
                                        new KeyboardButton[]
                                        {
                                            new KeyboardButton("Привет!"),
                                            new KeyboardButton("Пока!"),
                                        },
                                        new KeyboardButton[]
                                        {
                                            new KeyboardButton("Позвони мне!")
                                        },
                                        new KeyboardButton[]
                                        {
                                            new KeyboardButton("Напиши моему соседу!")
                                        }
                                    })
                                {
                                    // автоматическое изменение размера клавиатуры, если не стоит true,
                                    // тогда клавиатура растягивается чуть ли не до луны,
                                    // проверить можете сами
                                    ResizeKeyboard = true,
                                };
                                
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "Это reply клавиатура!",
                                    replyMarkup: replyKeyboard); // опять передаем клавиатуру в параметр replyMarkup

                                return;
                            }

                            if (message.Text == "Позвони мне!")
                            {
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "Хорошо, присылай номер!",
                                    replyToMessageId: message.MessageId);

                                return;
                            }

                            if (message.Text == "Напиши моему соседу!")
                            {
                                await botClient.SendTextMessageAsync(
                                    chat.Id,
                                    "А самому что, трудно что-ли ?",
                                    replyToMessageId: message.MessageId);

                                return;
                            }
                            
                            return;
                        }

                        // Добавил default , чтобы показать вам разницу типов Message
                        default:
                        {
                            await botClient.SendTextMessageAsync(
                                chat.Id,
                                "Используй только текст!");
                            return;
                        }
                    }
                    
                    return;
                }

                case UpdateType.CallbackQuery:
                {
                    // Переменная, которая будет содержать в себе всю информацию о кнопке, которую нажали
                    var callbackQuery = update.CallbackQuery;
                    
                    // Аналогично и с Message мы можем получить информацию о чате, о пользователе и т.д.
                    var user = callbackQuery.From;

                    // Выводим на экран нажатие кнопки
                    Console.WriteLine($"{user.FirstName} ({user.Id}) нажал на кнопку: {callbackQuery.Data}");
                  
                    // Вот тут нужно уже быть немножко внимательным и не путаться!
                    // Мы пишем не callbackQuery.Chat , а callbackQuery.Message.Chat , так как
                    // кнопка привязана к сообщению, то мы берем информацию от сообщения.
                    var chat = callbackQuery.Message.Chat; 
                    
                    // Добавляем блок switch для проверки кнопок
                    switch (callbackQuery.Data)
                    {
                        // Data - это придуманный нами id кнопки, мы его указывали в параметре
                        // callbackData при создании кнопок. У меня это button1, button2 и button3

                        case "button1":
                        {
                            // В этом типе клавиатуры обязательно нужно использовать следующий метод
                            await botClient.AnswerCallbackQueryAsync(callbackQuery.Id);
                            // Для того, чтобы отправить телеграмму запрос, что мы нажали на кнопку
                            
                            await botClient.SendTextMessageAsync(
                                chat.Id,
                                $"Вы нажали на {callbackQuery.Data}");
                            return;
                        }
                        
                        case "button2":
                        {
                            // А здесь мы добавляем наш сообственный текст, который заменит слово "загрузка", когда мы нажмем на кнопку
                            await botClient.AnswerCallbackQueryAsync(callbackQuery.Id, "Тут может быть ваш текст!");
                            
                            await botClient.SendTextMessageAsync(
                                chat.Id,
                                $"Вы нажали на {callbackQuery.Data}");
                            return;
                        }
                        
                        case "button3":
                        {
                            // А тут мы добавили еще showAlert, чтобы отобразить пользователю полноценное окно
                            await botClient.AnswerCallbackQueryAsync(callbackQuery.Id, "А это полноэкранный текст!", showAlert: true);
                            
                            await botClient.SendTextMessageAsync(
                                chat.Id,
                                $"Вы нажали на {callbackQuery.Data}");
                            return;
                        }
                    }
                    
                    return;
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

Теперь запускаем проект и проверяем кнопки!

Заключение

К сожалению, на этом пока всё. Скорее всего будет вторая часть этой статьи или полноценный видеоурок на Youtube, но пока вот так.

Прошу оценить мою статью и оставить комментарий, так как это первая моя подобная работа, до этого мне не доводилось писать статьи или что-нибудь подобное.

Комментарии (42)


  1. lair
    25.08.2023 02:49

    После первого же примера кода расхотелось читать дальше:

    private static ITelegramBotClient _botClient;
    private static ReceiverOptions _receiverOptions;
    

    А зачем это поля, а не переменные? Почему они статические?

    using var cts = new CancellationTokenSource();
    

    Зачем создавать CancellationTokenSource, если вы его никогда не активируете?

    await Task.Delay(-1);
    

    ...а выходить из программы не надо?


    1. Zara6502
      25.08.2023 02:49
      +3

      После первого же примера кода расхотелось читать дальше

      Мне кажется проблема Хабра в том, что те кто знают и умеют хорошо - ничего не пишут. А те кто знают и умеют, но допускают ошибки - пишут и получают шишки.

      На Хабре про Телеграм бота на C# тут, тут и тут. Считаю что - больше статей Богу статей, прошлые три мне не помогли.


      1. UndefinedRef
        25.08.2023 02:49
        +3

        Нет никакой проблемы. Подобных статей написано на разных ресурсах, включая хабр, огромное количество и все они под копирку. Но помимо хабра и статей, если другие источники, в которых можно посмотреть как и что делается и как делают другие люди.
        Тем более у dotnet хорошее комьюнити у которого всегда можно поспрашивать вопросы и попросить примеры.
        ИМХО более управляемо и понятнее стартовать бота подобным образом:
        https://github.com/ImoutoChan/GreenCaptchaBot
        https://github.com/ForNeVeR/Megadeth
        И в дальнейшем будет более проще им управлять (например, прикрутить админку или сорт оф)


        1. Zara6502
          25.08.2023 02:49
          -3

          Подобных статей написано на разных ресурсах, включая хабр, огромное количество и все они под копирку

          И ни одна не работает. За лето так и не нашел работающий вариант.

          ИМХО более управляемо и понятнее стартовать бота подобным образом

          Совсем не понятно что это и как с этим работать.


      1. lair
        25.08.2023 02:49

        А те кто знают и умеют, но допускают ошибки - пишут и получают шишки.

        Так это же хорошо. Смысл "шишек" в том, чтобы люди, которые допускают ошибки, исправлялись, а те, которые читают - видели, что это ошибки, и так делать не надо.


    1. mambastick Автор
      25.08.2023 02:49

      Приветствую. Спасибо за критику, ваши замечания действительно верны, просто изначально я хотел написать кучу методов и классов, а потом передумал, так как посчитал это неуместным для первой статьи, вот и забыл некоторые моменты подправить. Еще раз спасибо за критику, во второй статье (она будет продолжением этой) я поправлю код.

      ...а выходить из программы не надо?

      Ctrl+C или простое закрытие консоли уже не работает ?


      1. mambastick Автор
        25.08.2023 02:49

        Зачем создавать CancellationTokenSource, если вы его никогда не активируете?

        Он используется в старте бота, в обработчике апдейтов и ошибок, а также его можно использовать, чтобы остановить бота, например, поставить while(!cts.Token.IsCancellationRequested)


        1. lair
          25.08.2023 02:49

          Он используется в старте бота, в обработчике апдейтов и ошибок

          Нет, не используется. Все, что вы делаете - это передаете токен (не source) в StartReceiving. Но поскольку вы никогда не активируете сам CancellationTokenSource, это совершенно бесполезно, и можно с тем же успехом передать CancellationToken.None.

          также его можно использовать, чтобы остановить бота, например, поставить while(!cts.Token.IsCancellationRequested)

          Где и зачем?


      1. lair
        25.08.2023 02:49

        Ctrl+C или простое закрытие консоли уже не работает ?

        Конечно, нет. Это аварийное закрытие программы, а надо обрабатывать еще и нормальное.


  1. NCNecros
    25.08.2023 02:49
    +1

    Мне кажется было бы нагляднее если бы код раскидали по методам хотя бы. Вся логика в одном методе это такое себе.


    1. Zara6502
      25.08.2023 02:49
      -2

      а мне ООП тем и не нравится что раскидывается по одной строчке по миллиону методов и вообще непонятно что и зачем. И вместо того чтобы созерцать единое целое приходится всё запихивать в голову и структуру держать в голове, отвлекся на другой проект и всё, приходишь - а фиг его знает что там и как устроено.


      1. NCNecros
        25.08.2023 02:49
        +2

        Похоже вы никогда не видели и не пытались изменить внутренность .Select() размером в 300+ строк.


      1. lair
        25.08.2023 02:49
        +1

        а мне ООП тем и не нравится что раскидывается по одной строчке по миллиону методов и вообще непонятно что и зачем.

        Это не ООП, это плохой код. Вы про структурное программирование не слышали?


        1. Zara6502
          25.08.2023 02:49
          -3

          Это не ООП, это плохой код. Вы про структурное программирование не слышали?

          я как раз и пишу таким способом, а вот вы читать не умеете. я пишу про ООП, а не про код автора (уж не знаю с чего вы решили что я пишу про него), так как комментирующий NCNecros как раз и хотел бы видеть код написанный в стиле ООП.

          то что я вижу у автора мне понятно, если раскидать по методам то читабельность ухудшится. Кстати в 90-е, когда я изучал программирование об этом как раз говорилось как явный минус ООП. Так как ООП раскрывается на большом коде, так как происходит структурирование данных но объектам и его методам и вы изучаете код как функцию объекта, а не как совокупность команд. Я слава богу такой код не пишу и не читаю. Как-то хотел на github взять для себя исходник чтения файлов типа QOI - это был просто трындец, переносимость нулевая, невозможно взять код за основу и перенести на другой язык. В общем не люблю я ООП, но понимаю чем он полезен и что без него никак.


          1. lair
            25.08.2023 02:49
            +2

            я как раз и пишу таким способом,

            Каким "таким"?

            я пишу про ООП, а не про код автора (уж не знаю с чего вы решили что я пишу про него)

            Я тоже не про код автора.

            так как комментирующий NCNecros как раз и хотел бы видеть код написанный в стиле ООП.

            Цитирую дословно: "было бы нагляднее если бы код раскидали по методам хотя бы". Это не про ООП, это про структурное программирование.


            1. Zara6502
              25.08.2023 02:49
              -3

              Это не про ООП, это про структурное программирование

              Угу. Только при процедурном у вас, внезапно, процедуры. Методы и классы - это признаки ООП.

              Ме́тод в объектно-ориентированном программировании — это функция или процедура, принадлежащая[1] какому-то классу или объекту.

              Как и процедура в процедурном программировании, метод состоит из некоторого количества операторов для выполнения какого-то действия и имеет набор входных аргументов.


              1. lair
                25.08.2023 02:49
                +2

                Я, вроде, нигде не упоминал процедурное программирование, я оба раза написал о структурном.


                1. Zara6502
                  25.08.2023 02:49
                  -2

                  я воспринял ваши слова как то, что вы не понимаете о чем пишете (похоже это так), поэтому предположил что вы путаете структурное и процедурное. но раз вы сами признались что ничего не перепутали, то у меня вопросов еще больше к вам, только обсуждать это не вижу смысла, так как парадигма структурного программирования изжила себя очень давно и сейчас она в измененном виде уже подразумевается во всех языках как основа (разве только где-то, где безусловный переход является частью самого ЯП, например ассемблер). В современных языках безусловные переходы реализуются через разные прерывающие конструкции вроде return, break, continue и т.п. Например на платформе ATARI BASIC это в принципе реализовать невозможно, но это и ЯП конца 70-х.

                  C# это ЯООП и вы либо раскидываете всё по классам и методам либо валите код в один метод реализуя процедурное программирование. Это фуфуфу и говнокод, но это сделать можно.

                  Тот код который выложил автор уже написан по принципам структурного программирования (а на C# можно это сделать иначе? да что C#, с середины 80-х уже ЯП реализовывали этот подход как основной, я учился в вузе в 90-е и нам уже давали материал именно в контексте структурного программирования).


                  1. lair
                    25.08.2023 02:49

                    Тот код который выложил автор уже написан по принципам структурного программирования

                    Я вроде бы и не говорил обратного.

                    Я всего лишь сказал, что "раскидывается по одной строчке по миллиону методов и вообще непонятно что и зачем" - это следствие не ООП, а плохого кода. ООП такого не требует.

                    А умение корректно декомпоновать задачу на иерархические блоки - это как раз и есть основа структурного программирования. Если этого не уметь, не важно, что является блоком - метод или функция.


                    1. Zara6502
                      25.08.2023 02:49

                      это следствие не ООП, а плохого кода

                      так написано всё ООПэшное, по собственной статистике скажу, что в среднем на одну позицию информации в ООП приходится 5-7 строк кода, в процедурном 1.5

                      Чем меньше операций осуществляется в методе тем выше вес обязательного кода только для оформления. С этим постоянно идёт борьба, во всяком случае в .NET. Что не влияет никак на читаемость кода (или влияет минимально), это только вопрос скорости разработки и компиляции..

                      А умение корректно декомпоновать задачу на иерархические блоки - это как раз и есть основа структурного программирования. Если этого не уметь, не важно, что является блоком - метод или функция

                      Подходы изначально разные, нет тождественности между методом и функцией. Разные принципы работы с памятью, то есть с переменными. Тот же "Hello World!" в ООП из одной строки превращается в 20. При этом класс helloworld уже размещают в другом файле. При вызове сначала создает объект скласса helloworld, а потом вызывают метод который выводит текст на экран. И то, если разработчик не пойдет дальше и не сделать отдельный интерфейс по работе с экраном, который используется классом helloworld. Оно выглядит всегда красиво, блоки-блоки-классы-объекты-методы, а на практике - больше кода Богу кода. Но не стану спорить что сейчас без этого некуда.

                      Когда работаешь с консолью или канвасом там вообще по 3-4 строки кода может быть чтобы что-то вывести на экран (а порой и несколько разных объектов создать дополнительно). Я обычно пишу функцию отдельную где в качестве параметра принимаю все необходимые настройки, тогда хоть код не замусоривается.


                      1. lair
                        25.08.2023 02:49

                        так написано всё ООПэшное

                        Это утверждение нуждается в формализации и доказательстве.

                        по собственной статистике скажу, что в среднем на одну позицию информации в ООП приходится 5-7 строк кода, в процедурном 1.5

                        К сожалению, без формальной методики подсчета это исключительно ваше субъективное мнение.

                        Подходы изначально разные, нет тождественности между методом и функцией.

                        Инструменты разные. А подход к иерархической декомпозиции один и тот же - я это говорю, как человек, который писал и в ООП, и в ФП, и в процедурной парадигме.

                        Можно раскидать по одной строчке по миллиону методов, можно раскидать по одной строчке по миллиону функций, а можно раскидать по одной строчке по миллиону процедур. А можно - во всех трех случаях - этого не делать.

                        Тот же "Hello World!" в ООП из одной строки превращается в 20. При этом класс helloworld уже размещают в другом файле. При вызове сначала создает объект скласса helloworld, а потом вызывают метод который выводит текст на экран.

                        Я просто оставлю это здесь:

                        using System;
                        
                        namespace ConsoleApp1
                        {
                            internal class Program
                            {
                                static void Main(string[] args)
                                {
                                    Console.WriteLine("Hello World!");
                                }
                            }
                        }
                        

                        Это вот прямо из шабона приложение. И это я еще не стал брать новый .NET, где есть top-level statements.


    1. mambastick Автор
      25.08.2023 02:49

      Да, я изначально так и писал, но потом передумал, так как проект показался мне простым. В следующей статье (она будет продолжением этой), я уже буду использовать разные классы и методы, например, хендлер (обработчик) inline-клавиатуры, хендлер сообщений и т.д.


  1. Protos
    25.08.2023 02:49

    Спасибо, для начала все понятно


    1. mambastick Автор
      25.08.2023 02:49
      -1

      Благодарю! Это моя первая статья, боялся, что будет что-то непонятно)
      Скоро выпущу вторую часть этой статьи :З


  1. mikegordan
    25.08.2023 02:49
    -1

    Меня больше в таких программах интересует параллельное выполнение, и многопоточность (ведь как я понял тут "закидываются" в твой код на колбэки, не дожидаясь ответов?!)

    Являются ли все созданные объекты от этой библиотеки потокобезопасными?

    в отличии от придирчивости @lair


    1. Zara6502
      25.08.2023 02:49

      не совсем понял что именно вы хотите распараллелить? система работает по принципу "вопрос-ответ".


      1. lair
        25.08.2023 02:49

        Например, что будет если в момент отправки одного ответа придет следующий "вопрос"?


        1. Zara6502
          25.08.2023 02:49

          это вопрос к Дурову, я не знаю как именно сервер ТГ обрабатывает запросы. И главное - как вы своим кодом можете на это влиять.


          1. lair
            25.08.2023 02:49
            -1

            это вопрос к Дурову, я не знаю как именно сервер ТГ обрабатывает запросы.

            Сервер ТГ тут не при чем - он, очевидно, имеет достаточную степень паралеллизма, особенно учитывая, что их заведомо больше одного.

            И главное - как вы своим кодом можете на это влиять.

            Это тоже весьма понятно: если мой бот блокирует получение новых сообщений от сервера на время обработки уже полученных, параллелизма точно не будет. А если не блокирует, а обрабатывает их в отдельном потоке - то уже можно говорить о параллелизме в той или иной степени. Ну а дальше есть всякие варианты того, как это реализовать и какие есть ограничения.

            Только в посте про это ни слова, автор поста целиком полагается на библиотеку, которую использует.


            1. Zara6502
              25.08.2023 02:49

              автор поста целиком полагается на библиотеку, которую использует.

              тогда какие вопросы к автору?


              1. lair
                25.08.2023 02:49

                Ровно те, которые заданы в стартовом комменте треда: как поведет себя код в описанных условиях.


      1. mikegordan
        25.08.2023 02:49

        ну я ИлонМаск который через бота веду свой блог. В Боте можно лайкать , из него репостить , голосовашки разные, добавлять комменты.

        Аудитория 150 млн человек. После поста в первые 20 секунд больше лайков чем у Леди Гаги около ~270k + прибавляем все остальные выше перечисленные действия.

        Что я должен делать? Твой код подойдет для меня (ИлонМаска)?

        Если как ты пишешь ""вопрос-ответ", то что миллионы пользователей будут жать на кнопку , ничего не будет происходить т.к. они все встанут последовательно в очередь на ответ?


        1. Zara6502
          25.08.2023 02:49
          -1

          я не специалист в этом, только сделаю предположение, что это задача сервера Telegram, он создаёт инстанс на каждый запрос - то есть копию обработчика и каждый запрос обрабатывается с разными ID Threads. Если API изначально задумывался как то, что будет работать на стороне владельца бота, то у меня для вас печальные новости, если вы захотите чтобы ваш бот работал с миллионами одновременных запросов, то у вас как минимум три проблемы которые нужно будет решить - провайдер, железо, а уже потом софт. Без первых двух абсолютно не важно насколько правильно и красиво у вас будет написан бот - он все равно не сможет обрабатывать все запросы (точнее, он будет обрабатывать только то, что будет успевать провайдер и ваше железо)


          1. lair
            25.08.2023 02:49
            -1

            Если API изначально задумывался как то, что будет работать на стороне владельца бота

            Какой API?

            Очевидно, что код бота выполняется на той стороне, где бот развернут.

            если вы захотите чтобы ваш бот работал с миллионами одновременных запросов, то у вас как минимум три проблемы которые нужно будет решить - провайдер, железо, а уже потом софт.

            Как раз провайдер и "железо" в наш век облачной инфраструктуры - не проблема. Проблема - написать софт так, чтобы он это все эффективно использовал.


            1. Zara6502
              25.08.2023 02:49
              -1

              Как раз провайдер и "железо" в наш век облачной инфраструктуры - не проблема

              Это не вопрос конечной реализации мощностей, на вашей площадке или на чужой, это вопрос финансов. На Марс слетать тоже вроде можно, но что-то очереди из обывателей пока нет.


              1. lair
                25.08.2023 02:49

                это вопрос финансов

                Предположительно, если мы пишем на аудиторию в 150 млн. человек, то финансы на мощности есть. Что возвращает нас к вопросу "как написать код, чтобы он справлялся".


        1. mambastick Автор
          25.08.2023 02:49

          Этот проект не рассчитывался как что-то большое, для большой аудитории. В начале статьи сказано, что в этой статье я даю необходимую базу, чтобы вы писали своих ботов, используя эту базу. Поэтому здесь идет акцент больше на теоретических знаний, что, куда и почему.


          1. lair
            25.08.2023 02:49

            Поэтому здесь идет акцент больше на теоретических знаний, что, куда и почему.

            "Как обрабатывать многопоточность" - это как раз вполне себе теоретическая база.


    1. mambastick Автор
      25.08.2023 02:49

      В данном коде все будет работать последовательно, параллельной работы программы нет. Чтобы код работал в параллельном режиме, нужно использовать треды (thread) или таски (task), я предпочитаю таски, ставлю Task.Run в UpdateHandler и все работает прекрасно.


  1. vliashko
    25.08.2023 02:49

    Интересно было бы посмотреть как это вяжется с ИИ, а не хардкод «ответ-вопрос». Есть над чем подумать в продолжении данной темы. В целом есть немного замечаний по коду, так как чувствуется немного универских винформ, но для начала пойдет. Также вопрос насколько система будет работать с полным потоком пользователей? Были ли мысли об этом на этапе разработки?


    1. mambastick Автор
      25.08.2023 02:49

      В смысле вяжется с ИИ ? Немного не понял вопроса. Продолжение обязательно будет.

      В целом есть немного замечаний по коду, так как чувствуется немного универских винформ, но для начала пойдет.

      Честно говоря, в универе меня учили только C и после C++, C# я учу сам, используя разные ресурсы (книги, видеоуроки, статьи, интернет-материалы), поэтому я был бы очень рад получать критику моему коду, чтобы совершенствоваться!

      Также вопрос насколько система будет работать с полным потоком пользователей? Были ли мысли об этом на этапе разработки?

      Конкретно этот пример работает в последовательном режиме, параллельности тут нет, но ее легко добавить. В следующей статье обязательно рассмотрим этот пример. Насчет пользователей, за моей спиной уже несколько ботов и пока полеты отличные, пользователей не так много, где-то 500-1000 на бота и все отлично. Может, при увеличении пользователей где-то будут провалы, но пока все стабильно.


      1. lair
        25.08.2023 02:49

        где-то 500-1000 на бота и все отлично

        Тут вопрос не в том, сколько пользователей на бота. Тут вопрос в том, сколько сообщений в квант времени.