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-интерфейсом библиотеки.


    Подключение 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


    gem tdlib-ruby


    Автор


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

    • +15
    • 4,2k
    • 6
    Southbridge 158,68
    Обеспечиваем стабильную работу серверов
    Поделиться публикацией
    Комментарии 6
      +1
      Скажите пожалуйста, есть ли возможность скомпилировать tdjson Си-компилятором? Я хочу использовать tdlib на старом девайсе, C++ компилятор которого лишь частично поддерживает С++11, не говоря уже о стандарте С++14. Вот думаю, реально ли хотя бы частично использовать ее с С-компилятором.
        +1
        Боюсь, что нет. В требованиях указан C++14-совместимый компилятор. Но можно попробовать спросить в issues — может быть, есть обходные пути.
          0
          Сомневаюсь, что есть обходные пути, лол. Больше 95% написано на чистом C++.
        0
        Работа с асинхронными сообщениями/обновлениями в синхронном стиле

        Я бы ребята посоветовал реализовать это через промисы/future, что намного удобнее и проще в работе, например

        promise = client.broadcast('@type' => 'getAuthorizationState')
        
        result = promise.await
        
        # или
        
        client.broadcast('@type' => 'getAuthorizationState').then { |result| ... }
        


        Тогда не нужно будет городить разные методы для синхронных/асинхронных методов, а иметь единый интерфейс.

        Кстати есть отличный нативный гем — 'concurrent-ruby', в котором уже все удобно реализовано.

        Если будет свободное время кину вам пулл-реквест, если не возражаете :)
          0
          Уже сделано в последней версии (через concurrent-ruby):
          me = client.broadcast('@type' => 'getMe').then { |result| puts result }.rescue { |error| puts error }.value
          

          broadcast_and_receive теперь просто вызывает broadcast(payload).value
            0
            Отлично. Спасибо!

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое