«Клиентов нужно не искать, а создавать»: погружение в Telegram API через TDLib

qaktuexrpvrljybv2glx5rtow8o.png


Сперва я рассказывал простые вещи о Telegram Bot API и делал интересных ботов — виртуальную подругу и друга для заказа шавермы. Затем коснулся тестовых серверов и юзерботов. И наконец, пришла пора заглянуть глубже — узнать, как сделать свой клиент для Telegram. Что такое TL-схема и TDLib? Об этом мы сегодня и узнаем.

Данная статья не только поможет тем, кто решил написать свой клиент для Telegram, но и немного расширит кругозор остальным: MTProto — это не приевшийся JSON API. Добро пожаловать под кат!

Готовы показать свои знания в IT? Примите участие в IT-кроссворде Selectel, выиграйте 10 000 рублей на аренду серверов и эксклюзивный мерч Selectel.


Прежде чем мы начнем


Telegram поощряет, ну или по крайней мере не наказывает, за разработку пользовательских клиентов. Для создания клиента необходимо придерживаться следующих правил:

  1. Необходимо использовать свой уникальный APP_ID.
  2. Необходимо следовать правилам безопасности.
  3. Можно расширять функциональность Telegram, но нельзя заставлять пользователей других приложений переходить в ваше приложение.
  4. Нельзя нарушать базовые механики мессенджера, например, делать «невидимки» и «нечитайки».
  5. Нельзя выполнять действия без ведома пользователя, например, автоматически подписываться на канал или рассылать сообщения.
  6. Если клиент обеспечивает доступ к каналам, то необходимо также реализовать функциональность «спонсированных сообщений».
  7. Нельзя выдавать приложение за официальное.
  8. Монетизировать можно любым легальным способам, если о нем написано на странице приложения.


Нарушение этих правил приведет к предупреждению, а его игнорирование — к отключению API для вашего приложения. Также команда Telegram может запросить удалить ваше приложение из магазинов.

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

Готовые решения


Когда смотришь на количество «фич» в современном Telegram-клиенте, невольно представляешь себе поезд прогресса, у которого отказали тормоза. Быстро написать «с нуля» что-то сравнимое по функциональности с официальными клиентами практически невозможно. Поэтому энтузиасты делают то, что официальный Telegram не дает. В первую очередь вспоминаются юзерботы — их можно разделить на две категории.

Классические боты в «шкуре» пользователя

У обычных ботов в Telegram очень мало прав. У них по умолчанию нет возможности посмотреть историю сообщений в чате, даже если есть разрешение на доступ к переписке.

За доступ к истории чатов придется «заплатить» — вы не сможете создавать кнопки и использовать особые сообщения, например, выставлять счета. Нехорошие люди используют такой вид ботов для спама в публичных чатах.

Автоматизация действий пользователя

В этом варианте «бот» — это программа, которая использует основной аккаунт пользователя и реализует функциональность, недоступную в официальном приложении. Например, есть расширение PMPermit, которое автоматически отправляет в черный список незнакомцев, которые вам пишут.

Использование такого вида ботов — интересный процесс. Пользователь пишет команду в чат, где хочет выполнить действие. Сообщение отправляется на серверы Telegram, а оттуда «прилетает» обновлением в «клиент» юзербота. Бот удаляет сообщение-команду из чата и выполняет заданное действие.

На GitHub есть много юзербот-проектов, но большинство из них на Python и используют фреймворки Telethon или Pyrogram. Для реализации своих задумок обычно достаточно использовать готового юзербота или написать личного на указанных фреймворках.

Функции и классы обычно имеют исчерпывающую документацию, которая доступна для среды разработки. Но почему у проектов такая хорошая документация? Пришла пора поговорить о TL-схемах.

tr8ugqti-mkqfdyxzlcgj1ovpy0.png


TL-схема


TL — это от словосочетания «Type Language». Если коротко, то это особый язык описания типов и функций. Для Telegram существует несколько схем: организация шифрования для MTProto, основное API, e2e-шифрование и секретные чаты.

Рассмотрим описание одного конструктора для класса User:

user#d23c81a3 id:int first_name:string last_name:string = User;


Что в этой строке есть:

  • user — человеко-читаемое имя конструктора.
  • d23c81a3 — машинное представление конструктора. Считается как CRC32 от строки.
  • id:int first_name:string last_name:string — имена аргументов и их типы.
  • User — человеко-читаемое имя класса, которому принадлежит конструкторв.


Описание функций выглядит аналогично, но находится после строки ---functions---. Рассмотрим объявление функции.

getUser#b0f732d5 id:int = User;


Отличия от описания типа:

  • getUser — это имя функции.
  • User — это возвращаемое значение. Обратите внимание, что функции могут вернуть ошибку вместо значения — и это никак не отображается в схеме.


Telegram выкладывает обновления в виде слоев (layer). Каждый слой API имеет полную TL-схему и определяет поддерживаемую функциональность приложения.

Type Language — это не совсем оригинальное детище Telegram. В TL-парсере можно встретить упоминание ВК. В первых опубликованных исходниках kPHP находится оригинальный парсер. Вторая попытка открыть исходный код kPHP принесла документацию, в том числе по Type Language. Документация ссылается… на сайт Telegram!


В идеальном мире TL-схема — это исчерпывающее описание интерфейса Telegram. В реальности же есть нюансы. На момент подготовки этой статьи Telegram выпустил обновление от 29 октября с персональными цветами, цитатами и подсветкой синтаксиса. Это 166 слой схемы. На официальном сайте же доступен только слой 158 — общие папки и выбор обоев в чате от 21 апреля.

На corefork-поддомене (как его нашли?) есть слой 164 с историями каналов от 22 сентября. Актуальную схему можно найти в репозитории Telegram Desktop.

Сам файл схемы не содержит документации, за объяснением и возможными ошибками необходимо идти на отдельную страницу Telegram. Но повторюсь: там может не быть актуальной схемы.

Кроме того, в 2019 году nuclight написал лонгрид, посвященный костылям вызовам, которые встречаются при попытке реализовать MTProto с нуля по документации. И там очень много интересного.


К счастью, у Telegram есть решение на случай, если вы не хотите разбираться в тонкостях MTProto — TDLib.

Если вам интересно читать топики о программировании, Telegram и других технологиях, подписывайтесь на мой канал, где периодически пишу на разные темы.


TDLib


TDLib (Telegram Database Library) — это библиотека, которая абстрагирует разработчика от тонкостей работы с MTProto. Библиотека написана на С++ и имеет несколько интерфейсов:

  • Нативный. Библиотека используется как обычная С++-библиотека.
  • JNI (Java Native Interface) — биндинги (bindings) для вызова нативного кода из Java.
  • С++/CX — интерфейс для вызова нативного кода из .NET-окружения.
  • JSON — интерфейс, в котором общение происходит в формате JSON. Этот интерфейс позволяет «связать» TDLib со множеством других языков программирования, например, Python. Поговорим подробнее об этом интерфейсе.


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

Как и любое другое приложение Telegram, TDLib использует схему для взаимодействия с API. Возникает вопрос:, а как в этом проекте с актуальностью? Здесь есть хорошая и плохая новости.

Плохая новость заключается в том, что TDLib все еще на шаг позади и использует слой 165. Актуальный, напомню, 166. Хорошая же новость — это документация интерфейсов TDLib. Библиотека приносит четвертую TL-схему — td_api.tl, которая содержит документацию в комментариях:

// @description Represents a user
// @id User identifier
// @first_name First name of the user
// @last_name Last name of the user
// @usernames Usernames of the user; may be null
// @phone_number Phone number of the user
// @status Current online status of the user
// @profile_photo Profile photo of the user; may be null
// @emoji_status Emoji status to be shown instead of the default Telegram Premium badge; may be null. For Telegram Premium users only
// @is_contact The user is a contact of the current user
// @is_mutual_contact The user is a contact of the current user and the current user is a contact of the user
// @is_close_friend The user is a close friend of the current user; implies that the user is a contact
// @is_verified True, if the user is verified
// @is_premium True, if the user is a Telegram Premium user
// @is_support True, if the user is Telegram support account
// @restriction_reason If non-empty, it contains a human-readable description of the reason why access to this user must be restricted
// @is_scam True, if many users reported this user as a scam
// @is_fake True, if many users reported this user as a fake account
// @has_active_stories True, if the user has non-expired stories available to the current user
// @has_unread_active_stories True, if the user has unread non-expired stories available to the current user 
// @have_access If false, the user is inaccessible, and the only information known about the user is inside this class. Identifier of the user can't be passed to any method
// @type Type of the user
// @language_code IETF language tag of the user's language; only available to bots
// @added_to_attachment_menu True, if the user added the current bot to attachment menu; only available to bots
user id:int53 first_name:string last_name:string usernames:usernames phone_number:string status:UserStatus profile_photo:profilePhoto emoji_status:emojiStatus is_contact:Bool is_mutual_contact:Bool is_close_friend:Bool is_verified:Bool is_premium:Bool is_support:Bool restriction_reason:string is_scam:Bool is_fake:Bool has_active_stories:Bool has_unread_active_stories:Bool have_access:Bool type:UserType language_code:string added_to_attachment_menu:Bool = User;
–--functions---

@description Returns information about a user by their identifier. This is an offline request if the current user is not a bot @user_id User identifier
getUser user_id:int53 = User;


Достаточно подробно. Имена полей совпадают с полями в JSON, а имя конструктора (user) передается в поле с именем @type:

{
    "@type": "user",
    "id": 777000,
    "first_name": "Telegram",
    "last_name": "Notifications",
    "phone_number": "42777",
    "status": {
        "@type": "userStatusOnline",
        "expires": 2147483647
    },
    "is_contact": false,
    "is_mutual_contact": false,
    "is_close_friend": false,
    "is_verified": true,
    "is_premium": false,
    "is_support": true,
    "restriction_reason": "",
    "is_scam": false,
    "is_fake": false,
    "has_active_stories": false,
    "has_unread_active_stories": false,
    "have_access": true,
    "type": {
        "@type": "userTypeRegular"
    },
    "language_code": "",
    "added_to_attachment_menu": false
}


Есть нюанс при работе с типами. Telegram различает целочисленные типы разной длины — int32, int53 и int64. Для JSON это все один целочисленный тип. Второй особенный тип — bytes. В JSON-интерфейсе это base64-строка.

С функциями все аналогично. В поле @type нужно передать имя функции, а остальное — как прописано в схеме:

{
    "@type": "getUser",
    "user_id": 777000
}


TDLib — это асинхронная библиотека, в которой практически отсутствуют блокирующие вызовы:

// Создаем инстанс TdJson
void* td = td_json_client_create();

// Ожидаем ответ от TdJson в течение одной секунды.
// Если библиотека ничего не отдала, то указатель будет NULL.
// Эту функцию следует вызывать исключительно в одном потоке.
const char* res = td_json_client_receive(td, 1);

// Отправляем запрос в TdJson. Этот метод потокобезопасный.
const char* payload = "{\"@type\": \"getUser\", \"user_id\": 777000}";
td_json_client_send(td, payload);

// Прибираем за собой при завершении программы.
td_json_client_destroy(td);


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

{
    "@type": "error",
    "code": 404,
    "message": "Not Found",
}


Как разобраться, к какому запросу относится ответ? Решение гениально и кроется в поле @extra. При вызове функции можно дополнить запрос полем, которое будет перенесено в ответ!

Содержимое этого поля может быть любым — числовым, строковым или даже словарем. TDLib перенесет его в ответ как есть:

// Запрос
{
    "@type": "getUser",
    "user_id": 777000,
    "@extra": {
        "request_id": "4a05c088-525f-4464-a501-017f1060fcc5"
    }
}

// Ответ
{
    "@type": "error",
    "code": 404,
    "message": "Not Found",
    "@extra": {
        "request_id": "4a05c088-525f-4464-a501-017f1060fcc5"
    }
}


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

  1. Побуждает TDLib сгенерировать объекты updateChat по одному на каждый чат. При этом обновления (update) неотличимы от обычных обновлений.
  2. После генерации обновлений возвращается результат работы — объект ok или error. Только этот ответ содержит поле @extra.


Заключение


Сейчас существует множество проектов, «улучшающих» взаимодействие с Telegram. Среди них — готовые юзерботы, которые можно расширять до «суровых» фреймворков. Интересно, как получить безграничную свободу в работе с Telegram API? Тогда следите за обновлениями в нашем блоге на Хабре!

Другие статьи по теме


© Habrahabr.ru