
Одна из особенностей мессенджера Telegram — широкие возможности API (Bot API и Telegram API). Команда Telegram пошла ещё дальше и выпустила библиотеку TDLib (Telegram Database Library), позволяющую разрабатывать альтернативные клиенты Telegram и не задумываться о низкоуровневых деталях реализации (работа с сетью, шифрование и локальное хранение данных).
TDLib работает на Android, iOS, Windows, macOS, Linux, Windows Phone, WebAssembly, watchOS, tvOS, Tizen, Cygwin и других *nix системах, а так же интегрируется с любым языком программирования, поддерживающим выполнение C-функций.
В этой статье мы рассмотрим использование TDLib в Ruby и создание gem'а для взаимодействия с JSON-интерфейсом библиотеки.
Подключение libtdjson
Для начала нам понадобится скомпилированная TDLib. Инструкцию по сборке можно прочесть на официальном сайте. Из скомпилированных бинарников нам нужен только libtdjson.[so|dylib|dll].
Чтобы подключить функции библиотеки в Ruby можно использовать модуль Fiddle из стандартной библиотеки. Fiddle::Importer предоставляет удобный DSL для импорта функций из динамических библиотек:
module Dl extend Fiddle::Importer dlload('libtdjson.so') extern 'void* td_json_client_create()' extern 'void* td_json_client_send(void*, char*)' extern 'char* td_json_client_receive(void*, double)' extern 'char* td_json_client_execute(void*, char*)' extern 'void td_set_log_verbosity_level(int)' extern 'void td_json_client_destroy(void*)' extern 'void td_set_log_file_path(char*)' end
Теперь мы можем вызывать функции TDLib:
client = Dl.td_json_client_create Dl.td_json_client_send(client, '{"@type": "getAuthorizationState"}')
Создание клиента
TDLib — полностью асинхронная библиотека (лишь немногие функции можно вызывать синхронно с помощью td_json_client_execute), поэтому работать с ней нужно соответствующим образом:
Dl.td_json_client_send(client, '{"@type": "getAuthorizationState"}') timeout = 10 loop do update = Dl.td_json_client_receive(client, timeout) next if update.null? update = JSON.parse(update.to_s) if update['@type'] = 'updateAuthorizationState' p update break end end
Это рабочий код, однако не самый удобный. Лучше разработать обертку для взаимодействия с библиотекой: с обработчиками событий, callback'ами, удобным конфигурированием и возможностью не писать boilerplate-код с первоначальной авторизацией.
Далее рассмотрим основную функциональность gem'а tdlib-ruby (ссылка в конце статьи).
Инициализация клиента
Процедуры отправки параметров библиотеки и проверки ключа шифрования скрыты внутри. Для начала работы достаточно создать экземпляр клиента:
client = TD::Client.new client.on_ready do |client| # some useful stuff end
Отправка "сообщений"
Сообщения отправляются в tdlib асинхронно.
client.broadcast('@type' => 'getAuthorizationState')
Есть возможность повесить callback-обработчик.
client.broadcast('@type' => 'getMe') do |update| p update end
Подписка на обновления определённого типа
client.on('updateAuthorizationState') do |update| p update end
При получении от TDLib обновления с типом `updateAuthorizationState' всегда будет выполняться обработчик, переданный как блок.
Синхронная отправка сообщений
Некоторые методы (их немного, и я пока что таковых не встретил) могут возвращать ответ синхронно. Для этих случаев предусмотрен метод execute.
client.execute('@type' => 'someType')
Работа с асинхронными сообщениями/обновлениями в синхронном стиле
Надо просто отправить запрос и получить результат? Асинхронная природа TDLib этого не позволяет, однако нужный механизм реализован в gem'е.
authorization_state = client.broadcast_and_receive('@type' => 'getAuthorizationState')
Напоследок приведу пример консольного скрипта авторизации:
require 'tdlib-ruby' TD.configure do |config| config.lib_path = 'path_to_dir_containing_tdlibjson' config.client.api_id = your_api_id config.client.api_hash = 'your_api_hash' end TD::Api.set_log_verbosity_level(1) client = TD::Client.new begin state = nil client.on('updateAuthorizationState') do |update| next unless update.dig('authorization_state', '@type') == 'authorizationStateWaitPhoneNumber' state = :wait_phone end client.on('updateAuthorizationState') do |update| next unless update.dig('authorization_state', '@type') == 'authorizationStateWaitCode' state = :wait_code end client.on('updateAuthorizationState') do |update| next unless update.dig('authorization_state', '@type') == 'authorizationStateReady' state = :ready end loop do case state when :wait_phone p 'Please, enter your phone number:' phone = STDIN.gets.strip params = { '@type' => 'setAuthenticationPhoneNumber', 'phone_number' => phone } client.broadcast_and_receive(params) when :wait_code p 'Please, enter code from SMS:' code = STDIN.gets.strip params = { '@type' => 'checkAuthenticationCode', 'code' => code } client.broadcast_and_receive(params) when :ready @me = client.broadcast_and_receive('@type' => 'getMe') break end end ensure client.close end p @me
Полезные ссылки
TDLib на Github
Документация TDLib
Инструкция по сборке
Telegram-плагины для Redmine от Southbridge
Автор
Ruby-разработчик Southbridge Владислав Яшин