Pull to refresh

Comments 39

Впервые увидел как это делается. Да еще и на c#. Спасибо за статью.

Тоже делал бота, чтобы разделить ответы, создал уровни пользователя (Пользователь идентифицируется по ID), при том или ином ответе переходит на новый уровень, или назад.
Проблема встала при работе с inline кнопками, я могу, их послать, принять ответ, но вот не могу поменять на ходу. Не присылает inlineMessageId.
Не подскажешь, что делаю не так?

Отправка кнопок:

public async void SendInline(long chatId, CancellationToken cancellationToken)
    {
        InlineKeyboardMarkup inlineKeyboard = new(new[]
        {
                // first row
                new[]
                {
                    InlineKeyboardButton.WithCallbackData(text: "Кнопка 1", callbackData: "post"),
                    InlineKeyboardButton.WithCallbackData(text: "Кнопка 2", callbackData: "12"),
                },
 
            });
 
        Message sentMessage = await botClient.SendTextMessageAsync(
            chatId: chatId,
            text: "за что мне это??",
            replyMarkup: inlineKeyboard,
            cancellationToken: cancellationToken);
    }

Попытка изменить:

if (update!.CallbackQuery!.InlineMessageId != null)
                {
                    await botClient.EditMessageReplyMarkupAsync(inlineMessageId: update!.CallbackQuery!.InlineMessageId,
                        replyMarkup: inlineKeyboard);
 
                }

Кнопки присылают callbackData, но inlineMessageId всегда null

Привет!

CallbackData вполне достаточно с моей точки зрения. На эти данные ты можешь ориентироваться и строить логику программы.

Попробуй так.

Hidden text
using System;
using System.Threading;
using System.Threading.Tasks;
using Telegram.Bot;
using Telegram.Bot.Extensions.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Types.ReplyMarkups;

namespace TelegramTestProject
{

    class Program
    {
        static ITelegramBotClient bot = new TelegramBotClient("524584647:AAH8gg9L6Xsfc0ecJN7KrTVOeuimNZJMBKc");
        public static async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
        {
            // Некоторые действия
            Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(update));
            if (update.Type == Telegram.Bot.Types.Enums.UpdateType.Message)
            {
                var message = update.Message;
                if (message.Text.ToLower() == "/test")
                {
                    SendInline(botClient: botClient, chatId: message.Chat.Id, cancellationToken: cancellationToken);
                    return;
                }
            }
            if (update.Type == Telegram.Bot.Types.Enums.UpdateType.CallbackQuery)
            {
                string codeOfButton = update.CallbackQuery.Data;
                if (codeOfButton == "post")
                {
                    Console.WriteLine("Нажата Кнопка 1");
                    string telegramMessage = "Вы нажали Кнопку 1";
                    await botClient.SendTextMessageAsync(chatId: update.CallbackQuery.Message.Chat.Id, telegramMessage, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
                }
                if (codeOfButton == "12")
                {
                    Console.WriteLine("Нажата Кнопка 2");
                    string telegramMessage = "Вы нажали Кнопку 2";
                    // await botClient.SendTextMessageAsync(chatId: update.CallbackQuery.Message.Chat.Id, telegramMessage, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);

                    InlineKeyboardMarkup inlineKeyBoard = new InlineKeyboardMarkup(
                        new[]
                        {
                            // first row
                            new[]
                            {
                                // first button in row
                                InlineKeyboardButton.WithCallbackData(text: "Кнопка 3", callbackData: "post3"),
                                // second button in row
                                InlineKeyboardButton.WithCallbackData(text: "Кнопка 4", callbackData: "post4"),
                            },

                        });

                    // await botClient.EditMessageCaptionAsync(chatId: update.CallbackQuery.Message.Chat.Id, caption: telegramMessage, messageId: update.CallbackQuery.Message.MessageId);
                    await bot.EditMessageTextAsync(update.CallbackQuery.Message.Chat.Id, update.CallbackQuery.Message.MessageId, telegramMessage, replyMarkup: inlineKeyBoard, parseMode: Telegram.Bot.Types.Enums.ParseMode.Html);
                }
            }
        }

        public static async Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
        {
            // Некоторые действия
            Console.WriteLine(Newtonsoft.Json.JsonConvert.SerializeObject(exception));
        }


        static void Main(string[] args)
        {
            Console.WriteLine("Запущен бот " + bot.GetMeAsync().Result.FirstName);

            var cts = new CancellationTokenSource();
            var cancellationToken = cts.Token;
            var receiverOptions = new ReceiverOptions
            {
                AllowedUpdates = { }, // receive all update types
            };
            bot.StartReceiving(
                HandleUpdateAsync,
                HandleErrorAsync,
                receiverOptions,
                cancellationToken
            );
            Console.ReadLine();
        }

        public static async void SendInline(ITelegramBotClient botClient, long chatId, CancellationToken cancellationToken)
        {
            InlineKeyboardMarkup inlineKeyboard = new InlineKeyboardMarkup(
                // keyboard
                new[]
                {
                    // first row
                    new[]
                    {
                        // first button in row
                        InlineKeyboardButton.WithCallbackData(text: "Кнопка 1", callbackData: "post"),
                        // second button in row
                        InlineKeyboardButton.WithCallbackData(text: "Кнопка 2", callbackData: "12"),
                    },
                    // second row
                    new[]
                    {
                        // first button in row
                        InlineKeyboardButton.WithUrl(text: "Ссылка", url: "https://google.com"),
                        InlineKeyboardButton.WithCallbackData("CallbackData кнопка")
                    },

                });

            Message sentMessage = await botClient.SendTextMessageAsync(
                chatId: chatId,
                text: "за что мне это??",
                replyMarkup: inlineKeyboard,
                cancellationToken: cancellationToken);
        }
    }
}
Netonsoft.Json применимо к нашей заготовке нужна для того, чтобы у нас была возможность вывести на консоль сообщение от пользователя.

Зачем, если есть встроенный System.Text.Json?

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


Код
using System.Diagnostics;
using System.Text.Json;
using Telegram.Bot;
using Telegram.Bot.Extensions.Polling;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;

var botClient = new TelegramBotClient("token");

var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, _) => cts.Cancel(); // Чтобы отловить нажатие ctrl+C и всякие sigterm, sigkill, etc

var handler = new UpdateHandler();
var receiverOptions = new ReceiverOptions();
botClient.StartReceiving(handler, receiverOptions, cancellationToken: cts.Token);

Console.WriteLine("Bot started. Press ^C to stop");
await Task.Delay(-1, cancellationToken: cts.Token); // Такой вариант советуют MS: https://github.com/dotnet/runtime/issues/28510#issuecomment-458139641
Console.WriteLine("Bot stopped");

// Чтобы сильно не захламлять Main - это можно вынести в отдельный файл
class UpdateHandler : IUpdateHandler
{
    public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken)
    {
        Debug.WriteLine(JsonSerializer.Serialize(update));
        // Вообще, для обработки сообщений лучше подходит паттерн "Цепочка обязанностей", но для примера тут switch-case
        // https://refactoring.guru/ru/design-patterns/chain-of-responsibility
        switch (update)
        {
            case
            {
                Type: UpdateType.Message,
                Message: { Text: { } text, Chat: { } chat },
            } when text.Equals("/start", StringComparison.OrdinalIgnoreCase):
            {
                await botClient.SendTextMessageAsync(chat!, "Добро пожаловать на борт, добрый путник!", cancellationToken: cancellationToken);
                break;
            }
            case
            {
                Type: UpdateType.Message,
                Message.Chat: { } chat
            }:
            {
                await botClient.SendTextMessageAsync(chat!, "Привет-привет!!", cancellationToken: cancellationToken);
                break;
            }
        }
    }
    public Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, CancellationToken cancellationToken)
    {
        Console.Error.WriteLine(exception);
        return Task.CompletedTask;
    }
}

Спасибо за дополнение!

Одно и то же действие можно сделать миллионом разных способов. Особенно в программировании. Netonsoft.Json мне просто нравится, я к ней привык и работаю с ней. К тому же я планировал показать в будущем не только элементарный вывод объекта в консольку.

Желательно начать отвыкать, потому что многие библиотеки отходят от Newtonsoft.Json, теперь это лишний оверхед.

System.Text.Json позволяет делать ту же сериализацию в одну строчку, при этом не надо тянуть никаких зависимостей:

JsonSerializer.Serialize(data)

Затем, что System.Text.Json не умеет создавать Json произвольной формы и при развитии такого проекта возникнут проблемы. Тебе для создания такого json потребуется делать класс и уже его сериализовывать. А в Netonsoft.Json можно написать так:
var json = new JObject
{
["error"] = e.Message.TrimEnd('.'),
["code"] = code,
["HResult"] = e.HResult,
["content"] = content
};
return json.ToString();

И конечно пока System.Text.Json не научится формировать и читать такие запросы {"data": [{"test1":1},{"test2":"2"}]} сложно воспринимать ее как полноценный json.

Умеет и то и другое.

https://learn.microsoft.com/ru-ru/dotnet/api/system.text.json.jsondocument?view=net-8.0

а точнее?

Как в System.Text.Json сделать json объект произвольной структуры не создавая класса, и как прочитать такой json {"data": [{"test1":1},{"test2":"2"}]} ?

Немного ошибся. JsonDocument только для чтения нужен. А для записи есть Utf8JsonWriter и System.Text.Json.Nodes.

Вот пример с System.Text.Json.Nodes:

using System;
using System.Text.Json;
using System.Text.Json.Nodes;

var jsonObject = new JsonObject {
  ["data"] = new JsonArray {
    new JsonObject {
      ["test1"] = JsonValue.Create(1),
    },
    new JsonObject {
      ["test2"] = JsonValue.Create("2")
    }
  }
};

var jsonText = JsonSerializer.Serialize(jsonObject);
Console.WriteLine(jsonText); // {"data":[{"test1":1},{"test2":"2"}]}

Но лично я ни разу не сталкивался с ситуацией, где прям совсем никак нельзя в виде объектов представить какой-то документ.
Конкретно в этом случае через объекты можно сделать так:

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

var rootObject = new Root {
 Data = [
     new Int32PropertyElement {Property = 1},
     new StringPropertyElement {Property = "2"}
 ],     
};

var jsonText = JsonSerializer.Serialize(rootObject);
Console.WriteLine(jsonText);

var deserialized = JsonSerializer.Deserialize<Root>(jsonText);

public class Root {
  [JsonPropertyName("data")]
  public List<Element> Data {get;set;}
}

[JsonDerivedType(typeof(Int32PropertyElement), typeDiscriminator: "int")] // typeDiscriminator нужен только для десериализации. Сериализация будет работать и без него
[JsonDerivedType(typeof(StringPropertyElement), typeDiscriminator: "string")]
public abstract class Element{}
public class Int32PropertyElement: Element {
  [JsonPropertyName("test1")]
  public int Property {get;set;}
}

public class StringPropertyElement:Element {
  [JsonPropertyName("test2")]
  public string Property {get;set;}
}

Кажется в начале статьи есть достаточно важная неточность, Вы указываете, что "обновления будем получать периодически опрашивая Telegram сервер на наличие новых обновлений", но ведь указанный Telegram.Bot.Extentions.Polling использует long polling, поддерживаемый API Telegram согласно документации:

getUpdates

Use this method to receive incoming updates using long polling

Таким образом, никакого периодического опроса сервера нет, мы подключаемся к нему и висим до тех пор, пока серверу не будет что отдать, сразу после получения новых данных библиотека делает повторное подключение к API Telegram и опять висит. За счёт этого нет каких - либо задержек при получении обновлений и боты работают моментально.

По моему скромному мнению, с момента поддержки long polling не стало самого главного преимущества вебхуков (моментальности), а инфраструктурных недостатков и неудобств сильно больше. В наших проектах перешли на long polling и очень довольны, удобно.

Т.е. нет смысла теперь использовать веб хуки? И при частом обращении с long pollingo'oм не блокирует сама телега, и не будет проблем? Можно использовать даже в коммерческих продуктах?

Не увидел комментария, отвечу спустя несколько месяцев... Да, я полагаю, что больше нет никакого практического смысла использовать вебхуки, в т.ч. в коммерческих продуктах.

Спасибо за дополнение!

Таким образом, никакого периодического опроса сервера нет...

Long polling... Запрос на сервер телеги, ожидание ответа, получение ответа, обработка. Снова запрос, снова ожидание ответа, снова получение ответа, еще одна обработка... Кажется это цикл, в каждой итерации которого мы опрашиваем сервер?

По моему скромному мнению, с момента поддержки long polling не стало самого главного преимущества вебхуков (моментальности), а инфраструктурных недостатков и неудобств сильно больше. В наших проектах перешли на long polling и очень довольны, удобно.

В красоте Ваших слов потерял смысл если честно ) Наверное Вы имели ввиду, что теперь технологии long polling == вебхукам по продуктивности? С этим, пожалуй, соглашусь.

Попробую спросить у Вас. Long polling теперь можно даже использовать в коммерческих/массовых продуктах? И теперь смысл вебхуков отпало? Можно одним long polling'ом обойтись?

У меня несколько ботов на подобном функционале работают. Им пользуется две организации. В каждой несколько десятков сотрудников постоянно онлайн. Заказывали для своих внутренних коммерческих целей. Ещё на одном, уже собственном, помимо технического функционала ещё и финансовая. Зарегистрировано 700+ юзеров. Серв был в Харькове, его разбомбили, света нет, проект перенесу, кину ссыль на бот. Оповещение о новых объявлениях на Колесах по подписке. И тот и другой и третий бот работают хорошо, не жалуются. Хотя там не 3, а 5 их уже. Возможно я наткнусь на какой-то технический потолок. Но пока все норм.

Пользуйтесь на здоровье!

Пожалуйста, теперь покажите пример на вебхуках

Может быть чуть позже )

Привет. Спасибо за статью. Пару вопросиков

1) Есть ли библиотека не для ботов, чтобы зайти под своим логином и паролем и например фильтровать сообщения из всех своих подписок?

2) Как можно выдернуть урл на медиа ресурсы, например на видео? Мы долго рыскали по API , но не нашли такого функционала. При этом знаем что существуют боты которые формируют эти линки если переслать им сообщение с видео.

Спасибо.

  1. Есть вполне стандартное официальное решение — TDLib
  2. Можно пример такого бота?
    В api для ботов можно получить ссылку для скачивания, но она в себя включает токен бота.
    Может, подходят только те видео, которые добавлены по ссылке (на yt, к примеру), а не те видео, которые загружаются в сам телеграм?

Привет всем. Новичок совсем в коде.

Но не понимаю до конца. Как мне сделать, чтобы бот работал всегда ? А то она работает, когда я запускаю программу. А желательно, что бы он работал круглосуточно.

За ранее Спасибо!

Бот — это такая же программа, как и любая другая.
Пока она запущена — он работает.


Чтобы автоматизировать, например, перезапуск при падениях, и запуск при включении компьютера/сервера — существуют различные планировщики задач.
На Windows — это встроенный, не поверишь, "Планировщик задач" он же "Task Scheduler".
На Linux — systemd.


Это не единственные варианты, но на данном этапе они будут самыми простыми.
Инструкции по настройке можно найти в интернете.

Думаю не верно выразился.

Хотелось бы, чтобы он работал, когда и копм выключен.

Чтобы бот работал в любое время когда напишут.

Может это я как-то неправильно выразился?

Бот — это такая же программа, как и любая другая. Пока она запущена — он работает.

Тоесть тебе нужно поставить его на такой компьютер, который работает круглосуточно и не выключается. Такой компьютер "Сервер". Также ты можешь арендовать небольшой кусок такого компьютера за сравнительно небольшие деньги: Это называется "VPS".

Ещё ты можешь посмотреть в сторону вебхуков и каких-нибудь serverless решений, типа Yandex Cloud Functions / AWS Lambda / Azure Functions итд.

Есть и бесплатные, но геморные варианты - heroku, например. На нём можно поднять webhook-бота совершенно бесплатно, но при 30 минутах бездействия он будет автоматически выключен.

Спасибо.

Отлично. Понял.

почему у меня не отображается кнопка "Запустить"?

Даже не знаю... Попробуйте нажать F5

Единственное, я не понял, почему был выбран .net core 3.1 :)Шестой же тоже LTS

Я начал работу над статьей и видеороликом когда у меня была установлена Visual Studio 2019.

Однозначно плюс! C# больше как хобби, а сам учусь только, но хотелось бы писать бота на телеграм для практики. Буду ждать новой информации по функционалу.

спасибо за статью, очень помогло

кто то знает как сохранять отправленное боту фото? буду очень благодарен

Sign up to leave a comment.

Articles