Использование Telegram Core API (MTProto) на PHP

Эта заметка не про Bot API, а про Core API Telegram, с помощью которого можно создавать полноценные клиенты для месседжера, и конечно же любой другой софт, например для сбора данных из Телеграма. Основная проблема заключается в том, что общение с серверами Telegram осуществляется по специальному протоколу разработанным внутри компании — MTProto. Именно благодаря этому протоколу данный месседжер и славится своей безопасностью и шифрование данных.

Вас мучают вопросы: как использовать Telegram Api на PHP? Как вызывать функции? Очень много примеров использования telegram api для бота, а как использовать обычное api telegram? Зарегистрировал приложение, получил api_id и api_hash, как получить все сообщения из телеграм-канала? https://core.telegram.org/method/messages.getHistory
Как вызвать этот метод? Как реализовать авторизацию с помощью API Telegram? Тогда эта статья для вас!

Естественно, разбирать нюансы протокола MTProto в данной заметке я не буду. Для работы с ним буду пользоваться PHP-библиотекой MadelineProto, доступной всем желающим на GitHub. Однако, нельзя просто так взять и воспользоваться библиотекой. Есть как минимум три нюанса, которые нужно решить.

Подготовка к установке MadelineProto

Во-первых, нужен установленный Python, будет достаточно версии 2.7.

Во-вторых, библиотека не помечена как стабильная, поэтому для подключения её через composer к существующему проекту нужно немного отредактировать composer.json:

"minimum-stability": "dev",

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

Your requirements could not be resolved to an installable set of packages.
Problem 1
 - Installation request for danog/madelineproto ^2.0 -> satisfiable by danog/madelineproto[2.0].
 - danog/madelineproto 2.0 requires danog/primemodule dev-master -> satisfiable by danog/primemodule[dev-master] but these conflict with your requirements or minimum-stability.
Installation failed, reverting ./composer.json to its original content.

Затем нужно указать git-репозиторий библиотеки:

"repositories": [
    {
        "type": "git",
        "url": "https://github.com/danog/phpseclib"
    }
],

и только затем можно устанавливать саму либу:

composer require danog/madelineproto

Если во время установки зависимостей появится ошибка на подобии такой:

github Failed to clone via https, ssh protocols, aborting.
error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version

То вам нужно обновить версию git.

Регистрация приложения для Telegram API

Теперь нужно зарегистрировать приложение в разделе API development tools и получить App api_id и App api_hash.

Как правильно использовать MadelineProto с Laravel

В-третьих, на сегодняшний день (2017-02-10) мне не удалось запустить MadelineProto из коробки, т.к. начинали сыпаться ошибки типа:

DataCenter: Connecting to DC 2 (main server, ipv4, tcp_full)...
Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153
DataCenter: Connection failed, retrying connection on port 443...
Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153
DataCenter: Connection failed, retrying connection on port 80...
Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153
DataCenter: Connection failed, retrying connection on port 88...
Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153
DataCenter: Connection failed, retrying connection on port 443 without the proxy...
Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153
DataCenter: Connection failed, retrying connection on port 80 without the proxy...
Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153
DataCenter: Connection failed, retrying connection on port 88 without the proxy...
Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153
Exception: Undefined offset: 2 in MsgIdHandler.php:77
CallHandler: An error occurred while calling method help.getNearestDc: Undefined offset: 2 in MsgIdHandler on line 77. Recreating connection and retrying to call method...
Exception: Undefined offset: 2 in MTProto.php:641
In MTProto.php line 641:
Undefined offset: 2

На самом деле здесь нет ничего фатального, просто фреймворк Laravel по-умолчанию перехватывает все ошибки и при отсуствии должных обработчиков завершает скрипт даже при наличии не критичных ошибок. Возможно такое поведение присутствует и в других фреймворках. Можно изменить уровень ошибок, добавив в метод \App\Providers\AppServiceProvider::boot() строку:

error_reporting(0);

Но тогда есть вероятность пропустить некритичные ошибки своего приложения.

Вторым способом устранения ошибок будет правка исходника /vendor/danog/madelineproto/src/danog/MadelineProto/Connection.php, а именно нужно закомментировать 3 строки в конструкторе в условии

case 'tcp_full':
//                $this->sock->setOption(\SOL_SOCKET, \SO_RCVTIMEO, $timeout);
//                $this->sock->setOption(\SOL_SOCKET, \SO_SNDTIMEO, $timeout);
//                $this->sock->setBlocking(true);

В коммите 56c0d431768c04009ae9aa3151715b5e6399ec4d эти строки находятся на 105-107 строках файла. Источник проблемы был найден с помощью отладчика xDebug. Проблема заключалась в том, что методы $this->sock->setOption() и  $this->sock->setBlocking() пытались работать с ещё не созданным объектом $this->sock->sock. Если у вас возникнут другие ошибки, то с помощью отладчика вы их легко обнаружите и исправите.

Также в библиотеку могут быть зашиты устаревшие или не актуальные IP-адреса серверов Телеграма. Их всегда можно посмотреть на странице API development tools и передать в ModelineProto через конструктор \danog\MadelineProto\API().

Список всех параметров которые можно изменить в этой библиотеке можно посмотреть в массиве $default_settings метода \danog\MadelineProto\MTProto::parse_settings().

В идеале нужно зарегистрировать для приложения новую чистую учётную запись, но для тестирования и отладки вполне сгодится любая уже имеющаяся учётка Телеграма.

Пример работы MadelineProto на Laravel

Как делать запросы к Telegram API на PHP?

Приведу простой пример кода на базе консольной команды для Laravel:

public function handle() {
        // Если файл с сессией уже существует, использовать его
        if(file_exists( env('TELEGRAM_SESSION_FILE') ) ) {
            $madeline = new API( env('TELEGRAM_SESSION_FILE') );
        } else {
        // Иначе создать новую сессию
            $madeline = new API([
                'app_info' => [
                    'api_id' => env('TELEGRAM_API_ID'),
                    'api_hash' => env('TELEGRAM_API_HASH'),
                ]
            ]);
            // Задать имя сессии
            $madeline->session = env('TELEGRAM_SESSION_FILE');
            // Принудительно сохранить сессию
            $madeline->serialize();
            // Начать авторизацию по номеру мобильного телефона
            $madeline->phone_login( env('TELEGRAM_PHONE') );
            // Запросить код с помощью консоли
            $code = readline('Enter the code you received: ');
            $madeline->complete_phone_login($code);
        }
        $messages = $madeline->messages->getHistory(['peer' => '@ANY_CHANNEL_ID', 'offset_id' => 0, 'offset_date' => 0, 'add_offset' => 0, 'limit' => 10, 'max_id' => 0, 'min_id' => 0, 'hash' => 0, ]);
        foreach($messages['messages'] as $msg) {
            dump($msg);
        }
    }

Для тех, кто не умеет в Laravel, кратко поясню. Вызовы env() — это запросы значений из файла конфигурации, можно заменить их на константы или захардкодить. Собственно:

TELEGRAM_SESSION_FILE — любое значение, которое можно использовать в качестве имени файла.

TELEGRAM_API_ID и TELEGRAM_API_HASH — Данные из API development tools.

TELEGRAM_PHONE — мобильный номер существующий учётки, например, +7XXXXXXXXXX.

Теперь пояснения о происходящем в коде. Сессия — достаточно важный объект клиента, без него при каждом запуске скрипта авторизовываться и вводить код из сообщения, который Telegram высылает либо в смс либо через сам месседжер. Также на этапе авторизации происходит вся знаменитая шифровочная телеграм-магия. Процесс не быстрый, на моём тестовом стенде он мог затягиваться на минуты.

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

API: Running APIFactory...
API: MadelineProto is ready!
API: Serializing MadelineProto...
Login: Sending code...
Login: Code sent successfully! Once you receive the code you should use the complete_phone_login function.
Enter the code you received: ...
Login: Logging in as a normal user...
MTProto: Trying to copy authorization from dc 2 to dc 1
MTProto: Trying to copy authorization from dc 2 to dc 3
MTProto: Trying to copy authorization from dc 2 to dc 4
ResponseHandler: Parsing updates received via the socket...

После чего можно полноценно использовать все возможности Telegram Core API, например,  $messages = $madeline->messages->getHistory().

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