Добрый день, уважаемые читатели!
В этой статье я расскажу, как достаточно просто отправлять сообщения в мессенджер Telegram из микроконтроллеров ESP8266 (и ESP32) и устройств на их основе без использования сторонних библиотек. Как мы выяснили из предыдущей статьи, только для отправки сообщений не требуется сложных манипуляций, достаточно вызвать один-единственный метод sendMessage. Чем мы и займемся в данной статье. Кроме того, я расскажу как это сделать разными вполне рабочими способами, а уж Вы сами выберете для себя наиболее удобный. А заодно расскажу и о грабельках, которые притаились на данном пути.
Когда-то достаточно давно я уже написал статью, как зарегистрировать своего собственного т.н. бота (или робота) Telegram и научиться отправлять сообщения с помощью скрипта на промежуточном сайте. Написана она была в те далекие и темные времена, когда Роскомнадзор блокировал мессенджер на территории матушки России, и приходилось “извращаться”, чтобы получить доступ к вожделенному Telegram API, в том числе и с “умных” и не очень устройств. Но времена идут, ситуация меняется, и рецепт, изложенной в той статье, стал далеко не актуален. Наоборот, далеко не каждый любитель имеет свой личный сайт и доступ к хостингу. А между тем, тема эта пользуется неизменно высокой популярностью, судя по поисковым запросам на данном сайте. Поэтому я и решил написать новую статью, где хочу “углубить и расширить” данную тему (как говаривал один небезызвестный исторически отмеченный персонаж).
Все манипуляции и жестокие опыты я буду проводить над прошивкой с MQTT-управлением, которую я описывал в статье: Телеметрия на ESP8266 и MQTT. Пошаговое руководство по созданию DIY-проекта с удаленным управлением. По сути данная статья может служить официальным продолжением этой статьи. Но Вы можете, конечно, встроить код и в Ваши наработки.
Кроме того, могу ответственно заявить (когда-то проверял лично), что всё то же самое можно применить и на микроконтроллерах ESP32, если вы пишете код для них на фреймворке Arduino. Но после соответствующей “доработки напильником”, так как сетевые классы WiFi и SSL у них немного отличаются. В остальном приведенный в данной статье код останется вполне работоспособен.
Что потребуется для отправки уведомлений?
1. Само собой, Вам потребуется микроконтроллер ESP8266 или ESP32, то есть любая плата на их основе. Программировать я буду в VSCode + PlatformIO, но всё то же самое можно проделать и ArduinoIDE, только с меньшим комфортом. В любом случае я буду использовать “стандартную” платформу ESP8266 Arduino Core. Для ESP32 потребуется немного другая платформа.
2. Прежде чем мы с вами начнем обсуждать программную реализацию, Вы должны создать бота, получить его токен и chat id. Как это сделать, я рассказывал в прошлой статье: Создание (регистрация) telegram-бота для отправки уведомлений из устройств умного дома и не только. Если Вы ещё не читали и не знаете как это делается – начните с этой статьи.
3. Неплохо бы знать, что такое HTTP-запросы и с чем их кушают. А поскольку Telegram API принимает только TLS-соединения, Вы должны знать что это такое и как их использовать в Вашей программе.
У Вас всё готово? Токен сгенерировали, статьи прочитали? Достаем плату, открываем проект и начинаем!
Подготовительные операции
Как я уже написал выше, для примера я буду использовать код, про который я рассказывал ранее в статье Телеметрия на ESP8266 и MQTT. Пошаговое руководство по созданию DIY-проекта с удаленным управлением. Давайте просто добавим туда функцию отправки сообщений. Но прежде чем мы это сделаем, нам придется отключить TLS-соединение с MQTT-брокером! Как так? Почему?
Дело в том, что Telegram API обрабатывает запросы только через защищенные соединения. И никак иначе. Все запросы, отправленные на порт HTTP (80) автоматически перенаправляются на порт HTTPS (443). То есть чтобы воспользоваться Telegram API, волей-неволей придется играть по их правилам. И это была бы не большая проблема, если бы не одно но, которое решительно меняет ситуацию.
Дело в том, что для установки и поддержания защищенного соединения требуется достаточно много памяти. Протокол TLS был разработан для настольных систем с большим объемом памяти, поэтому им по умолчанию требуется буфер размером как минимум 16 КБ для приема и передачи. А всего на одно TLS-соединение библиотекой BearSSL может расходоваться аж до 22 КБ оперативной памяти (пруфы здесь). Поэтому для ESP8266, у которого общая доступная куча составляет что-то около 40 КБ, просто не хватит памяти для нескольких соединений одновременно, и в каждый момент времени возможно использовать только одно защищенное соединение.
Исходя из этого мы не можем одновременно поддерживать защищенное соединение с MQTT-брокером и отправлять сообщения в Telegram. Придется пожертвовать MQTT, хоть это и не желательно. Справедливости ради стоит отметить, что иногда защищенное подключение к брокеру и вовсе не требуется. Например, если вы планируете “поднять” свой личный локальный MQTT-брокер, или подключаетесь к локальному брокеру Home Assistant.
Это и есть одни из тех самых граблей, про которые я упомянул выше.
Для ESP32 + Arduino, это выполнять не нужно!
Итак, модифицируем ранее сделанную прошивку.
1. Первым делом убираем глобальную переменную типа BearSSL::X509List и WiFiClientSecure заменяем на простой WiFiClient:
/************************************************************************** * Глобальные переменные *************************************************************************/ // Подключение к MQTT брокеру (серверу) // !!! Поскольку у ESP8266 памяти очень мало (~40 kB), нам придется пожертвовать "постоянным" TLS-подключением к MQTT // BearSSL::X509List certISRG(ISRG_Root_x1); WiFiClient wifiMqtt; // BearSSL::WiFiClientSecure wifiClient; PubSubClient mqttClient(wifiMqtt);
2. Затем убираем привязку хранилища сертификата certISRG к wifiClient в теле функции bool wifiConnected(). Я специально закомментировал ненужные функции, а не удалил их, дабы было понятно, как и что убирать.
Serial.print("Current time: "); Serial.print(asctime(&timeinfo)); // Теперь можно привязать корневой сертификат к клиенту WiFi // wifiClient.setTrustAnchors(&certISRG); };
3. Не спешите удалять сам сертификат ISRG_Root_x1
Он нам еще пригодится для OTA! Для возможности OTA через TLS просто перенесем нужный код в функции обработки обновлений OTA otaStart() (с использованием локальных переменных, дабы они были автоматически удалены из памяти после использования):
// ОТА обновление void otaStart(const char* linkOTA) { Serial.print("OTA :: Receiving OTA: "); Serial.print(linkOTA); Serial.println("..."); // Настраиваем безопасное подключение к серверу BearSSL::WiFiClientSecure wifiOTA; BearSSL::X509List certISRG(ISRG_Root_x1); wifiOTA.setTrustAnchors(&certISRG); ...
4. Не забудьте также заменить порт MQTT-брокера обратно на обычный:
// Настраиваем MQTT клиент mqttClient.setServer(mqttServer, mqttPort);
Отлично! Можно загрузить модифицированную в микроконтроллер и убедиться, что все работает, но без TLS.
Работает? Идем далее…
Добавляем сертификат ЦС для Telegram API
Прежде всего нам нужно добавить сертификат центра сертификации (ЦС) в нашу программу. Если бы Telegram API использовал бы тот же самый ЦС ISRG Root X1, то ничего не потребовалось бы, но увы – у них другой ЦС, и нам придется добавить его сертификат в прошивку. Как это сделать, я писал ранее в другой статье. Но вы можете не мучаться, а просто скопировать готовый код в свою программу отсюда (или из кода на GitHub):
// Корневой сертификат для Telegram API, действителен до 29 июня 2034 static const char TG_API_Root[] PROGMEM = R"CERT( -----BEGIN CERTIFICATE----- MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEh MB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBE YWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3 MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRo ZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3Mg MiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggEN ADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCA PVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6w wdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXi EqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMY avx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+ YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLE sNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h /t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5 IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmlj YXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQAD ggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNy OO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7P TMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQ HmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mER dEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5Cuf ReYNnyicsbkqWletNw+vHX/bvZ8= -----END CERTIFICATE----- )CERT";
Все готово, можно писать функцию кода отправки сообщения. Я написал аж четыре разных варианте – просто для того, чтобы продемонстрировать возможные варианты работы с запросами. А вы можете выбрать оптимальный на ваш взгляд.
Повторюсь ещё раз, для отправки сообщения нам нужно отправить к API GET или POST запрос. По идее, мы можем воспользоваться классом HTTPClient, который объявлен в модуле <ESP8266HTTPClient.h> и “не делать себе моск”. И мы этот вариант обязательно рассмотрим, но в конце. Но лично на мой взгляд, HTTPClient явно избыточен для наших целей, обычный WiFiClientSecure легко справится и сам – тем более что нам и не особо важно обрабатывать коды ответа.
Способ 1. Отправляем GET-запрос “по простому”
Вначале нужно настроить безопасное подключение к серверу Telegram API.
void telegramSendMessage_Get_Raw(const char* chatId, char* message) { // Настраиваем безопасное подключение к API WiFiClientSecure wifiTg; // wifiTg.setInsecure(); X509List certTg(TG_API_Root); wifiTg.setTrustAnchors(&certTg); ...
Важно! Для отправки уведомлений здесь и далее следует использовать только локальные переменные X509List и WiFiClientSecure. Это позволит освободить память после того, как сообщение будет отправлено. Если этого не сделать, память будет использована постоянно и OTA через TLS работать не будет.
По большому счёту, вы можете “забить болт с левой резьбой” на сертификат и использовать опцию wifiTg.setInsecure(); при подключении к API. В этом случае TLS соединение устанавливается, но сертификат сервера никак не проверяется. Однако экономию памяти это почти не дает, так что особого смысла не имеет – разве что за сроками действия сертификатов следить не нужно. Пока вы только отправляете сообщения, особых проблем с безопасностью от “подмены сервера” ожидать не приходится. Поэтому я оставил этот вариант в тексте функций, но не действительным. Поступайте по своему разумению.
Далее пробуем подключиться к серверу и порту 443:
// Пробуем подключиться к Telegram API: обратите внимание - API принимает входящие запросы только по HTTPS на 443 порту if (wifiTg.connect("api.telegram.org", 443)) { ... // Закрываем соединение wifiTg.stop(); } else { Serial.printf("#ERROR# :: Failed to connect to Telegram API: %d\r\n", wifiTg.getLastSSLError()); };
Если подключение не выполнено – проверяем код ошибки через getLastSSLError(). Если он равен -1000 – то это означает, что МК не хватает памяти для установки TLS соединения. Скорее всего, TLS-соединение уже где-то существует.
Теперь можно сформировать нужный запрос:
// Формируем и отправляем GET-запрос для отправки сообщения wifiTg.printf("GET https://api.telegram.org/bot%s/sendMessage?chat_id=%s&parse_mode=HTML&text=%s HTTP/1.1\r\n", tgToken, chatId, message); // Отправляем служебные HTTP-заголовки wifiTg.println(F("Host: api.telegram.org")); wifiTg.println(F("User-Agent: ESP8266")); // Отправляем пустую строку, которая указывает серверу, что запрос полностью отправлен wifiTg.println(); // Читаем ответ сервера (только первую строку) Serial.print(F("TG :: API responce: ")); Serial.println(wifiTg.readStringUntil('\n')); // Закрываем соединение wifiTg.stop();
Полный текст всех функций вы найдете в конце статьи на GitHub-е.
Проверяем созданную функцию. Я подготовил многострочное текстовое сообщение такого вида:
Версия функции отправки
Жирный шрифт
Наклонный шрифт
Подчеркнутый шрифт
Это позволяет оценить правильность работы функции. Я вставил код отправки сообщения сразу после подключения к MQTT. Разумеется, вы должны вставить его в нужном вам месте, например при изменении температуры в доме или в других случаях. Но для примера сойдет и так.
telegramSendMessage_Get_Raw(tgChatId, (char*)"Версия telegramSendMessage_Get_Raw (не правильно)\n<b>Жирный шрифт</b>\n<i>Наклонный шрифт</i>\n<u>Подчеркнутый шрифт</u>");
Ага! Что-то блямкнуло, сообщение пришло. Только как-то не так - пришла только первая строка:
Произошло так потому, что заголовки запроса передаются на сервер построчно, и строки текста были восприняты сервером как отдельные заголовки запроса. Как же быть?
Нужно применить “процентный” способ кодирования символов в URI, когда вместо символа передается его код в HEX-виде и символом % впереди. Например пробел обозначается как %20, а символ переноса строки – %0A. Но в данном случае пробелы кодировать не нужно. Тогда наш текст мы должны записать так:
telegramSendMessage_Get_Raw(tgChatId, (char*)"Версия telegramSendMessage_Get_Raw (правильно)%0A<b>Жирный шрифт</b>%0A<i>Наклонный шрифт</i>%0A<u>Подчеркнутый шрифт</u>");
Проверяем. Теперь все работает корректно.
Но это не очень удобно. Давайте сделаем эту замену программным способом, а заодно и функцию немного “оптимизируем”.
Способ 2. Оптимизируем GET-запрос
Во первых, сразу добавим преобразование текста запроса. Я использовал замену только служебных символов переноса, хотя percent-encoding предусматривает больше символов под замену:
void telegramSendMessage_Get_Encoded(const char* chatId, char* message) { // Percent-encoding символов в URI на HTML-коды %XX String msg = String(message); msg.replace("\n", "%0A"); msg.replace("\r", "%0D");
Ещё давайте перенесем наши локальные переменные для подключения wifiTg и certTg из стека в кучу (heap). В предыдущем варианте я объявил их как самые обычные локальные переменные, и при выполнении функции они попадут в стек. Я предлагаю перенести их в общую кучу (heap) или динамическую память.
Зачем это нужно? Просто для демонстрации, что “так можно было”. С точки зрения памяти почти ничего не меняется – стек растет “сверху”, куча – “снизу”, но общий объем у них один на двоих. Фрагментации при короткоживущих переменных практически не будет. В общем, особого смысла в переносе переменных лично я не вижу. Но в сети часто встречаются примеры и такого применения. Поэтому я решил использовать такой вариант – для демонстрации.
// Настраиваем безопасное подключение к API с размещением временных объектов в куче (heap) WiFiClientSecure *wifiTg = new WiFiClientSecure; // wifiTg->setInsecure(); X509List *certTg = new X509List(TG_API_Root); wifiTg->setTrustAnchors(certTg);
Обратите внимание – теперь наши переменные это указатели. И вместо точки при вызове методов класса следует использовать стрелку ->. Ну и нужно обязательно не забыть удалить эти переменные “вручную” в конце работы функции, иначе сразу же произойдет утечка большого объема памяти.
// Не забываем удалить данные, размещенные в куче delete wifiTg; delete certTg;
Кроме того, допустим я решил, что передавать заголовки запроса отдельно – это слишком утомительно, и подготовил шаблон запроса целиком:
static const char* tgGetRequest = "GET https://api.telegram.org/bot%s/sendMessage?chat_id=%s&parse_mode=HTML&text=%s HTTP/1.1\r\n" "Host: api.telegram.org\r\n" "User-Agent: ESP8266\r\n\r\n";
Теперь отправка сообщения сводится к одной строчке:
// Формируем и отправляем GET-запрос для отправки сообщения (сразу целиком, по шаблону) wifiTg->printf(tgGetRequest, tgToken, chatId, msg.c_str());
Проверяем:
Всё работает. Конечно, все эти оптимизации спорны, но так тоже можно. Можно было бы на этом и остановиться, но….
Способ 3. Используем POST и JSON
Но всё-таки взаимодействие с Telegram API GET-запросами – это не совсем правильно. API рассчитан на применение POST-запросов и JSON-пакетов, которые представляют собой объекты, “упакованные” специальным образом в строки. Это позволит не думать о кодировках и символах, а отправлять текст в любом виде.
Вначале необходимо этот JSON сформировать. Можно, конечно, надыбать специальную библиотечку для работы с JSON, но для генерации она нам не нужна. Сделаем все гораздо проще, “ручками”, с помощью банального класса String:
// Формируем JSON-пакет с данными String sJSON = "{\"chat_id\":" + String(chatId) + ",\"parse_mode\":\"HTML\",\"disable_notification\":false,\"text\":\"" + String(message) + "\"}";
Дополнительно JSON позволяет передавать любое количество полей и опций запроса – просто загляните в документацию к sendMessage.
Теперь можно отправить эту строку, воспользовавшись предыдущим способом:
static const char* tgPostRequest = "POST https://api.telegram.org/bot%s/sendMessage HTTP/1.1\r\n" "Host: api.telegram.org\r\n" "User-Agent: ESP8266\r\n" "Content-Type: application/json\r\n" "Content-Length: %d\r\n\r\n" "%s\r\n\r\n";
Обратите внимание на шаблон HTML-запроса: перед самими POST-данными (это последний аргумент %s в шаблоне) и после него должна быть пустая строка (два раза \r\n). А кроме того, мы должны передать серверу формат передаваемых данных и длину этой строки JSON.
// Формируем и отправляем POST-запрос для отправки сообщения (сразу целиком, по шаблону) wifiTg->printf(tgPostRequest, tgToken, sJSON.length(), sJSON.c_str());
Вот, в общем-то и все отличия. Зато гораздо правильнее. Подумав, можно добавить отключение звука ( disable_notification=true ) и прочие опции при отправке сообщения. Но это Вам на “домашнее задание”.
Полный текст новой функции выглядит так:
void telegramSendMessage_Post(const char* chatId, char* message) { Serial.print("TG :: Send message: "); Serial.print(message); Serial.println("..."); // Формируем JSON-пакет с данными String sJSON = "{\"chat_id\":" + String(chatId) + ",\"parse_mode\":\"HTML\",\"disable_notification\":false,\"text\":\"" + String(message) + "\"}"; Serial.println(sJSON.c_str()); // Настраиваем безопасное подключение к API с размещением временных объектов в куче (heap) WiFiClientSecure *wifiTg = new WiFiClientSecure; // wifiTg->setInsecure(); X509List *certTg = new X509List(TG_API_Root); wifiTg->setTrustAnchors(certTg); // Пробуем подключиться к Telegram API: обратите внимание - API принимает входящие запросы только по HTTPS на 443 порту if (wifiTg->connect("api.telegram.org", 443)) { // Формируем и отправляем POST-запрос для отправки сообщения (сразу целиком, по шаблону) wifiTg->printf(tgPostRequest, tgToken, sJSON.length(), sJSON.c_str()); // Читаем ответ сервера (только первую строку) Serial.print(F("TG :: API responce code: ")); Serial.println(wifiTg->readStringUntil('\n')); // Закрываем соединение wifiTg->stop(); } else { Serial.printf("#ERROR# :: Failed to connect to Telegram API: %d\r\n", wifiTg->getLastSSLError()); }; // Не забываем удалить данные, размещенные в куче delete wifiTg; delete certTg; }
В общем, формирование HTTP-запросов “вручную” не представляет из себя ничего сложного, но можно воспользоваться и готовыми объектами…
Способ 4. Используем класс HTTP Client
Если Вам все-таки лень собирать запрос “вручную”, то можно воспользоваться готовым классом HTTPClient. Он позволяет сформировать и GET и POST запросы, но формат данных все равно придется указать. Кроме того, придется предварительно сформировать URI для отправки данных.
Я не буду детально расписывать происходящее, думаю и там все будет ясно-понятно, а просто приведу всю функцию целиком:
void telegramSendMessage_Post_Http(const char* chatId, char* message) { Serial.print("TG :: Send message: "); Serial.print(message); Serial.println("..."); // Формируем JSON-пакет с данными String sJSON = "{\"chat_id\":" + String(chatId) + ",\"parse_mode\":\"HTML\",\"disable_notification\":false,\"text\":\"" + String(message) + "\"}"; Serial.println(sJSON.c_str()); // Настраиваем безопасное подключение к API WiFiClientSecure wifiTg; HTTPClient tgHttp; // wifiTg.setInsecure(); X509List certTg(TG_API_Root); wifiTg.setTrustAnchors(&certTg); // Формируем динамическую URI String sURI = "https://api.telegram.org/bot" + String(tgToken) + "/sendMessage"; // Пробуем подключиться к Telegram API if (tgHttp.begin(wifiTg, sURI.c_str())) { // Добавляем заголовок с типом данных tgHttp.addHeader("Content-Type", "application/json"); // Формируем и отправляем POST-запрос для отправки сообщения int httpCode = tgHttp.POST(sJSON); // Выводим в лог код ответа сервера (только первую строку) Serial.printf("TG :: API responce code: %d\r\n", httpCode); // Закрываем соединение tgHttp.end(); } else { Serial.printf("#ERROR# :: Failed to connect to Telegram API: %d\r\n", wifiTg.getLastSSLError()); }; }
Сообщение будет также успешно доставлено, но размер скомпилированного bin-файла прошивки будет чуть больше. Да и код функции не стал короче и понятнее… Поэтому это не мой выбор.
Какой способ передачи сообщений использовать – выбирайте сами. Хоть все сразу. Вам и карты в руки. Все эти функции одновременно представлены в репозитории проекта на GitHub: github.com/kotyara12/arduino. PS: Чувствительные данные токена и все пароли намеренно испорчены перед публикацией, поэтому не советую их использовать, а подставить свои.
Скачать файлы проекта можно совершенно бесплатно. Но если Вы вдруг пожелаете меня отблагодарить за труды, то Вы можете это сделать – просто откройте одно или несколько рекламных объявлений на данном сайте и “посмотрите” их пару минут. Этого будет вполне достаточно.
А что, если хочется обратной связи – то есть управлять ESP через Telegram?
Как я уже писал в предыдущей статье, для этого придется использовать другой метод – getUpdates(), затем распарсить полученный JSON, проанализировать команды и обработать их.
Но в этом случае я Вам не советую делать это “самостоятельно” – слишком громоздко и сложно. Советую Вам воспользоваться любой готовой библиотекой, например FastBot. Единственное, что мне в ней не очень нравится – setInsecure(), но это дело не строго обязательное. И я понимаю, почему автору пришлось это сделать – другого выхода у него и не было.
Ещё одна сложность, которая может вас подстерегать – если Вы захотите отправлять команды нескольким устройствам, которые находятся в одной и той же группе, то сделать это будет не очень просто. Но это из-за особенностей самого Telegram, а не библиотеки.
Ну этом позвольте откланяться, с вами был ваш Александр ака kotyara12. До следующих встреч. Ваши вопросы или комментарии вы можете оставить под статьей или в telegram-чате https://t.me/k12chat.
💠 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью: