«Клиентов нужно не искать, а создавать»: погружение в Telegram API через TDLib
Сперва я рассказывал простые вещи о Telegram Bot API и делал интересных ботов — виртуальную подругу и друга для заказа шавермы. Затем коснулся тестовых серверов и юзерботов. И наконец, пришла пора заглянуть глубже — узнать, как сделать свой клиент для Telegram. Что такое TL-схема и TDLib? Об этом мы сегодня и узнаем.
Данная статья не только поможет тем, кто решил написать свой клиент для Telegram, но и немного расширит кругозор остальным: MTProto — это не приевшийся JSON API. Добро пожаловать под кат!
Готовы показать свои знания в IT? Примите участие в IT-кроссворде Selectel, выиграйте 10 000 рублей на аренду серверов и эксклюзивный мерч Selectel.
Прежде чем мы начнем
Telegram поощряет, ну или по крайней мере не наказывает, за разработку пользовательских клиентов. Для создания клиента необходимо придерживаться следующих правил:
- Необходимо использовать свой уникальный APP_ID.
- Необходимо следовать правилам безопасности.
- Можно расширять функциональность Telegram, но нельзя заставлять пользователей других приложений переходить в ваше приложение.
- Нельзя нарушать базовые механики мессенджера, например, делать «невидимки» и «нечитайки».
- Нельзя выполнять действия без ведома пользователя, например, автоматически подписываться на канал или рассылать сообщения.
- Если клиент обеспечивает доступ к каналам, то необходимо также реализовать функциональность «спонсированных сообщений».
- Нельзя выдавать приложение за официальное.
- Монетизировать можно любым легальным способам, если о нем написано на странице приложения.
Нарушение этих правил приведет к предупреждению, а его игнорирование — к отключению API для вашего приложения. Также команда Telegram может запросить удалить ваше приложение из магазинов.
Также не стоит забывать, что деструктивные действия, такие как спам и распространение незаконного контента, вне зависимости от клиента являются нарушением и могут привести к бану аккаунта.
Готовые решения
Когда смотришь на количество «фич» в современном Telegram-клиенте, невольно представляешь себе поезд прогресса, у которого отказали тормоза. Быстро написать «с нуля» что-то сравнимое по функциональности с официальными клиентами практически невозможно. Поэтому энтузиасты делают то, что официальный Telegram не дает. В первую очередь вспоминаются юзерботы — их можно разделить на две категории.
Классические боты в «шкуре» пользователя
У обычных ботов в Telegram очень мало прав. У них по умолчанию нет возможности посмотреть историю сообщений в чате, даже если есть разрешение на доступ к переписке.
За доступ к истории чатов придется «заплатить» — вы не сможете создавать кнопки и использовать особые сообщения, например, выставлять счета. Нехорошие люди используют такой вид ботов для спама в публичных чатах.
Автоматизация действий пользователя
В этом варианте «бот» — это программа, которая использует основной аккаунт пользователя и реализует функциональность, недоступную в официальном приложении. Например, есть расширение PMPermit, которое автоматически отправляет в черный список незнакомцев, которые вам пишут.
Использование такого вида ботов — интересный процесс. Пользователь пишет команду в чат, где хочет выполнить действие. Сообщение отправляется на серверы Telegram, а оттуда «прилетает» обновлением в «клиент» юзербота. Бот удаляет сообщение-команду из чата и выполняет заданное действие.
На GitHub есть много юзербот-проектов, но большинство из них на Python и используют фреймворки Telethon или Pyrogram. Для реализации своих задумок обычно достаточно использовать готового юзербота или написать личного на указанных фреймворках.
Функции и классы обычно имеют исчерпывающую документацию, которая доступна для среды разработки. Но почему у проектов такая хорошая документация? Пришла пора поговорить о TL-схемах.
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
, которая делает следующее:
- Побуждает TDLib сгенерировать объекты
updateChat
по одному на каждый чат. При этом обновления (update) неотличимы от обычных обновлений. - После генерации обновлений возвращается результат работы — объект
ok
илиerror
. Только этот ответ содержит поле@extra
.
Заключение
Сейчас существует множество проектов, «улучшающих» взаимодействие с Telegram. Среди них — готовые юзерботы, которые можно расширять до «суровых» фреймворков. Интересно, как получить безграничную свободу в работе с Telegram API? Тогда следите за обновлениями в нашем блоге на Хабре!