Делаем бот для обратной связи в Telegram

Вариант бота на 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) {

            }
        }
?>


Создадим еще один метод, сделаем его приватным т.к. он нужен нам только внутри класса, вызываться из вне как публичный метод init() он не будет. Его задача будет преобразовывать через функцию json_decode() преданные ему данные из JSON в ассоциативный массив, назовем его getData().

<?php
    // преобразовываем входящие данные в массив
    private function getData($data) {
        return json_decode(file_get_contents($data), TRUE);
    }
?>


Для отладки можно создать метод, который будет записывать в файл все результаты работы метода getData(). Это очень удобно, ведь можно посмотреть какую информацию отправляет нам Bot API. На входе он принимает результат метода getData(). Вызывать метод можно вот так: $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 - он позволяет переслать сообщение, и не важно, что в нем текст, стикер или медиа, бот просто его перенаправит в тот чат в какой укажете. Этим методом и воспользуемся. Пользователю мы будем просто отвечать на его сообщение, используя для этого доступную функцию "Ответить" в контекстном меню интерфейса приложения Телеграм, специального кода для этого не нужно. 

Плюсы от использования:

  1. когда сообщение переслано то в нем видно кто написал, и даже если у пользователя нет @userName мы сможем посмотреть его профиль;
  2. когда мы отвечаем на сообщение, то видим в общем списке сообщений на что мы ответили.
     


Сторона пользователя


Отвечаем на сообщение пользователя


Сторона администратора


Пишем логику бота обратной связи

В момент старта - бот отправляет нам сообщение с текстовым сообщением /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, перечислю только те, которые мы будем использовать в рамках этой статьи:

  1. sendMessage - текстовое сообщение
  2. sendPhoto - фотография
  3. sendAudio - аудио-файл
  4. sendDocument - документ
  5. sendVideo - видео-файл
  6. 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;
    }
?>


Добавим еще пару свойств и изменим наш публичный метод init(), опишем в нем действия если пришло сообщение о начале диалога с ботом. В добавленных свойствах будет приветственное слово: одно для админа, другое для пользователя. И еще один метод для замены в приветственном сообщении для пользователя {username} на имя пользователя из полученных данных при помощи функции str_replace(), это придаст приветствию персонализацию. 

<?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'];
    }
?>


Теперь давайте опишем действия если это уже очередное сообщение, а не начало диалога с ботом, оно может быть, как от пользователя, так и от админа. Следующий код - это обработка сообщения от пользователя - перенаправление сообщения администратору - здесь все просто: создаем массив с обязательными параметрами для метода forwardMessage и передадим в наш метод для отправки запроса в API. 

<?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");
    }
?>

Подводим итоги

У нас получился вполне работоспособный бот для поставленной задачи по осуществлению обратной связи с администратором канала. Его также можно применять и для службы поддержки. Весь исходный код бота вы можете скачать и пользоваться им без каких-либо ограничений на свой страх и риск. 


Комментарии

Авторизуйтесь через Telegram, чтобы оставить комментарий.
Откройте бот @SiteAuthBot, нажмите кнопку Старт/Start. Следуйте инструкциям бота.


  • Спасибо большое за статью!
    Vyacheslav Koval 24.06.2018 в 17:12
  • А как php привязать к боту?
    nun 28.06.2019 в 18:18
    • Не совсем правильный вопрос. Бот по сути это приложение - его можно написать с использованием разных языков программирования, в том числе и на php.
      iMakeBots 28.06.2019 в 18:23
  • Большое спасибо за статью и бота. Подскажите, а как запустить? Я очень слаб во всем этом. Есть опыт работы с ботами через веб хук. фа как тут быть. При обращении к телу бота напрямую из хостинга выдало {"ok":false,"error_code":400,"description":"Bad Request: chat_id is empty"} Я что-то пропустил?
    Arteater ✏️ 12.09.2019 в 11:28
    • Вы как вызываете бота через браузер? Ошибка простая - в теле запроса отсутствует chat_id - то есть Телеграм ругается потому что не знает куда направлять сообщение - в какой чат.
      iMakeBots 12.09.2019 в 11:43
      • Бота вызвал через прямой путь к нему)
        Arteater ✏️ 12.09.2019 в 11:46
        • )))) так не получиться.
          Приложение получает необходимые параметры из направленных ему Телеграмом набором данных через webHook или getUpdates
          iMakeBots 12.09.2019 в 11:48
          • Фух, запустил, спасибо огромное! Проблемы с вебхук были.
            Arteater ✏️ 12.09.2019 в 13:42
            • Отлично))
              iMakeBots 12.09.2019 в 17:32
          • Не могу понять как связывается именно конкретный файл php с конкретным ботом. Откуда Телеграм понимает что если пришло сообщение боту, нужно дернуть именно конкретный файл php?
            Vladimir Belyaev 22.10.2020 в 07:37
            • Нашел
              https://api.telegram.org/botYOUR-TOKEN/setWebhook?url=https://<YOURWEBSITE>/<YOUR_PHP_URL>
              Vladimir Belyaev 22.10.2020 в 08:11
      • А бот в телеграм просто молчит. Хотя токен вроде верный.
        Arteater ✏️ 12.09.2019 в 11:48
        • Нужно искать ошибки в логах.
          Возможно неправильно настроены webHook или SSL сертификат отсутствует.
          iMakeBots 12.09.2019 в 11:50
          • SSl сертификат есть. На этом сервере уже один бот работает.
            Arteater ✏️ 12.09.2019 в 11:54
            • Версия php?
              iMakeBots 12.09.2019 в 11:55
          • 7.2
            Arteater ✏️ 12.09.2019 в 11:56
            • Нужно тогда смотреть логи - какая там информация.
              iMakeBots 12.09.2019 в 11:58
          • Я бы с радостью посмотрел. Но они не появились. Что странно.
            Arteater ✏️ 12.09.2019 в 12:00
            • Сложно диагностировать к сожалению не имея возможности дебажить.
              Попробуйте оставить в точке входа (index.php) прием данных ('php://input') от Телеграм и запишите их в файл.

              Если запись идет - значит webHook настроен правильно, тогда продолжайте искать причину дальше - шаг за шагом. например отправьте тестовое сообщение в ответ.
              iMakeBots 12.09.2019 в 12:04
            • Хорошо спасибо. буду искать
              Arteater ✏️ 12.09.2019 в 12:16
          • И последний вопрос. Я правильно понимаю, что в боте нужно только 2 строчки отредактировать с user id и бот токен? Я ничего не пропустил?
            Arteater ✏️ 12.09.2019 в 12:20
          • admin ID *быстрофикс
            Arteater ✏️ 12.09.2019 в 12:21
            • Да: токен и admin_id
              iMakeBots 12.09.2019 в 17:32
  • определяем id пользователя для уведомления - не работаеть
    Dilshod Abdujalilov 21.01.2020 в 11:34
    • Скорее всего у пользователя закрыта в настройках передавать данные при пересылке сообщений. Бот сообщение вам в чат пересылает но без данных о его аккаунте.
      iMakeBots 21.01.2020 в 11:50
  • Бот работает, но почему то не создается никакой лог файл
    Psycho 29.04.2020 в 02:39
    • Так он и не должен создавать лог файл
      Freddy Krueger 06.08.2020 в 07:13
  • Ответ пользователю не работает если у него в настройках безопасности стоит Пересылка сообщений: Никто. Можно это как то исправить?
    Админ 18.09.2020 в 05:47
    • Исправить можно.
      Переписать логику перенаправления и ответа на сообщение.

      Это ограничение работает если вы пересылаете сообщение, но если вы сделаете его копию и направите админу, то в текст сообщения можно добавить необходимую техническую информацию, или если использовать систему хранения информации (файлы или субд) то можно хранить техданные по message_id.
      И при ответе на сообщение админом считывать необходимую информацию - в том числе и id пользователя.
      iMakeBots 18.09.2020 в 16:04