Telegram Bot: Broadcast – Użycie Extensions.Polling

W porzednim wpisie zmusiliśmy bota do pobierania wiadomości przy użyciu bardzo prymitywnego i chamskiego pollingu. Odpytywaliśmy nowe wiadomości z API co sekundę.

To bardzo toporne rozwiązanie. Nic nie potrafi poza odczytywaniem wiadomości i np. obsługę błędów musimy sobie sami napisać.

Telegram.Bot.Extensions.Polling

Z pomocą przyjdzie nam fajny dodatek Telegram.Bot.Extensions.Polling, który ładnie uporządkuje nam kod i doda kilka usprawnień.

Otwieramy nuget’a i instalujemy Polling

Robimy porządki

Przypomnijmy jak wygląda główna pętla programu

        public async Task Run()
        {
            await _bot.Init(); // inicjalizujemy bota
            await Task.Run(async () =>
            {
                while (true)
                {
                    await _bot.Process(); // tutaj się wszystko rozgrywa
                    await Task.Delay(1000); // czekamy sekundę
                }
            });
        }

Prawda, że paskudna? Zatąpimy ją teraz elementami z w/w liba. Nie będziemy już potrzebować pętli, wszystko zinicjujemy w _bot.Init()

        public async Task Run()
        {
            await _bot.Init();
        }

Od razu się można poczuć lepiej widząc taką ilośc kodu!

Zobaczmy na Init() co tam mu w duszy gra…

        public async Task Init()
        { 
            const int lastUpdate = -1;
            
            var messages = await GetMessages(-1); // pobieramy ostatnie wiadomości
            if (!messages.Any())
            {
                return;
            }

            _messageOffset = messages.Last().Id; // ost. id wiadomości to offset
        }

Pamiętacie? Na początku odczytywaliśmy wszystkie ostatnie wiadomości aby wyciągnąc na wstępie offset, którego użyliśmy do pobierania najnowszych wiadomości w metodzie Process(). Fuj.

Co powiecie na takiego zgrabnego inita?

        public async Task Init()
        {
            var receiverOptions = new ReceiverOptions
            {
                // chcemy wyłącznie wiadomości
                AllowedUpdates = new[] { UpdateType.Message },
                // pobieramy wszystkie wiadomości jakich nie przetworzyliśmy
                ThrowPendingUpdates = true
            };

            // włączamy odbieranie wiadomości
            await _client.ReceiveAsync(
                UpdateHandlerAsync,
                ErrorHandlerAsync,
                receiverOptions
            );
        }

        private async Task UpdateHandlerAsync(ITelegramBotClient client, Update update, CancellationToken token)
        {
            throw new NotImplementedException();
        }

        private async Task ErrorHandlerAsync(ITelegramBotClient client, Exception ex, CancellationToken token)
        {
            throw new NotImplementedException();
        }

Mamy tutaj 3 kroki:
1️⃣ włączamy nasłuch na nowe wiadomości
2️⃣ nowa wiadomość to trafi do metody UpdateHandlerAsync
3️⃣ wszelkie błędy ze strony API uruchomią metodę ErrorHandlerAsync

Poprzednio obsługę otrzymywania oraz procesowania nowych wiadomości załatwialiśmy w metodzie Process.

public async Task Process()
{
	// pobieramy nowe wiadomości
	var newUpdates = await GetMessages(_messageOffset);

        // latalmy po zebranych wiadomościach
	foreach (var update in newUpdates)
	{
		// kazdą wiadomość wysyłamy do naszych grup (CHAT_ID)
		foreach (var chatId in _config.Chats)
		{
		       // wysłanie wiadomości dalej
		}
	}



      // aktualizujemy offset
      if (messages.Any())
      {
           _messageOffset = messages.Last().Id;
      }
}

Teraz przetwarzanie musimy przenieść do metody UpdateHandlerAsync

  private async Task UpdateHandlerAsync(ITelegramBotClient client, Update update, CancellationToken token)
        {
             // kazdą wiadomość wysyłamy do naszych grup (CHAT_ID)
             foreach (var chatId in _config.Chats)
	     {
		// wysłanie wiadomości dalej
	     }
        }

Pozbyliśmy się pętli, nie musimy się martwić o _messageOffset . Pobieranie wiadomości zrzuciliśmy na coś co na pewno lepiej działa od poprzedniej metody.

⚠️ Ważne: nie musimy wymyślać wszystkiego na nowo (DRY!)

A co z obsługą błędów? Możemy ja bezproblemu zaimplementowac w metodzie ErrorHandlerAsync. Mamy tam zwykły `Extension` i możemy sobie z nim zrobić co tylko chcemy. Wrzucić do loga, powiadomić kogoś o błędzię czy też wysłać użytkownikowi wiadomość, że „something is no yes” i aby spróbował ponownie.

Podsumowanie

Dzisiaj zastąpiliśmy naszą prymitywną pętlę requestująca co sekundę API bota libem, który ładnie i sprawnie nam to opakował. Co za tym idzie uprościliśmy kod.

Polling nie jest oczywiście jedyną metodą pobierania wiadomości z API. Poza tym ma wady:
👎 opóźnienie
👎 wysyłanie cały czas requestów do serwera przez co zaśmiecamy łącze
👎 niepotrzebnie obciążamy server zbędnymi requestami

👍 Dla mnie plusem pollingu jest prostota. Bardzo łatwo go zrozumieć i wdrożyć.

Mamy jakaś alternatywe do pollingu? Jasne, że tak: webhooki. W tej metodzie już nie odpytujemy API o nowe wiadomości. To API nam wysyła na nasz URL wszelkie informacje. Ale to temat na osobny post…

Tymczasem zapraszam do dodania się do newslettera aby niczego nie przegapić!

👉 https://high-five.cc/newsletter

Telegram Bot: Broadcast – rozgłaszamy wiadomości na inne grupy

Dzisiaj zajmiemy się pisaniem prostego bota do rozgłaszania wiadomości na inne grupy, które nie mają ze sobą żadnego kontaktu. Punktem styku grup będzie bot. Każdą wiadomość weźmie i roześle na wszystkie grupy. Dzięki takiemu mechanizmowi ludzie z różnych grup mogą się porozumieć między sobą.

🧾 Założenia projektu

Bot ma rozesłać wiadomość do wszystkich zdefiniowanych kanałów (chat_id) wtedy gdy:

✔️ napiszemy coś bezpośrednio do bota
✔️ napiszemy wiadomość na jedną z naszych grup

🤴👸 Tworzymy grupy testowe

W telegramie klikamy w hamburgera

i wybieramy: New Group

Wpisujemy nazwę grupy i klikamy: NEXT

Następnie wpisujemy nazwę naszego bota w wyszukiwarkę (uwaga! boty są dostępne dla wszystkich publicznie)

klikamy w bota, po czym w button: CREATE

Tworzymy min. 2 grupy do testów.



CHAT_ID pobieramy indentycznie jak to opisałem w pierwszym poście. Jedyną różnicą jest to, że w każdej z grup musimy skierować wiadomość do bota:

/ – slash oznacza, że takie wiadomości bot zobaczy (są to komendy, ale o tym poźniej)

Po wysłaniu w/w wiadomości wchodzimy na znany nam adres:
👉 https://api.telegram.org/botTOKEN/getUpdates

I pobieramy stamtad CHAT_ID.

Na ten moment mamy niezbędne tokeny/idki:
✔️ TOKEN
✔️ CHAT_ID Grupy 1
✔️ CHAT_ID Grupy 2

Możemy przystąpić do programowania 👍

✍️ Piszemy (najprostszy) kod bota!

Musimy zaprogramować bota, aby był wstanie czytać wiadomości. Dzisiaj zajmiemy się metodą zwaną „pollingiem„. Co 1 sekundę, będziemy odpytywać wiadomości w pętli. Napiszmy najpierw szkielet samego kodu:

    class Bot
    {
        private readonly BotConfig _config;
        private readonly ITelegramBotClient _client;

        public static Bot Create(BotConfig config)
        {
            return new Bot(config);
        }

        private Bot(BotConfig config)
        {
            _config = config;
            _client = new TelegramBotClient(config.Token);
        }

        public async Task Init()
        {
        }
      
        public async Task Process() 
        {
        }
    }

Zaznaczam, że piszemy możliwie najprostszy kod, a nie najbardziej elegancki.

Bot będzie implementować dwie publiczne metody:
✔️ Init() – inicjalizacja bota
✔️ Process – przetwarzanie wiadomości, uruchamiane co sekundę

Bota zamkniemy w specjalnym kontekście aby nie wrzucać wszystkiego do Program.cs

    class RunningContext
    {
        private readonly Bot _bot;

        public RunningContext(Bot bot)
        {
            _bot = bot;
        }

        public async Task Run()
        {
            await _bot.Init();

            await Task.Run(async () =>
             {
                 while (true)
                 {
                     await _bot.Process();
                     await Task.Delay(1000);
 // czekamy sekundę aby znów odpytać telegrama
                 }
             });
        }
    }

Metodą z liba Telegram.Bot z jakiej skorzystamy będzie await _client.GetUpdatesAsync. Uruchomiona bez parametrów zwraca 100 ostatnich wiadomości. Problem w tym, że chcemy wyłącznie mieć nowe updaty. Rozwiazać to możemy przez przesunięcie/offset. Pierwszym parametrem dla w/w metody jest właśnie offset, określający ID ostatniej wiadomości, od której chcemy pobierać kolejne informacje.

Napiszemy metodę, która zwróci wyłącznie zwykłe, ostatnie wiadomości (allowedUpdates: UpdateType.Messages) na podstawie offsetu (messageOffset).

        private async Task<Update[]> GetMessages(int messageOffset)
        {
            var messages = await _client.GetUpdatesAsync(messageOffset, allowedUpdates: new []
            {
                UpdateType.Message,
            });

            return messages;
        }

Ograniczamy się wyłącznie do Message, ponieważ nie chcemy rozgłaszać innych treści.

Przejdźmy do Init(). Pobierzemy ID ostatniej wiadomości (lastUpdate). Będzie to nasz offset wykorzystywany w dalszej części.

        public async Task Init()
        {
            const int lastUpdate = -1; // ✨magiczna wartosc -1✨ 

            var messages = await GetMessages(lastUpdate);
            if (!messages.Any())
            {
                return;
            }

            _messageOffset = messages.Last().Id;
        }

Świetnie, w _messageOffset mamy offsert.

Możemy się zająć implementacją broadcastowania wiadomości, który będzie w metodzie Process.

Działanie metody Process:
✔️ pobranie najnowszych wiadomości na postawie offset
✔️ wysłanie do zdefiniowanych grup (CHAT_ID)
✔️ aktualizacja offset

        public async Task Process()
        {
            // pobieramy nowe wiadomości
            var newUpdates = await GetMessages(_messageOffset);

            foreach (var update in newUpdates)
            {
                // kazdą wiadomość wysyłamy do naszych grup (CHAT_ID)
                foreach (var chatId in _config.Chats)
                {
                    var message = update.Message;
                    var messageChatId = message.Chat.Id;
                    // robimy zwykłego forwarda do naszych grup
                    await _client.ForwardMessageAsync(chatId, messageChatId, message.MessageId);
                }
            }

            // aktualizujemy offset
            if (newUpdates.Any())
            {
                _messageOffset = newUpdates.Last().Id;
            }
        }

Teraz gdy napiszemy bezpośrednio wiadomość do bota

To każda zostanie rozgłoszona (forwardowana) do wszystkich grup. Mamy załatwione nasze pierwsze założenie:
✔️ bot ma rozsyłać wiadomości bezpośrednio skierowane do bota

Jeszcze chcelibyśmy móc rozgaszać wiadomości, które zostały bezpośrednio napisane na grupach. Domyślnie bot nie widzi co piszemy (to ze względu na domyślny tryb: private, niech tak zostanie). Natomiast gdy użyjemy komendy bot ją zobaczy.

Czym jest komenda? To wiadomość zaczynająca się od / (slash):
/komenda [dalsza część wiadomości]

Napiszemy teraz kawałek kodu rozgłaszającego wiadomości. Przyjmiemy, że każda zaczynająca się od: /bc wiadomość zostanie przesłana wszędzie

Wracamy do metody Process

        public async Task Process()
        {
            // pobieramy nowe wiadomości
            var newUpdates = await GetMessages(_messageOffset);

            foreach (var update in newUpdates)
            {
                // każda wiadomość wysyłamy do naszych grup (CHAT_ID)
                foreach (var chatId in _config.Chats)
                {
                    var message = update.Message;
                    var messageChatId = message.Chat.Id;

                    // nie rozgłaszamy na grupę skąd pochodzi wiadomość, nie ma sensu powielać jej tutaj
                    if (chatId == messageChatId)
                    {
                        continue;
                    }

                    // pobieramy treść
                    var text = message.Text;

                    // UWAGA! bardzo prymitywne sprawdzenie, można o wiele lepiej
                    // ale to nie jest cel tego posta. Przypominam, że ma byc prosto :)
                    if (!string.IsNullOrEmpty(text))
                    {
                        // jeśli zaczyna się od / znaczy ze jest komendą
                        if (text.StartsWith("/"))
                        {
                            // jeśli to "inna" komenda, to nic nie rób
                            if (!text.StartsWith("/bc "))
                            {
                                continue;
                            }
                        }
                    }

                    // robimy zwykłego forwarda
                    await _client.ForwardMessageAsync(chatId, messageChatId, message.MessageId);
                }
            }

            // aktualizujemy offset
            if (newUpdates.Any())
            {
                _messageOffset = newUpdates.Last().Id;
            }
        }

Oczywiście nie jest to kod do wykorzystania na produkcji, ponieważ brakuje mu zabezpieczeń, np. teraz każdy nieproszony gość może spamować grupy wrzucając wiadomości do bota.

✔️ Podsumowanie

Nie jest to jedyny możliwy realizacji broadcasta. Tutaj użyliśmy najprostszego sposoby. Moglibyśmy napisać to napisać zupełnie inaczej np.:
– kopiując 1:1 wiadomości (wtedy użytkownicy staną się anonimowi) i wrzucić wszędzie
– przechwytywać treść, zmienić ją i przesłać dalej

Możliwości botów telegramowych są na prawdę ogrome. Użylismy w tym poście zaledwie wycinka ich możliwości.

Napisaliśmy nasz własny polling, co prawda chamski, ale działa. W następnym poście przerobimy sposób odczytywania wiadomości. Użyjemy do tego fantastycznego liba Telegram.Bot.Extensions.Polling, którego stosowanie jest bardzo wygodne.

Jeśli chciałbyś zautomatyzować/usprawnić procesy w swojej firmie za pomocą botów (telegram, slack, inne) albo po prostu Twój biznes potrzebuje bota to skontaktuj się ze mną.

Tymczasem zapraszam do dodania się do newslettera aby niczego nie przegapić!

👉 https://high-five.cc/newsletter

Telegram Bot: Jak zacząć?

Cześć ✋!
Dzisiaj będziemy pisać własnego bota do telegrama. Jeśli nie wiesz co to jest telegram, to zapraszam na oficjalną stronę po więcej informacji.
Dla tych, którym nie chce się kliknąc w linka 😉 to telegram w skrócie to: lekki, prosty w użytkowaniu, bezpieczny komunikator internetowy.

🤔 Ok, jak zacząć „programować” telegrama?

Telegrama „programuje się” pisząc boty. Boty można traktować jako programy zainstalowane w środowisku telegrama. Zachęcam do odwiedzenia strony https://core.telegram.org/bots, gdzie są przestawione ich możliwości (również polecam przeczytanie sekcji BotFather).


Ok, z w/w źródeł powinniśmy mieć już jakieś pojęcie na temat botów: czym są, jak się z nimi komunikujemy, jakie mają możliwości (z grubsza przynajmniej). Możemy iść dalej…

Najpierw musimy utworzyć bota i zdobyć jego API Token, aby móc się z nim komunikować. Robimy to właśnie za pomocą BotFathera.

Uruchamiamy telegrama i w wyszukiwarce wpisujemy: BotFather


Po wybraniu BotFathera pojawia się zwykle okno do komunikacji.
Wrzucamy tam komendę: /newbot

BotFather ładnie nas poinformuje, że czeka na podanie nazwy naszego bota.

Możemy tutaj wpisać dowolną nazwę. Będzie to imię bota, które będzie widoczne podczas komunikacji.

Następnie BotFather zapyta się o jego nazwę (@username). Musi to być unikatowa nazwa w całym systemie telegrama oraz musi się kończyć na: bot

Ja wybieram H5BroadcastBot ponieważ w następnym poście pokażę jak napisać prostego bota rozgłaszającego wiadomości na różne kanały, które nie są ze sobą w żaden sposób połączone. Prywatnie potrzebuję takiej funkcjonalności, więc chętnie opiszę proces jej powstawania.

BotFather po utworzeniu bota odeśle nam wiadomość, gdzie znajduje się token HTTP API (zachowajcie go TYLKO dla siebie).


Świetnie! Wszystko gotowe, możemy zacząć programować.

✋ Hello World (w .NETcie)

Do komunikacji z botem, możemy użyć dowolnego języka/technologii. Wszystko odbywa się przez API telegrama. Z racji tego, że siedzę w .NETcie od „N”-lat, więc to dla mnie jest naturalny wybór.

Na początku utworzymy zwykły projekt .NETowy. Nie ważne czy to core, czy framework >= 4, kto co woli. Ja wybieram core 3.1.

Skorzystamy z najpopularniejszej biblioteki do telegrama: telegram.bot

Biblioteka jest bardzo prosta w użyciu. Na początku wyślemy standardowy: Hello World, bo jakże zacząć od czegoś innego?

static async Task Main(string[] args)
{
    ITelegramBotClient client = new TelegramBotClient(TOKEN);
    await client.SendTextMessageAsync(CHAT_ID, "Hello World!");
}

I to prawie wszystko… gdyby nie jedna, mała rzecz… TOKEN! wiemy czym jest. Dostaliśmy go od BotFathera. Natomiast czym jest CHAT_ID!?

💬 Pobieramy CHAT_ID

CHAT_ID to nic innego jak ID kanału, na jakim bot ma działać. Może to być grupa, kanał lub bezpośrednia rozmowa. „Hello worda” zrobimy jako zwykłą rozmowę. Dobrze, wróćmy do BotFathera


Klikamy w zaznaczony wyżej adres (u Was będzie oczywiście inny).

Wybieramy START:


i już możemy komunikować się z botem. Napiszmy mu zwykłe „hi


dzięki temu, że coś wysłaliśmy do bota jesteśmy w stanie przechwycić CHAT_ID. Wystarczy wejść na adres:

👉 https://api.telegram.org/botTOKEN/getUpdates

W odpowiedzi znajdziecie CHAT_ID:

Wrzucamy tę wartość do consta w kodzie i uruchamiamy aplikację

Działa 👌

W następnym poście rozbudujemy możliwości bota.

Dodaj się do newslettera aby niczego nie przegapić!
👉 https://high-five.cc/newsletter