Простой роутер для бота на PHP

Роутер необходим для обработки запроса пользователя в бот. Он позволяет направить данные от Телеграм в нужный метод сценария. Этот пример для приема объектов типа message и callback_query.

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

От Телеграм "прилетают" обновления (update) с разным набором свойств. Можно с легкостью это все дело ловить и направлять в нужное место с нужными данными.

Для определения чем обрабатывать пришедшие данные нам нужен роутер. Роутер должен быть в любом приложении, где это требуется, на этом сайте он также присутствует. Телеграм бот не исключение:

  1. Поймать Старт бота
  2. Отловить нажатие по inline-кнопке
  3. Определить inline запрос
  4. Вывести информацию при нажатии по клавиатуре
  5. Направить на сохранение веденных данных от пользователя
  6. ...

Для быстрых ботов с малым функционалом, могу предложить вариант роутера, который решает поставленные перед ним задачи.

Для начала посмотрим какие данные приходят от Телеграм

Update от Телеграм не может сразу иметь более одного типа объекта.

Вот пример update при старте бота:

{
  update_id: 123456789000,
  message: {
    message_id: 1,
    from: {
      id: 123456,
      is_bot: false,
      first_name: '—',
      last_name: '—',
      language_code: 'ru'
    },
    chat: { id: 123456, first_name: '—', last_name: '—', type: 'private' },
    date: 1666004497,
    text: '/start',
    entities: [{"offset":0,"length":6,"type":"bot_command"}]
  }
}

Если отправлена команда Start или любое другое текстовое сообщение в том числе и команда от клавиатуры - то это объект Message со свойством text (пример выше).

Если это медиа сообщение, то это также объект Message с соответствующими свойствами:

  1. photo - картинка
  2. video - видео-файл
  3. audio - аудио-файл
  4. document - файл любого формата, в том числе и photo, video или audio, но направленные в виде файла без визуализации
  5. video_note - видео-заметка
  6. voice - аудио-заметка
  7. location - геолокация
  8. sticker - стикер
  9. animation - анимация (gif)

В этих объектах нам будет интересны свойства:

  1. message->from
  2. message->[text | photo | video | audio | document | video_note | voice | location | sticker | animation]
  3. message->[entities | caption_entities]

Если мы работаем с приватным типом chat, то message->chat не всегда пригодиться, только если для проверки типа чата.

message->from - в этом объекте лежат данные пользователя, в частности его Телеграм id, можно еще использовать другие данные объекта, например для приветствия пользователя по его имени.

message->[text | photo | video | audio | document | video_note | voice | location | sticker | animation] - направленные пользователем данные

message->[entities | caption_entities] - форматирование текстового содержания сообщения

* * *

При нажатии на inline-кнопку приходит уже другой тип объекта: CallBack_Query 

{
  update_id: 123456789001,
  callback_query: {
    id: '00001222',
    from: {
      id: 123456,
      is_bot: false,
      first_name: '—',
      last_name: '—',
      language_code: 'ru'
    },
    message: {
      message_id: 11,
      from: {
        id: 123456,
        is_bot: false,
        first_name: '—',
        last_name: '—',
        language_code: 'ru'
      },
      chat: { id: 123456, first_name: '—', last_name: '—', type: 'private' },
      date: 1665488669,
      text: 'Текст сообщения',
      reply_markup: {"inline_keyboard":[[{"text":"Test Button","callback_data":"inline_click"}]]}
    },
    chat_instance: '040495084748393',
    data: 'inline_click'
  }
}

Из этого объекта нам практически всегда потребуются несколько свойств:

  1. callback_query->id - id запроса
  2. callback_query->from - объект пользователя
  3. callback_query->message->message_id - id сообщения по кнопке которого было событие click
  4. callback_query->data - здесь зашитые данные кнопки, очень важная часть данных

  

ПРИМЕЧАНИЕ. После того, как пользователь нажмет кнопку обратного вызова, клиенты Telegram будут отображать индикатор выполнения, пока вы не вызовете answerCallbackQuery . Следовательно, необходимо отреагировать, вызвав answerCallbackQuery , даже если уведомление пользователю не требуется (например, без указания каких-либо необязательных параметров).

Из документации Телеграм [https://core.telegram.org/bots/api]

callback_query->id - это свойство нам необходимо для уведомления Телеграм через метод answerCallbackQuery.

Когда мы не уведомим Телеграм, что запрос пришел по месту назначения и обработан, то он будет засылать повторно один и тот же запрос (update) в соответсвии со своей политикой.

Также это происходит когда в момент обработки сервер ответил ошибкой.

По моему опыту повторы идут через: 1 сек, 5 сек, 20 сек, 1 мин, 2 мин, 5 мин ... и т.д. в течении дня.

Повторные update от Телеграм чреваты! Например если у вас нажатие по кнопке должно быть пополнение баланса на определенную сумму, и если у вас в коде будет ошибка до выполнения уведомления или вообще отсутствие уведомления, то нажав один раз пользователь может не по своей вине пополнять свой баланс "за кадром" в течении дня, он будет рад, но для вас это будет проблемой. 

callback_query->from - в этом объекте лежат данные пользователя, в частности его Телеграм id.

callback_query->message->message_id - нам может пригодиться, например для удаления сообщения, чтобы закрепленные inline-кнопки не были доступны для повторного нажатия.

callback_query->data - самое важное свойство, в нем лежат параметры для выполнения задуманной логики

Строим простой роутер на php 

Телеграм направляет на установленный webHook POST запрос с update, нам необходимо получить эти данные, так как они в формате JSON, мы может спокойно их перевести или в объект stdClass() или в ассоциативный массив используя функцию json_decode.

Данные мы получим в сыром виде из потока php://input

// получаем данные от телеграм - преобразуем в объект stdClass
$data = json_decode(file_get_contents("php://input"));

Так как $data это объект, то обращаться к его свойствам мы можем используя конструкцию ->

// получим update_id
$update_id = $data->update_id;

Проверить существование свойства у объекта можно используя функцию isset() 

// проверим существует ли свойство update_id
$isSetUpdateId = isset($data->update_id);

Или через функцию property_exists()

// проверим существует ли свойство update_id
$isPropertyExists = property_exists($data, "update_id");

Оба варианта вернут bool значение, с разницей при значении у свойства null если оно существует - property_exists() вернет true

$data->update_id = null;

return property_exists($data, "update_id");    // true
return isset($data->update_id);                // false

Теперь с умением проверять наличие свойств у объекта можно смело начинать строить роутер.

/**
 * Простой роутер бота
 */
if (isset($data->message)) {
    // получим id чата
    $chat_id = $data->message->from->id;
    // проверим что это текстовое сообщение
    if (isset($data->message->text)) {
        // проверим что это старт бота
        if ($data->message->text == "/start") {
            // направим запрос в метод старта бота
            startBot($chat_id);
        }
    } elseif(isset($data->message->photo)) {
        // направим на сохранение photo
        savePhoto($chat_id, $data->message->photo);
    }
// если это нажатие по кнопке
} elseif (isset($data->callback_query)) {
    // получим id чата
    $chat_id = $data->callback_query->from->id;
    // получим callBackQuery_id
    $cbq_id = $data->callback_query->id;
    // получим переданное значение в кнопке
    $c_data = $data->callback_query->data;
    // спарсим значения
    $params = explode("_", $c_data);
    /**
     В этом месте мы в зависимости от заложенных параметров по принципу
     param1_param2_param3_param4 и т.д.
     можем направлять запрос в нужный метод
    */
    if($params[1] === "getPrice") {
        price($chat_id, $cbq_id, $params);
    } elseif($params[1] === "getDemo") {
        demo($chat_id, $cbq_id, $params);
    }
}
6 комментариев
Авторизуйтесь через Telegram, чтобы оставить комментарий.
Откройте по ссылке или QR бот @iMakeBot, нажмите кнопку Старт/Start.
Следуйте инструкциям бота.

  • Sergey Michaylovich [1 год назад]

    Здравствуйте!
    Спасибо за статью!
    Подскажите, пожалуйста, на следующие вопросы/моменты.

    ?1.
    При получении "пакета данных" кодом бота он понимает что это бот направили или пользователь?
    Как я понимаю, что идет дифференциация по полю message[from][is_bot], верно или нет?

    ?2.
    Каким образом разделить сообщение пришло:
     от пользователя в бот 
    от бота пользователю
    от админа в бот
    от бота админу бота
    от админа пользователю
    от пользователя к админу.

    ?3
    Может ли быть пользователи бота с разными ролями?
    И как их учитывать тогда? Например, если бот это какая-то группа в которую поступают сообщения и их надо модерировать, чем занимается группа админов/модераторов.
    То как  это реализовать? 
    Надо узнать ид ид-ры и прописать в массив - свойство экземпляра бота?


  • iMakeBots [1 год назад → Sergey Michaylovich]

    1. Насколько мне подсказывает опыт, то бот в боту писать не может. 

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

    3. Пользователи с разными ролями конечно могут быть, для этого необходимо боту сообщить их id и роли заранее. Как это реализовывать зависит от ситуации и сценария необходимого вам.

  • Sergey Michaylovich [1 год назад]

    Также вот такой вопрос а как организовать сценарий работы бота? например, если бот это тест-опросник-анкета?
    То есть как сценарий работы бота формализовать в виде кода?
    В виде ассоциативного массива правил/сцен или есть какие-то примеры других вариантов?
    Если да, то как и где это правильно сделать?

  • iMakeBots [1 год назад → Sergey Michaylovich]

    За пару строк не объяснить, готового варианта нет, можете посмотреть моего бота-опросника

  • Andrew° [10 месяцев назад]

    Приветствую тебя, величайший! Очень нужен твой совет по одному моменту, можешь подсказать, пожалуйста.

    Как мне фиксировать этапы входящий сообщений?

    Например:

    Есть keyboard кнопки: [Отправить контакт], [Изменить имя], [Обратная связь]

    Во всех случаях при клике на любую кнопку, юзер должен вписать текст (за исключением отправки контакта, но там так же может быть вписан номер ручками), который на пхп будет обработан соответственно.

    Вот я не как не могу уловить, как мне сделать фильтрацию входящий данных в зависимости от выбора пунктов.

    Я сделал так (возможно бред), при старте бота, я записываю юзера в базу, у него есть в базе поле "step" в которой сразу по умолчанию стоит main допустим, т.е. главное меню якобы.

    Далее если юзер кликает на keyboard [Изменить имя], то я меняю в базе "step" на  "change_name", и уже через условие фильтрую входящее сообщение с проверкой на каком шаге (step) находится юзер и обрабатываю соответствующим образом.

    Пожалуйста, дай совет, верный ли подход?

  • iMakeBots [10 месяцев назад → Andrew°]

    Подход верный, у меня примерно также