Вариант бота на Node.js описан в отдельной статье
Немаловажная часть работы администратора Телеграм канала - это конечно же работа с обратной связью. Многое приходит из общения с подписчиками, ведь они и оценивают ваш труд, и подсказывают где у вас недочеты, бывает даже подкинут новую идею. Они всегда будут вам благодарны если вы открыты для общения, и у вас налажена обратная связь, которой удобно и легко пользоваться.
В этой статье я хочу поделиться своим ботом для взаимодействия с подписчиками, который я уже использую. Для начала нам в качестве необходимых параметров нам нужно знать наш личный ID Telegram (через бот @userinfobot) и Token API нашего бота.
* * *
Начинаем работать
Данные от Bot API приходят в формате application/json, поэтому доступа к ним через массив $_POST нет, и брать их будем в "сыром", необработанном виде через 'php://input' используя встроенную функцию PHP file_get_contents().
В работе мы будем использовать кодировку UTF-8, это требование Telegram Bot API. Определим ее в начале скрипта index.php. Создадим класс Bot, для начала добавим ему два приватных свойства и один публичный метод. Очень часто я в коде буду использовать в качестве условного оператора тернарный оператор.
<?php
// определим кодировку UTF-8
header('Content-type: text/html; charset=utf-8');
// Создаем объект класса Bot
$bot = new Bot();
// передаем методу init() данные от BOT API
$bot->init('php://input');
// Класс Bot
class Bot
{
// токен API
private $botToken = "34069ХХХХ:XXXXXXXXXXXX-XXXXXXXXXXXXKVW3qILFpY";
// Ваш ID Telegram
private $adminId = 123456789;
// инициализируем объект класса
public function init($data) {
}
}
?>
Создадим еще один метод, сделаем его приватным т.к. он нужен нам только внутри класса, вызываться из вне как публичный метод
он не будет. Его задача будет преобразовывать через функцию json_decode() преданные ему данные из JSON в ассоциативный массив, назовем его init()
.getData()
<?php
// преобразовываем входящие данные в массив
private function getData($data) {
return json_decode(file_get_contents($data), TRUE);
}
?>
$this->setFileLog($data);
<?php
private function setFileLog($data) {
$fh = fopen('log.txt', 'a') or die('can\'t open file');
((is_array($data)) || (is_object($data))) ? fwrite($fh, print_r($data, TRUE)."\n") : fwrite($fh, $data . "\n");
fclose($fh);
}
?>
Находим наш бот в Telegram и запускаем его. Бот примет нашу команду Strat/Старт, обработает и запишет результат в файл. Теперь мы можем рассмотреть содержимое файла.
Array
(
[update_id] => 22918823
[message] => Array
(
[message_id] => 1
[from] => Array
(
[id] => 123456789
[is_bot] =>
[first_name] => Name
[last_name] => Surname
[language_code] => ru
)
[chat] => Array
(
[id] => 123456789
[first_name] => Name
[last_name] => Surname
[type] => private
)
[date] => 1512554545
[text] => /start
[entities] => Array
(
[0] => Array
(
[offset] => 0
[length] => 6
[type] => bot_command
)
)
)
)
Как видно из содержимого, приходит много служебной информации. Сейчас нас интересует элемент массива
, который тоже является массивом и содержит в себе информацию, с которой предстоит работать. Проверяя наличие нужных нам ключей в массиве, мы можем определить какой тип данных был отправлен боту. Для данного бота важно определить кто написал, ведь от этого зависит куда направлять сообщение. $data['message']
У нас предусмотрено две роли: администратор и пользователь. Есть еще один участник - это сам бот. Мы его будет определять по специальному флагу, но это чуть позже.
* * *
Мои первые ошибки
В начале, когда я делал этого бота, то потратил время на написание лишней логики. Бот принимал сообщения от пользователя проверял тип и направлял его админу как новое сообщение с вложенной кнопкой "Ответить", при нажатии на которую происходила временная запись в файл id пользователя, и бот следующим действием ждал сообщение от админа для ответа пользователю. Получилось много ненужного кода, каких-то проверок и вообще пользоваться было не удобно.
Также если админ начал писать ответ, то написать ему еще одно сообщение, даже другому пользователю было нельзя пока админ не закончит. Еще ограничение было если у пользователя не задан @userName, то просмотреть его профиль было никак. Конечно придумать хаки для достижения полной работоспособности этой логики можно было, но все оказалось намного проще.
После создания этого бота в Телеграм добавлена возможность отключения пользователям пересылки, в этом случае бот не может работать в предложенном варианте. Решение этой проблемы не сложное. Обновление скоро будет на сайте.
Решение пришло из официальной документации
На самом деле нужно было просто внимательнее читать официальную документацию Telegram Bot API. Есть такой метод
- он позволяет переслать сообщение, и не важно, что в нем текст, стикер или медиа, бот просто его перенаправит в тот чат в какой укажете. Этим методом и воспользуемся. Пользователю мы будем просто отвечать на его сообщение, используя для этого доступную функцию "Ответить" в контекстном меню интерфейса приложения Телеграм, специального кода для этого не нужно. forwardMessage
Плюсы от использования:
- Когда сообщение переслано, то в нем видно кто написал, и даже если у пользователя нет @userName мы сможем посмотреть его профиль;
- Когда мы отвечаем на сообщение, то видим в общем списке сообщений на что мы ответили.
* * *
Пишем логику бота обратной связи
В момент старта - бот отправляет нам сообщение с текстовым сообщением
. Нам надо это сообщение поймать и обработать, например, поздороваться с пользователем, дать ему понять, что бот готов к работе. А если стартуем админ, то просто сообщить ему, что бот готов. /start
Давайте напишем несколько вспомогательных методов для нашего класса Bot
<?php
// проверим кто пишет админ или нет
private function isAdmin($id) {
return ($id == $this->adminId) ? true : false;
}
// проверим на начало диалога с ботом
private function isStartBot($data) {
return ($data['message']['text'] == "/start") ? true : false;
}
?>
Настало время поговорить о том, как отправить сообщение через бот. За это отвечает целый набор методов Telegram Bot API, перечислю только те, которые мы будем использовать в рамках этой статьи:
- sendMessage - текстовое сообщение
- sendPhoto - фотография
- sendAudio - аудио-файл
- sendDocument - документ
- sendVideo - видео-файл
- sendSticker - стикер
Telegram Bot API принимает как POST так и GET HTTP-запросы. Мы напишем метод, который будет оправлять POST запрос используя возможности CURL
<?php
// передаем массив данных, id чата, и метод передачи данных
private function requestToTelegram($data, $chat_id, $type) {
$result = null;
// id чата куда отправляем
$data['chat_id'] = $chat_id;
if (is_array($data)) {
// инициализируем CURL
$ch = curl_init();
// укажем url доставки запроса
curl_setopt($ch, CURLOPT_URL, "https://api.telegram.org/bot".$this->botToken.'/'.$type);
// скажем что хотим POST запрос отправить
curl_setopt($ch, CURLOPT_POST, count($data));
// Генерирует URL-кодированную строку запроса
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
// Выполняем CURL
$result = curl_exec($ch);
// закрываем CURL
curl_close($ch);
}
return $result;
}
?>
Добавим еще пару свойств и изменим наш публичный метод
, опишем в нем действия если пришло сообщение о начале диалога с ботом. В добавленных свойствах будет приветственное слово: одно для админа, другое для пользователя. И еще один метод для замены в приветственном сообщении для пользователя {username} на имя пользователя из полученных данных при помощи функции str_replace(), это придаст приветствию персонализацию. init()
<?php
// Приветствие для админа при старте
private $helloAdmin = "Начинаем ждать сообщений от пользователей.";
// Приветствие для пользователя при старте
private $helloUser = "Приветствую Вас {username}.\nЯ очень жду вашего сообщения.\n------\nСпасибо.";
public function init($data) {
// создаем массив из пришедших данных от API Telegram
$arrData = $this->getData($data);
// Определяем id отправителя
$chat_id = $arrData['message']['chat']['id'];
// если это Старт
if($this->isStartBot($arrData)) {
// Определяем id чата куда будем отправлять админу или пользователю
$chat_id = $this->isAdmin($chat_id) ? $this->adminId : $chat_id;
// Выводим приветственное слово
$hello = $is_admin ? $this->helloAdmin : $this->setTextHello($this->helloUser, $arrData);
// Отправляем приветственное сообщение
$this->requestToTelegram(array("text" => $hello), $chat_id, "sendMessage");
} else {
// Если это не старт
}
}
private function setTextHello($text, $data) {
// узнаем имя и фамилию отправителя
$username = $this->getNameUser($data);
// подменяем {username} на Имя и Фамилию
return str_replace("{username}", $username, $text);
}
// получаем name и surname пользователя
private function getNameUser($data) {
return $data['message']['chat']['first_name'] . " " . $data['message']['chat']['last_name'];
}
?>
Теперь давайте опишем действия если это уже очередное сообщение, а не начало диалога с ботом, оно может быть, как от пользователя, так и от админа. Следующий код - это обработка сообщения от пользователя - перенаправление сообщения администратору - здесь все просто: создаем массив с обязательными параметрами для метода
и передадим в наш метод для отправки запроса в API. forwardMessage
<?php
// Если это не старт
if($this->isAdmin($chat_id)) {
// Здесь далее будет код если сообщение отправляет админ
} else {
// Если это написал пользователь то перенаправляем админу
$dataSend = array('from_chat_id' => $chat_id, 'message_id' => $arrData['message']['message_id']);
$this->requestToTelegram($dataSend, $this->adminId, "forwardMessage");
}
?>
Чтобы пользователь получил от бота сообщение, боту нужно сказать в какой чат отправлять, узнаем id чата пользователя из специального параметра
$data['message']['reply_to_message']['forward_from']['id']
Добавим несколько проверок
Исключим ситуацию когда админ может забыть использовать в контекстном меню команду "Ответить", для этого нужно сделать проверку, и в таком случае вывести предупреждение, добавим еще одно свойство в котором будет текст предупреждения, и метод в котором будем проверять наличие ключа reply_to_message с помощью функции array_key_exists(), это ключ скажет нам, что это ответ на чье-то сообщение
<?php
private function isReply($data) {
return array_key_exists('reply_to_message', $data['message']) ? true : false;
}
?>
Обработаем еще одну ситуацию, когда админ по ошибке может начать отвечать на сообщение бота. Согласитесь такое ведь тоже может быть, у меня было )) поэтому и родилась эта проверка. Когда сообщение прислал бот (это может быть предупреждение или приветствие для админа) то это можно определить проверив значение
оно будет равно единице. Вторым условием в проверке исключим ситуацию, когда бот пересылает сообщение пользователя, потому что при ответе на сообщение пользователя, в служебных данных будет присутствовать флаг, определяющий отправителя как бота.['from']['is_bot']
<?php
private function isBot($data) {
return ($data['message']['reply_to_message']['from']['is_bot'] == 1
&& !array_key_exists('forward_from',$data['message']['reply_to_message']));
}
?>
Опишем обработку отправки сообщения пользователю от админа, здесь нам нужно сначала определить, что мы будем отправлять: текст, документ, картинку, стикер, аудио или видео. Для администратора это сделает интерфейс Телеграма, боту же нужно использовать методы Telegram Bot API. У каждого метода API есть свои обязательные параметры. Создадим свой метод, который будет понимать, что отправил админ, заполнит необходимые параметры и предаст их в метод для отправки запроса в Telegram с соответствующей командой.
<?php
private function getTypeCommand($data)
{
// определяем id пользователя для уведомления
$chat_id = $data['message']['reply_to_message']['forward_from']['id'];
// если текст
if (array_key_exists('text', $data['message'])) {
// готовим данные
$dataSend = array(
'text' => $data['message']['text'],
);
// отправляем - передаем нужный метод
$this->requestToTelegram($dataSend, $chat_id, "sendMessage");
} elseif (array_key_exists('sticker', $data['message'])) {
$dataSend = array(
'sticker' => $data['message']['sticker']['file_id'],
);
$this->requestToTelegram($dataSend, $chat_id, "sendSticker");
} elseif (array_key_exists('document', $data['message'])) {
$dataSend = array(
'document' => $data['message']['document']['file_id'],
'caption' => $data['message']['caption'],
);
$this->requestToTelegram($dataSend, $chat_id, "sendDocument");
} elseif (array_key_exists('photo', $data['message'])) {
// картинки Телеграм ресайзит и предлагает разные размеры,
// мы берем самый последний вариант
// так как он самый большой - то есть оригинал
$img_num = count($data['message']['photo']) - 1;
$dataSend = array(
'photo' => $data['message']['photo'][$img_num]['file_id'],
'caption' => $data['message']['caption'],
);
$this->requestToTelegram($dataSend, $chat_id, "sendPhoto");
} elseif (array_key_exists('video', $data['message'])) {
$dataSend = array(
'video' => $data['message']['video']['file_id'],
'caption' => $data['message']['caption'],
);
$this->requestToTelegram($dataSend, $chat_id, "sendVideo");
} else {
// другие тип не поддерживаем
$this->requestToTelegram(array("text" => "Тип передаваемого сообщения не поддерживается"), $chat_id,"sendMessage");
}
}
?>
И заключительная часть - это отправка и реализация проверок сообщений от администратора к пользователю
<?php
if($this->isReply($arrData)) {
// если ответ самому себе
if($this->isAdmin($arrData['message']['reply_to_message']['from']['id'])) {
$this->requestToTelegram(array("text" => "Вы ответили на свое сообщение."), $this->adminId, "sendMessage");
} elseif ($this->isBot($arrData)) {
// если ответ боту
$this->requestToTelegram(array("text" => "Вы ответили на сообщение Бота."), $this->adminId, "sendMessage");
} else {
// все нормально отправляем на обработку
$this->getTypeCommand($arrData);
}
} else {
// нажать кнопку ответить
$this->requestToTelegram(array("text" => $this->answerAdmin), $this->adminId, "sendMessage");
}
?>
* * *
Подводим итоги
У нас получился вполне работоспособный бот для поставленной задачи по осуществлению обратной связи с администратором канала. Его также можно применять и для службы поддержки. Весь исходный код бота вы можете скачать и пользоваться им без каких-либо ограничений на свой страх и риск.
Откройте по ссылке или QR бот @iMakeBot, нажмите кнопку Старт/Start.
Следуйте инструкциям бота.
Спасибо большое за статью!
А как php привязать к боту?
Не совсем правильный вопрос. Бот по сути это приложение - его можно написать с использованием разных языков программирования, в том числе и на php.
Большое спасибо за статью и бота. Подскажите, а как запустить? Я очень слаб во всем этом. Есть опыт работы с ботами через веб хук. фа как тут быть. При обращении к телу бота напрямую из хостинга выдало:
Я что-то пропустил?
Вы как вызываете бота через браузер? Ошибка простая - в теле запроса отсутствует chat_id - то есть Телеграм ругается потому что не знает куда направлять сообщение - в какой чат.
Бота вызвал через прямой путь к нему)
)))) так не получиться.Приложение получает необходимые параметры из направленных ему Телеграмом набором данных через webHook или getUpdates
Фух, запустил, спасибо огромное! Проблемы с вебхук были.
Отлично))
Не могу понять как связывается именно конкретный файл php с конкретным ботом. Откуда Телеграм понимает что если пришло сообщение боту, нужно дернуть именно конкретный файл php?
Нашел
А бот в телеграм просто молчит. Хотя токен вроде верный.
Нужно искать ошибки в логах. Возможно неправильно настроены webHook или SSL сертификат отсутствует.
SSl сертификат есть. На этом сервере уже один бот работает.
Версия php?
7.2
Нужно тогда смотреть логи - какая там информация.
Я бы с радостью посмотрел. Но они не появились. Что странно.
Сложно диагностировать к сожалению не имея возможности дебажить. Попробуйте оставить в точке входа (index.php) прием данных ('php://input') от Телеграм и запишите их в файл.
Если запись идет - значит webHook настроен правильно, тогда продолжайте искать причину дальше - шаг за шагом. например отправьте тестовое сообщение в ответ.
Хорошо спасибо. буду искать
И последний вопрос. Я правильно понимаю, что в боте нужно только 2 строчки отредактировать с user id и бот токен? Я ничего не пропустил?
Да: токен и admin_id
определяем id пользователя для уведомления - не работает
Скорее всего у пользователя закрыта в настройках передавать данные при пересылке сообщений. Бот сообщение вам в чат пересылает но без данных о его аккаунте.
Бот работает, но почему то не создается никакой лог файл
Так он и не должен создавать лог файл
Ответ пользователю не работает если у него в настройках безопасности стоит
Можно это как то исправить?
Исправить можно.
Переписать логику перенаправления и ответа на сообщение.
Это ограничение работает если вы пересылаете сообщение, но если вы сделаете его копию и направите админу, то в текст сообщения можно добавить необходимую техническую информацию, или если использовать систему хранения информации (файлы или субд) то можно хранить техданные по message_id.
И при ответе на сообщение админом считывать необходимую информацию - в том числе и id пользователя.
Работает. только появился сомнение
Сделал пересылку копию сообщения, в тело сообщения вставляется ID пользователя. Если ID пользователя скопировать и отправить боту. то он поймет куда переслать то, что ему отправил админ. Но если нажать именно "ответить" то бот не может считать ID пользователя в тексте сообщения на которое отвечаю. То есть бот не может добраться до цитаты и взять оттуда текст. Может у Вас есть мысли как сделать чтобы нажав на кнопку "ответить" бот смог добраться до содержимого сообщения на которое получает ответ.
https://imakebots.ru/article/bot-obratnoy-svyazi-bez-hostinga-google-apps-script-telegram-bot-api
Посмотрите там есть пример обход блокировки пересылки. Может развернуть бота и на примере посмотреть.
спасибо
Нашел по ссылке ответ - $data['message']['reply_to_message']['text'] это текст сообщения на которое отвечаешь. Бот к нему имеет доступ и там можно забрать ID. у Вас по ссылке это this.bot.data.message.reply_to_message.text
Заменил admin_id и токен, начал выдавать ошибку
Если там вписать id админа, что куда направлять сообщение, то ругается и выплевывает ошибку
И как с этим быть? Заранее благодарю
если в настройках безопасности стоит
то бот отвечает сам себе
Комментарием выше есть решение.
вот это решение?
Да
Я не силен в том чтобы переписывать логику. Спасибо
Пожалуйста исправьте в примере, не работает с закрытым профилем.
Отличный бот, спасибо!
Можно ли сюда добавить автоответ на определенные слова или добавить кнопки в меню, типа Адреса и Часто задаваемые вопросы, а в ответ соответственно заготовка, что не было нужды каждый раз отвечать на одно и тоже.
Если это возможно, будьте добры написать пример кода, спасибо
Спасибо. В этот пример бота можно добавить все, что необходимо - Телеграм АПИ в этом плане гибкий. К сожалению времени на дополнения сейчас нет.
Что-то туплю :(
Что нужно послать в канал, чтобы бот ответил картинкой?
Не создается файл логов. Какие возможные причины?
Как и где смотреть через этот файл и ли через браузер реакцию бота?
Если через браузер, то по какому адресу?
Каким образом осуществляется взаимодействие между сайтом, ботом, телеграмом?
То есть каким образом движется запрос и ответ?
Пользователь отправил текст/нажал кнопку в телеграм боте запрос ушел куда, что и каким образом происходит дальше?
Предполагаю, что через вебхук на адрес апи телеграмма пошел зарпос, правильно?
Для этого чтобы этот сервер апи телеграмма понимал что это запрос от такого юота мы его регистрируем с помощью вебхука?
Как и где посмотреть состояние текущего запроса?
Через лог файл, но он не создается даже при запуске кода из архива к этой статье(токен и админ ид-р разумеется указал).
Или через браузер?
Тогда по какому адресу?
Хостинг использую также timeweb, сертифтикат установил бесплатный.
В предлагаемом архиве для скачивания нет метода для записи в лог файл.
Скопировал из статьи соответвующий метод.
Получал при каждом запросе строку php://input в лог файле
Переделал метод
Вроде заработало, но в лог фал пока добавляется в конец новый запрос, а не перезаписывается, это правильно?!
Правильно ли я внес следующие изменения в скачанный архив.
Заранее спасибо!
Совершенно правильно сделали, что взяли метод логирования из статьи, он в статье как раз для этого и был описан.
При авторизации с помощью iMakeBot в Google Chrome увидел на странице вашего сайта свое имя, а вот в FireFox это не работает. В чем может быть проблема?
Протестил в FF только что - все норм. Не подскажу, так как надо смотреть на вашей стороне. Но там нет ничего, что могло бы конфликтовать. После авторизации идет перезагрузка страницы, и если в браузере есть авторизация то имя выводится из бд в шаблон страницы.
1. Провел эксперимент с FF второй раз - результат положительный ;)
Вижу, что у вас ссылка/кнопка на iMakeBot передает секретный код типа "0d98...57d8", по которому потом происходит в базе идентификация с кукой выставленной на сайте. Если не секрет, какое время жизни этого кода? Оно совпадает с временем куки?
2. В статьях такого рода хотелось бы слышать и несколько слов о безопасности. Например, вебхук вашего бота имеет простой вид, типа https://imakebots.ru/bots/index.php
Эту ссылку могут дёргать не только сервера Telegram, но в принципе кто угодно, при этом отправляя JSON-код с подставными данными (с вытекающими негативными результатами). Ставите защиту от такого?
1. Отлично
Передает не секретный код, а просто сгенерированную одноразовую строку, для индентификации в бд, эта строка не хранится в бд или перегенерируется
2. Про какую статью вы имеете в виду и с чего вы взяли что вебхук моего бота имеет такой вид?
1. Пока посетитель не зашел в бот страница циклически посылает запросы к серверу (проверяя, пришел ли код от бота). Пока этот код (параметр start в "сгенерированной одноразовой строке") не пришел, он должен где-то храниться, например, в БД, в файле или буквально в PHP-сессии.
Я хочу сделать аналогичный механизм, интересует практическое время хранения кода.
2. Я сказал слово НАПРИМЕР. Кстати, в вашей статье "Регистрируем бот у @BotFather, устанавливаем WebHook" так и написано: ...setwebhook?url=https://ВАШ_ДОМЕН/bots/index.php
Многие ведь так и сделают ;)
Сейчас вопрос о вариантах защиты при вызовах данного URL.
Обсуждение не про данную статью, есть на сайте по теме статья и не совсем место это здесь обсуждать. Просьба, откройте ветку на форуме.
1. Код не храниться в бд, алгоритм простой:
Генерируется код, идет запрос AJAX на проверку наличия этого кода в бд. Если его нет значит пользователь не авторизовался.
Пользователь когда проходит по ссылке для авторизации - этот код передается в бот, бот передает на сервер мета данные пользователя с кодом, идет запись в бд
Далее запрос AJAX находит запись с этим кодом в бд, идет 3 процедуры - удаляется проверочный код из бд (null или новое значение тут на выбор), идет авторизация пользователя в системе и перезагружается страница откуда вызов. При загрузке страницы если пользователь авторизован то ему доступны доп возможности.
2. Этой статье уже более 5 лет )) на тот момент я совсем по-другому работал - сам так делал.
Для проверки вы можете использовать один из предложенных вариантов самого Телеграм:
If you'd like to make sure that the webhook was set by you, you can specify secret data in the parameter secret_token. If specified, the request will contain a header “X-Telegram-Bot-Api-Secret-Token” with the secret token as content.
https://core.telegram.org/bots/api#setwebhook
“X-Telegram-Bot-Api-Secret-Token” - классная штука. Я недавно начал писать боты и как-то не заметил этого параметра в документации. Проверил, работает. Спасибо за совет, теперь я спокоен! ;)
Можно ли добавить несколько админов?