В 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)
Не писать ответ