Я занимаюсь разработкой АТС с открытым исходным кодом MikoPBX.
Недавно познакомился с проектом tg2sip. Шлюз позволяет подключить Telegram аккаунт к офисной АТС, принимать и совершать звонки.
После настройки шлюза, решили, что было бы неплохо после завершения телефонного разговора отправить клиенту клавиатуру для оценки качества обслуживания.
При попытке реализовать функцию столкнулись со сложностями:
Пользователь не может отправлять / пересылать клавиатуру другому пользователю
Бот не может писать пользователю, если тот на него не подписан
Как же быть? Решение опишу под катом. Приступим...
Шлюз tg2sip позволяет работать только с аудио звонками. Работа с сообщениями не поддерживается. Начались поиски библиотеки для работы с Telegram.
Так как в своих проектах мы активно используем PHP 7.4, то выбор пал на проект MadelineProto. Он позволяет:
Работать в качестве бота
Работать в качестве клиента Telegram, аналог desktop приложения
Работать асинхронно (non-blocking I/O). Разработан на основе amphp
Поиски привели к документации telegram InlineQueryResultArticle.
Единственно возможный способ для пользователя отправить клавиатуру другому пользователю - это использовать inline бота.
Алгоритм следующий:
Открываем любой чат
Вводим имя бота
После имени бота вводим произвольную строку - запрос
Бот присылает "результаты"
Пользователь кликает по одному из результатов
Отправляется сообщение собеседнику
Вот пример работы с inline ботом:
Теперь необходимо это реализовать в своем скрипте.
Запускаем бота
Пример работы с библиотекой MadelineProto:
<?php
/** Инициализация бота telegram */
$MadelineProto []= new API('bot.madeline');
/** Подключаем класс обработчик для бота telegram */
$handlers []= BotEventHandler::class;
try {
API::startAndLoopMulti($MadelineProto, $handlers);
}catch (Throwable $e){
}
Класс BotEventHandler
<?php
class BotEventHandler extends EventHandler
{
public function onAny($update)
{
if('updateBotInlineQuery' === $update['_']){
// Обработка inline запроса.
yield $this->getCallbackKeyboardMessage($update);
}elseif ('updateInlineBotCallbackQuery' === $update['_']) {
// Обновление / изменение inline клавиатуры после нажатия.
yield $this->updateInlineKeyboard($update);
}
}
private function updateInlineKeyboard($update){
$bytes = $update['data']->__toString();
// Тут можно обработать данные нажатой кнопки.
$replyKeyboardMarkup = [
'_' => 'replyInlineMarkup',
'resize' => true,
'single_use' => true,
'selective' => true,
'rows' => [
['_' => 'keyboardButtonRow', 'buttons' => [
['_' => 'keyboardButtonCallback', 'text' => "Нажми меня снова!", 'data' => "changed:$bytes", 'requires_password' => false],
]
],
]
];
$params = [
'no_webpage' => true,
'id' => $update['msg_id'],
'message' => 'Обновленная клавиатура: '.time(),
'reply_markup' => $replyKeyboardMarkup,
'parse_mode' => 'html'
];
yield $this->messages->editInlineBotMessage($params);
}
private function getCallbackKeyboardMessage($update)
{
$query = $data['query']??'';
$replyKeyboardMarkup = [
'_' => 'replyInlineMarkup',
'resize' => true,
'single_use' => true,
'selective' => true,
'rows' => [
['_' => 'keyboardButtonRow', 'buttons' => [
['_' => 'keyboardButtonCallback', 'text' => "Нажми меня", 'data' => "callback:$query", 'requires_password' => false],
]
],
]
];
$message = 'Текст сообщения:';
$params = [
'query_id' => $update['query_id'],
'results' => [
[
'_' => 'inputBotInlineResult',
'id' => '1',
'type' => 'article',
'title' => 'Заголовок сообщения',
'send_message' => [
'_' => 'inputBotInlineMessageText',
'no_webpage' => true,
'message' => $message,
'reply_markup' => $replyKeyboardMarkup
]
],
],
'cache_time' => 1,
];
yield $this->messages->setInlineBotResults($params);
}
}
Этот inline Бот может предложить один "результат" - inline клавиатуру. После нажатия на кнопку, клавиатура будет модифицирована Ботом.
При первом запуске скрипта Madeline запросит информацию для авторизации. Подробно механизм описан в документации.
Запускаем Telegram клиент
Пример скрипта:
<?php
// Идентификатор бота
const BOT_ID = 5118292901;
/** Инициализация клиента telegram */
$MadelineProto []= new API('session.madeline');
/** Подключаем класс обработчик для "клиента" telegram */
$handlers []= TgUserEventHandler::class;
try {
API::startAndLoopMulti($MadelineProto, $handlers);
}catch (Throwable $e){
}
Скрипт отличается от бота только именем файла сессии session.madeline'
при первом запуске потребуется ввести данные авторизации.
Класс TgUserEventHandler
<?php
class TgUserEventHandler extends EventHandler
{
private int $myId;
public function onStart()
{
// Получаем свой ID.
$this->myId = $this->getAPI()->getSelf()['id'];
}
public function onUpdateNewMessage(array $update)
{
$reason = $update['message']["action"]["reason"]['_']??'';
$fromId = $update['message']["from_id"]["user_id"]??0;
// Обрабатываем оповещение о завершении звонка.
if($reason === 'phoneCallDiscardReasonHangup' && $this->myId === $fromId){
yield $this->sendKeyboard($update);
}
}
private function sendKeyboard(array $update)
{
// Собираем информацию о собеседнике
$userId = $update["message"]["peer_id"]["user_id"]??'';
$userData = yield $this->users->getUsers(['id' => [$userId]]);
if(yield $userData){
// ID собеседника передадим в inline запросе.
$data = $userData[0]['id'];
$peer = yield $this->getAPI()->getID($update);
// Отправляем inline запрос боту
$messages_BotResults = yield $this->getResultsFromBot($peer, $data);
$results = yield $messages_BotResults['results'];
if((yield $results) && count($results)>0){
$msg = [
'peer' => $peer,
'query_id' => $messages_BotResults['query_id'],
'id' => $messages_BotResults['results'][0]['id'],
];
// Отправляем первый из результатов собеседнику.
yield $this->messages->sendInlineBotResult($msg);
}
}
}
private function getResultsFromBot(int $peer, string $query)
{
$params = [
'bot' => BOT_ID,
'peer' => $peer,
'query' => $query,
'offset'=> '0'
];
return yield $this->messages->getInlineBotResults($params);
}
}
Важно отметить:
Бот не может писать пользователю, который на него не подписан
Если клавиатура отправляется через sendInlineBotResult, то, формально, Бот будет взаимодействовать с текущим пользователем, а не с собеседником. Это значит, когда собеседник нажмет кнопку, бот в качестве user_id получит ваш идентификатор, а не собеседника
Эти факты требуют от нас дополнительных действий для передаче боту информации о "собеседнике". Пример реализации приведен в скрипте выше.
Немного о callback методах:
onStart - вызывается после создания объекта "TgUserEventHandler", тут можно выполнить инициализацию
onUpdateNewMessage - вызывается при получении нового сообщения, к примеру, при завершении звонка приходит сообщение "Исходящий звонок" и его статус "Отменен"
Теперь все вместе - клиент и Бот
<?php
require_once 'vendor/autoload.php';
const BOT_ID = 5118292901;
/** Инициализация клиента telegram */
$MadelineProto []= new API('session.madeline');
/** Подключаем класс обработчик для "клиента" telegram */
$handlers []= TgUserEventHandler::class;
/** Инициализация бота telegram */
$MadelineProto []= new API('bot.madeline');
/** Подключаем класс обработчик для бота telegram */
$handlers []= BotEventHandler::class;
try {
API::startAndLoopMulti($MadelineProto, $handlers);
}catch (Throwable $e){
}
Клиент и бот будут работать в одном процессе асинхронно не мешая друг другу. Пример, как это может выглядеть:
Итоги
Задачу мы успешно решили, бесценный опыт получили. Telegram действительно современный, удобный, и мощный инструмент для коммуникаций.
Области применения ограничиваются только фантазией:
Продажи - отображение статуса заказов, обновление информации по заказу, запрос обратного звонка клиентом
Доставка - отображение текущего статуса, выполнение действий над заказом
Телефония - оценка качества обслуживания после телефонного звонка
Полезные материалы
Бесплатная АТС с открытым исходным кодом MikoPBX
Документация Madeline
Документация Telegram API
Проект tg2sip