Добавить в botman editMessageText для Telegram

В Telegram bot API предусмотрен метод для изменения отправленного сообщения, и для изменения кнопок. К сожалению в текущей версии botman (telegram driver) нет встроенных функций для реализации этой задачи, поэтому пришлось дописать самому.

В чем проблема?

Не получается отправить запрос на изменение кнопок и или текста сообщения с помощью стандартного метода ask(). А использовать хочется именно его.

Это происходит потому, что мы формируем сообщение (текст и кнопки) классом BotMan\BotMan\Messages\Outgoing\Question, и передаем его в ask(). Дальше по цепочке вызывается метод reply(), за ним buildServicePayload() класса BotMan\Drivers\Telegram\TelegramDriver. В buildServicePayload() выявляется каким будет метод API, и условие построено таким образом, что при ..\Question ставится метод sendMessage. И все. А для изменения сообщения и или кнопок нужно использовать метод editMessageText.

Поэтому либо нужно делать аналог метода ask, со всей цепочкой вызова, либо внедряться в существующую систему. Я выбрал второй вариант, хотя изящество этого решения меня вовсе не впечатляет..

В этой заметке вы узнаете

  • как использовать методы API Telegram editMessageReplyMarkup, editMessageText в Botman
  • как создавать inline_keyboard с несколькими кнопками на строке, и использовать это в связке с методом ask()

Задача в том, чтобы иметь возможность вызывать $this->ask и не отправлять новое сообщение с кнопками и текстом, а менять уже отправленное. То есть сделать удобную навигацию по меню бота.

Можно отправить: новый текст / новые кнопки / новые кнопки и новый текст.

Надстройка, которую я предлагаю, интегрируется в vendor\botman\driver-telegram\src\TelegramDriver.php. Там нужно добавить дополнительные условия в методе buildServicePayload. Но сперва создадим 2 новых класса и 1 тестовый диалог.

Нюанс

Если пользователь отправил сообщение, то уже не получится изменить кнопки. И честно говоря для меня осталось загадкой почему. Ведь если кликать по этим кнопкам, то они отвечают. Но если, например, попросить пользователя либо выбрать из предлагаемых вариантов, либо ввести вручную ответ, то никак не получилось у меня добиться, чтобы менялся текст сообщения или кнопки именно после пользовательского ввода. И видимо так действительно нельзя сделать. По крайней мере в (весьма популярном и навороченном) боте магазина ВкусВилл так тоже не работает. Если совершен пользовательский ввод, то нужно отправлять новое сообщение, а не менять предыдущее. Однако в целом это лично — если мы бы меняли сообщения после пользовательского ввода, то рискнули бы оказаться в ситуации, когда наше изменяемое сообщение уже где-то скрылось в истории, а пользователь все продолжает вводить команды..

Решение задачи

Настройка кофнига, чтобы не пропадали кнопки

В файле config\botman\telegram.php

'hideInlineKeyboard' => false

Класс для отправки сообщений

В Botman есть класс для отправки сообщений - это Question. Но я создаю свой класс чтобы иметь возможность добавлять по несколько кнопок на строку, и для единообразия кода..

namespace App\Services;

class TelegramQuestionMessage{

  protected $text = "";
  protected $keyboard = ['reply_markup' => ""];
  protected $message_id = "";

  public function __construct($text = "") {
    $this->text = $text;
  }

  public static function create($text = "") {
    return new static($text);
  }

  public function setKeyboard($keyboard) {
    $this->keyboard = $keyboard;
    return $this;
  }

  public function getReplyMarkup() {
    return $this->keyboard['reply_markup'];
  }

  public function getText() {
    return $this->text;
  }
}

Класс для редактирования сообщений

namespace App\Services;

class TelegramQuestionEditMessage extends TelegramQuestionMessage {
  public function setMessageId($message_id) {
    $this->message_id = $message_id;
    return $this;
  }
  public function getMessageId() {
    return $this->message_id;
  }
}

Тестовый диалог (Conversation)

Добавить route в routes\botman.php

$botman->hears('/demo',  function($bot){
  $bot->startConversation(new \App\Conversations\DemoEditConversation());
});

Создать диалог

php artisan botman:make:conversation DemoEditConversation

Поместить код в файл

Пример достаточно расширенный, включающий разные варианты развития событий.

namespace App\Conversations;

use BotMan\BotMan\Messages\Conversations\Conversation;
use BotMan\BotMan\Messages\Incoming\Answer;
use BotMan\BotMan\Messages\Outgoing\Question;
use BotMan\BotMan\Messages\Outgoing\Actions\Button;

use BotMan\Drivers\Telegram\Extensions\Keyboard;
use BotMan\Drivers\Telegram\Extensions\KeyboardButton;

use App\Services\TelegramQuestionEditMessage as TgEditMessage;
use App\Services\TelegramQuestionMessage as TgMessage;

class DemoEditConversation extends Conversation {

  public $name;
  public $song;
  public $movie;

  public function getResult() {
    $question = TgEditMessage::create("Ok *" . $this->name . "*, best song is *" . $this->song . "*, movie is *" . $this->movie . "*. " . PHP_EOL . "Type «back» to go back." . PHP_EOL . "Type «super-back» to go super-back.")
      //->setKeyboard($keyboard)
      ->setMessageId($this->bot->getMessage()->getPayload()['message_id']);

    return $this->ask(
      $question,
      function (Answer $answer) {
        if (strtolower($answer->getText()) === "back") {
          return $this->askMovie();
        }
        if (strtolower($answer->getText()) === "super-back") {
          return $this->start();
        }
        return $this->getResult();
        //$this->stopsConversation($answer);
      },
      ['parse_mode' => 'Markdown']
    );
  }
  public function askMovie() {
    $keyboard = Keyboard::create()
      ->type(Keyboard::TYPE_INLINE)
      ->addRow(
        KeyboardButton::create('<<< Back')->callbackData('back'),
      )
      ->addRow(
        KeyboardButton::create('Star wars')->callbackData('Star wars'),
      )
      ->addRow(
        KeyboardButton::create('Lord of the rings')->callbackData('Lord of the rings'),
      )
      ->addRow(
        KeyboardButton::create('Harry Potter')->callbackData('Harry Potter'),
      )
      ->toArray();

    // Если нет кнопок, тогда их и менять нельзя..
    if (empty($this->bot->getMessage()->getPayload()['reply_markup'])) {
      $question = TgMessage::create("Well, *" . $this->song . "* is very nice song! What about movie?")
        ->setKeyboard($keyboard);
    } else {
      $question = TgEditMessage::create("Wow! *" . $this->song . "* is very nice song! What about movie?")
        ->setKeyboard($keyboard)
        ->setMessageId($this->bot->getMessage()->getPayload()['message_id']);
    }

    return $this->ask(
      $question,
      function (Answer $answer) {
        if ($answer->isInteractiveMessageReply()) {
          switch ($answer->getValue()) {
            case "back":
              return $this->askSong();
              break;
            case "Star wars":
            case "Lord of the rings":
            case "Harry Potter":
              $this->movie = $answer->getValue();
              return $this->getResult();
              break;
          }
        }
        $this->movie = $answer->getText();
        return $this->getResult();
      },
      ['parse_mode' => 'Markdown']
    );
  }

  public function askSong() {
    $keyboard = Keyboard::create()
      ->type(Keyboard::TYPE_INLINE)
      ->addRow(
        KeyboardButton::create('<<< Back')->callbackData('back'),
        KeyboardButton::create('Skip >>>')->callbackData('skip'),
      )
      ->addRow(
        KeyboardButton::create('Merry Christmas')->callbackData('Merry Christmas'),
      )
      ->addRow(
        KeyboardButton::create('Show must go on')->callbackData('Show must go on'),
      )
      ->toArray();

    // Если нет кнопок, тогда их и менять нельзя..
    if (empty($this->bot->getMessage()->getPayload()['reply_markup'])) {
      $question = TgMessage::create("Yo, " . $this->name . "! What is your favorite song - _type_ or _select_?")
        ->setKeyboard($keyboard);
    } else {
      $question = TgEditMessage::create("Hey, " . $this->name . "! What is your favorite song - _type_ or _select_?")
        ->setKeyboard($keyboard)
        ->setMessageId($this->bot->getMessage()->getPayload()['message_id']);
    }

    return $this->ask(
      $question,
      function (Answer $answer) {
        if ($answer->isInteractiveMessageReply()) {
          switch ($answer->getValue()) {
            case "back":
              return $this->start(true);
              break;
            case "skip":
              if (!$this->song) {
                $this->song = "n/a";
              }
              return $this->askMovie(true);
              break;
            case "Merry Christmas":
            case "Show must go on":
              $this->song = $answer->getValue();
              return $this->askMovie(true);
              break;
          }
        }
        $this->song = $answer->getText();
        return $this->askMovie();
      },
      ['parse_mode' => 'Markdown']
    );
  }

  public function start() {
    $keyboard = Keyboard::create()
      ->type(Keyboard::TYPE_INLINE)
      ->addRow(
        KeyboardButton::create('Skip >>>')->callbackData('skip'),
      )
      ->toArray();

    // Если нет кнопок, тогда их и менять нельзя..
    if (empty($this->bot->getMessage()->getPayload()['reply_markup'])) {
      $question = TgMessage::create("Your name?")
        ->setKeyboard($keyboard);
    } else {
      $question = TgEditMessage::create("Your name?")
        ->setKeyboard($keyboard)
        ->setMessageId($this->bot->getMessage()->getPayload()['message_id']);
    }

    return $this->ask(
      $question,
      function (Answer $answer) {
        if ($answer->isInteractiveMessageReply()) {
          switch ($answer->getValue()) {
            case "skip":
              if (!$this->name) {
                $this->name = "...";
              }
              return $this->askSong(true);
              break;
          }
        }
        $this->name = $answer->getText();
        return $this->askSong();
      },
      ['parse_mode' => 'Markdown']
    );
  }

  public function run() {
    return $this->start();
  }
}

Внедряемся в TelegramDriver.php

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

// В файле
// vendor\botman\driver-telegram\src\TelegramDriver.php

// После условия
} elseif ($message instanceof OutgoingMessage) {...}

// Добавить строки:
} elseif($message instanceof \App\Services\TelegramQuestionEditMessage) {
  // For "editMessage"
  $parameters['message_id'] = $message->getMessageId();
  $parameters['text'] = $message->getText();
  $this->endpoint = 'editMessageText';
  $parameters['reply_markup'] = $message->getReplyMarkup();
  // 
  //if(!empty($message->getReplyMarkup())){
  //  $this->endpoint = 'editMessageReplyMarkup';
  //}
} elseif ($message instanceof \App\Services\TelegramQuestionMessage) {
  $parameters['text'] = $message->getText();
  $parameters['reply_markup'] = $message->getReplyMarkup();
}

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

  1. Напишите первый комментарий
*Комментарий будет опубликован после проверки модератором

Похожие статьи

Загрузка изображения в Laravel

Русификация Laravel

Добавить поле к существующей таблице Laravel

Транслитерация URL в Laravel. Примеры str_slug()

Как поменять язык в Faker

Разработка бота Telegram с помощью Botman на локальном компьютере

Валидация данных в Laravel form request

Laravel Excel - Базовый экспорт

Добавить в botman editMessageText для Telegram

Как сделать middleware в Laravel 6 - простой пример

Создать ссылку на storage из внешнего каталога для Laravel

Установить Laravel в отдельную папку (site.ru/laravel/)

Как русифицировать или поменять шаблон уведомления о сбросе пароля в Laravel

Laravel: Отношения моделей многие ко многим - belongsToMany

Валидация номера кредитной карты на PHP (Laravel)

Laravel: Отношения моделей один ко многим - hasMany, belongsTo

Связать папку storage с папкой public в Laravel

Откуда в Laravel Jetstream (inertia) prop auth.user?

Экспорт маршрутов из Laravel в JSON файл

Создание form request в Laravel: руководство для начинающих

Постраничная навигация на Bootstrap в Laravel 8

Обработка ошибок в Laravel form request

Blade - расширить @section с использованием директивы @parent

Добавить данные об авторизации при каждом запросе к API в Laravel

Работа с файлами в Laravel Form Request

Работа с вложенными объектами и коллекциями в Laravel form request

Стандартные свойства модели в Laravel

Laravel: Отношение через таблицу-посредника - hasOneThrough()

Laravel: Отношения моделей один к одному - hasOne, belongsTo

Авторизацией и аутентификация в Laravel form request

Настройка команды CRON на хостинге nic.ru для активации Laravel schedule

Laravel form request для создания API-запросов

Laravel: Отношение через таблицу-посредника - hasManyThrough()

HTTP-запросы в Laravel form request - работа с различными типами запросов

Создание класса модели в Laravel

Создание уникального индекса в миграции Laravel

Установка Laravel и создание нового проекта

Как создать модель в Laravel

Создание таблицы в базе данных (миграции) для модели в Laravel

Базовые методы CRUD контроллера для модели в Laravel

Наш сайт использует куки, нажмите «ОК» если вы не против
OK