Я занимаюсь разработкой АТС с открытым исходным кодом MikoPBX.

Недавно познакомился с проектом tg2sip. Шлюз позволяет подключить Telegram аккаунт к офисной АТС, принимать и совершать звонки.

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

При попытке реализовать функцию столкнулись со сложностями:

  • Пользователь не может отправлять / пересылать клавиатуру другому пользователю

  • Бот не может писать пользователю, если тот на него не подписан

Как же быть? Решение опишу под катом. Приступим...


Шлюз tg2sip позволяет работать только с аудио звонками. Работа с сообщениями не поддерживается. Начались поиски библиотеки для работы с Telegram.

Так как в своих проектах мы активно используем PHP 7.4, то выбор пал на проект MadelineProto. Он позволяет:

  • Работать в качестве бота

  • Работать в качестве клиента Telegram, аналог desktop приложения

  • Работать асинхронно (non-blocking I/O). Разработан на основе amphp

Поиски привели к документации telegram InlineQueryResultArticle.

Единственно возможный способ для пользователя отправить клавиатуру другому пользователю - это использовать inline бота.

Алгоритм следующий:

  1. Открываем любой чат

  2. Вводим имя бота

  3. После имени бота вводим произвольную строку - запрос

  4. Бот присылает "результаты"

  5. Пользователь кликает по одному из результатов

  6. Отправляется сообщение собеседнику

Вот пример работы с 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'],
                ];
                // Отправляем первый из результатов собеседнику. 
                $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

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