...

вторник, 20 февраля 2018 г.

tdlib-ruby: как сделать Telegram-клиент на Ruby

image
Одна из особенностей мессенджера 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-интерфейсом библиотеки.


Для начала нам понадобится скомпилированная 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

gem tdlib-ruby


Ruby-разработчик Southbridge Владислав Яшин

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий