Привет, HABR! В этой статье постараюсь показать маленький пример, использования клавиатуры под сообщением в Telegram. То есть мы с помощью бота будем отправлять пост в наш канал с использованием языка программирования PHP. Telegram BOT API неплохо документирован, но всё же остаются вопросы у новичков, как это всё собрать чтобы работало.

InlineKeyboardMarkup — этот объект представляет встроенную клавиатуру, которая появляется под соответствующим сообщением.

Что мы хотим сделать?


При публикации сообщение с использованием бота под сами сообщением вывести 5 кнопок (Нравиться, Ха-ха, Ух ты!, Сочувствую, Возмутительно) в виде смайликов. И цель их отображать количество людей, которые нажали на определённую кнопку.

В сети уже много готовых библиотек на PHP по работе с Telegram BOT API. Для нашей задачи они не понадобятся. Итак приступаем.

Нам понадобится


  1. Создать telegram канал;
  2. Создать бота;
  3. Добавить бота в telegram канал;
  4. Хостинг на котором будет лежать файл обработки логики кнопок;
  5. База данных в которой будем хранить id пользователей, которые нажал на кнопку.

Логика


При нажатии на определённую кнопку добавить счётчик в неё. Пользователь может нажать только на одну из 5-кнопок. В БД мы будем хранить ID сообщения и ID пользователей, которые уже отреагировали на сообщение.

Структура файлов


  1. send.php — отправляем сообщение в Telegram канал
  2. callback.php — обрабатываем клики кнопок получены из Telegram канала

Содержание файла send.php


<?php
   define( 'TOKEN', '<token>' );
   define( 'CHAT_ID', '<chat_id>' ); // @name_chat
   define( 'API_URL', 'https://api.telegram.org/bot' . TOKEN . '/' );
   
   function request($method, $params = array()) {
      if ( (!empty($params) ) {
         $url = API_URL . $method . "?" . http_build_query($params);
      } else {
         $url = API_URL . $method;
      }

      return json_decode(file_get_contents($url), JSON_OBJECT_AS_ARRAY);
   }

   $keyboard = array(
      array(
         array('text'=>':like:','callback_data'=>'{"action":"like","count":0,"text":":like:"}'),
         array('text'=>':joy:','callback_data'=>'{"action":"joy","count":0,"text":":joy:"}'),
         array('text'=>':hushed:','callback_data'=>'{"action":"hushed","count":0,"text":":hushed:"}'),
         array('text'=>':cry:','callback_data'=>'{"action":"cry","count":0,"text":":cry:"}'),
         array('text'=>':rage:','callback_data'=>'{"action":"rage","count":0,"text":":rage:"}')
      )
   );

   request("sendMessage", array(
      'chat_id' => CHAT_ID,
      'text' => "hello world!",
      'disable_web_page_preview' => false,
      'reply_markup' => json_encode(array('inline_keyboard' => $keyboard))
   ));

Как видим по коду он очень просто и мы тут просто отправляем «Hello World!» сообщение и к нему добавляем 5 кнопок.

Нам также нужно установить Webhook для бота. Это нужно для того, чтобы указать боту куда (http://site.ru/callback.php) отправлять результат обработки кнопок. Это очень просто сделать, в браузер соберите ссылку такого формата:

https://api.telegram.org/bot{my_bot_token}/setWebhook?url={url_to_send_updates_to}

В ответ получите json примерно такого содержания:

{"ok":true,"result":true,"description":"Webhook was set"}

Содержание файла callback.php


<?php
   define( 'TOKEN', '<token>' );
   define( 'CHAT_ID', '<chat_id>' ); // @name_chat
   define( 'API_URL', 'https://api.telegram.org/bot' . TOKEN . '/' );

   function request($method, $params = array()) {
      if ( (!empty($params) ) {
         $url = API_URL . $method . "?" . http_build_query($params);
      } else {
         $url = API_URL . $method;
      }

      return json_decode(file_get_contents($url), JSON_OBJECT_AS_ARRAY);
   }

   function editMessageReplyMarkup($params){
      //В этом цикле мы изменяем сами кнопки, а именно текст кнопки и значение параметра callback_data
      foreach ( $params['inline_keyboard'][0] as $key => $value ) {
         $data_for = json_decode($value->callback_data, true); // изначально у нас callback_data храниться в виде json-а, декатируем в массив
         if ( $params['data']['action'] == $data_for['action'] ) { // определяем, на какую именно кнопку нажал пользователь под сообщением
            $data_for['count']++; //плюсуем единичку
            $value->text = $data_for['text'] . " " . $data_for['count']; // Изменяем текст кнопки смайлик + количество лайков
         }
         $value->callback_data = json_encode($data_for); // callback_data кнопки кодируем в json
         $params['inline_keyboard'][0][$key] = (array)$value; // изменяем кнопку на новую
      }

      //Изменяем кнопки к сообщению
      request("editMessageReplyMarkup", array(
         'chat_id' => CHAT_ID,
         'message_id' => $params['message_id'],
         'reply_markup' => json_encode(array('inline_keyboard' => $params['inline_keyboard'])),
      ));

      //Выводим сообщение в чат
      request("answerCallbackQuery", array(
         'callback_query_id' => $params['callback_query_id'],
         'text' => "Спасибо! Вы поставили " . $params['data']['text'],
      ));
}

   $result = json_decode(file_get_contents('php://input')); // получаем результат нажатия кнопки
   $inline_keyboard = $result->callback_query->message->reply_markup->inline_keyboard; // текущее состояние кнопок при нажатии на одну из 5 кнопок
   $data = json_decode($result->callback_query->data, true); // получаем значение с кнопки, а именно с параметра callback_data нажатой кнопки
   $message_id = $result->callback_query->message->message_id; // ID сообщения в чате
   $callback_query_id = $result->callback_query->id; //ID полученного результата
   $user_id = $result->callback_query->from->id; // ID пользователя

   $db_message = $db->super_query("SELECT * FROM bot_like WHERE message_id={$message_id}"); //Ищем в БД ID сообщения

   /*
   Я использую библиотеку ($db = new db;) от CMS DLE для работы с БД.
   super_query - этот метод возвращает первую найденную запись в виде массива
   */

   if ( $db_message === null ) {
      // Если не нашли в БД ID сообщения, записываем в БД текущий ID сообщения и ID пользователя, который отреагировал (нажал на одну из 5 кнопок) на сообщение. 
      $db->query("INSERT INTO " . PREFIX . "_posting_tg_bot_like (message_id, users) VALUES ('{$message_id}', '{$user_id}')");

      editMessageReplyMarkup(array(
         'inline_keyboard' => $inline_keyboard,
         'data' => $data,
         'message_id' => $message_id,
         'callback_query_id' => $callback_query_id
      ));
   } else {
      //Если в БД нашли сообщение
      $users = explode(",", $db_message['users']);
      
      // Если нашли в БД пользователя, выводим сообщение "Вы уже нажали на одну из 5 кнопок"
      if( in_array($user_id, $users) ) {
         request("answerCallbackQuery", array(
            'callback_query_id' => $callback_query_id,
            'text' => "Вы уже отреагировали на новость",
         ));
      } 

      // Если не нашли ID в БД изменяем одну из 5 кнопок и добавляем ID пользователя
      else {
         editMessageReplyMarkup(array(
            'inline_keyboard' => $inline_keyboard,
            'data' => $data,
            'message_id' => $message_id,
            'callback_query_id' => $callback_query_id
         ));

         array_push($users, $user_id);
         $users = implode(',', $users);
         $db->query("UPDATE bot_like SET users='{$users}' WHERE message_id='{$message_id}'");
    }
}

Вот и всё, такой маленький пример получился, заранее извините, если код написан безобразно. Можно расширить этот код до нужной функциональности, это уже зависит от вашей задачи. Моя задача была показать маленький пример, и я надеюсь у меня это получилось. Спасибо за внимание, встретимся в комментариях.

Комментарии (1)


  1. FanatPHP
    25.07.2022 16:45
    +2

    Код действительно получился так себе. Несколько замечаний, если позволите.


    Вы не совсем правильно понимаете смысл константы JSON_OBJECT_AS_ARRAY. Во второй аргумент этой функции надо передавать булево значение, в данном случае — true, а не константу.


    Если у вас нет глобального обработчика ошибок, то результат file_get_contents() желательно проверить вручную, и остановить выполнение. Иначе при ошибке запроса в логи будет сыпаться миллион совершенно ненужных сопутствующих ошибок.


    Форматирование кода — это не мелочь. Очень трудно бывает понять, "кто на ком стоял". Скобки скачут в разные стороны, чтобы понять подчиненность операторов, мне пришлось скопировать код в редактор, который автоматически форматирует код.


    Хранение данных через запятую — это совершенно детсадовская, но при этом очень грубая ошибка, которая показывает, увы, полное незнание основ реляционных баз данных. Перечисления должны храниться отдельно, каждое в своей ячейке. И, соответственно, вот это вот if( in_array($user_id, $users) ) — это испанский стыд. БД придумана для того, чтобы искать нужные нам данные. А не возвращать гору мусора, в которой потом надо будет копаться вручную. Запрос должен сразу запрашивать нужное значение, т.е.


    SELECT * FROM bot_like WHERE message_id=? AND user_id=?

    Соответственно, логика приложения сразу станет значительно проще, безо всех этих вложенных if и повторений повторений:


    // Если не нашли в БД ID сообщения, записываем в БД текущий ID сообщения и ID
    // пользователя, который отреагировал (нажал на одну из 5 кнопок) на сообщение. 
    if ( $db_message === null ) {
        $sql = "INSERT INTO bot_like (message_id, user_id) VALUES (?,?)";       
        $db->query($sql, [$message_id, $user_id]);
    
        editMessageReplyMarkup(array(
            'inline_keyboard' => $inline_keyboard,
            'data' => $data,
            'message_id' => $message_id,
            'callback_query_id' => $callback_query_id
        ));
    //Если нашли
    } else {
        request("answerCallbackQuery", array(
            'callback_query_id' => $callback_query_id,
            'text' => "Вы уже отреагировали на новость",
        ));
    }

    Ну и последнее. Знаки вопроса в SQL запросах выше не просто так. Они называются плейсхолдерами или параметрами. И 2022 году запрос к БД из РНР должен выглядеть именно так (ну или, для педантов, может как :mid, :uid). И здесь мы переходим к следующему пункту:


    $db->query() и $db->super_query() — это, извините, позор.


    Я понимаю, что вам, возможно, привычна эта библиотека. Но всё-таки, надо хотя бы немного интересоваться такими вещами, как безопасность кода вообще, и защита от SQL инъекций в частности. Надо забыть про эту "библиотеку от CMS DLE" и научиться использовать что-то более безопасное, хотя бы PDO.


    Сразу скажу, не стоит оправдываться такими вещами, как "это внутренние идентификаторы Телеграма, а в них инъекции быть не может" — это, извините, детский лепет. По двум причинам:


    • понятно, что и в других местах вы пишете то же самое. Частный случай — вообще ни разу не оправдание. Безопасность работает только тогда, когда правила соблюдаются неукоснительно, а не по настроению
    • мы сейчас говорим о статье, которая размещена на публичном ресурсе. То есть ваш случай автоматически перестаёт быть частным. Лично у вас в этом месте — да, возможно, инъекции не будет. Но вы этот код написали сейчас не для себя, а для других. И у того, кто возьмёт ваш код и будет его адаптировать подл себя — будет инъекция, и в полный рост. Этот момент очень многие не понимают, будучи преисполнены заслуженной гордости — "я сделал доброе дело, а меня критикуют за какие-то мелочи". Проблема в том, что дело должно быть добрым целиком, а не по принципу — здесь лечим, а тут калечим.

    В целом, я совсем не против статей даже на такие тривиальные темы, на которые есть сотня готовых ответов на Тостере. Статьи нужны для любого уровня. Но к таким статьям должны тем более применяться более строгие требования, потому что иначе нет смысла её размещать — плохого кода в интернете и так навалом.