Сперва я рассказывал простые вещи о 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? Тогда следите за обновлениями в нашем блоге на Хабре!
Kenya-West
Извините, что на правах оффтопа. Статья хорошая, просто подгорело немного с ToS Телеграма.
Я бы в список добавил:
Обязательное внедрение поддержки Premium и возможности его оплаты. Без роялти, просто добавь кнопку и иди на**й;
Обязательная мимикрия UI официальных приложений Telegram в той же "весовой категории", то есть той же платформы;
Обязательная мимикрия всех фич официального приложения (завезли углубленное управление кэшем - будь добр, реализуй) в течение трех месяцев;
Обязательный перенос доски с issue на bugs.telegram.org и интеграция с ней в т. ч. в стороннем клиенте. Какая же у них доска дерьмовая, если честно...
Отсылка артефактов релизов в том числе и на серверы Telegram.
Паша, спасибо за то, что по-тихой душишь Unigram!
ohno1052
а не пойти ли им куда подальше, что мешает сохдавает новые аккаунты и снова получать доступ к api, или даже автоматизировать этот процесс? например клиент, который реализует эти самые запрещенные возможности/удаляет рекламу и пр. и работает по подписке, на деньги от которой покупаются телефоны для новых аккаунтов
Firemoon Автор
Спамеры, скорее всего, именно так и делают: где-то находят номера телефонов, покупают премиум, чтобы забанили не сразу, и спамят. Отличная тактика, когда аккаунт не жалко.
Если вы будете создавать кучу аккаунтов, чтобы получить APP_ID для вашего приложения, которое нарушает ToS и из-за этого отлетает в бан, то команда Telegram, кажется, может отследить, что приложением пользуется какой-то конкретный основной аккаунт и забанят «основу».
Если, конечно, я правильно понял Вашу мысль.
grishkaa
Ага, особенно если ходить под api_id одного из официальных приложений. Я искренне не понимаю, зачем вообще свой получать, давая им техническую возможность забанить твоё приложение в случае чего.
С Павлом я знаком лично, но его одержимость искусственными клиентскими ограничениями как, по его мнению, формой приватности — это достаточно новый феномен, который всех бесит. Из самого бесячего:
Запрет на скриншоты, сохранение фото/видео и копирование текста
Удаление сообщений и чатов у собеседника, да так, что от них вообще следа не остаётся
"Самоуничтожающиеся" фотки и видео
Скрытие last seen и статуса тайпинга в списке чатов
Если в чате включен "медленный режим", тебе в принципе не будет давать отправить сообщение до истечения таймера, хотя можно было бы положить его в очередь и отправить автоматически, когда таймер истечёт
У меня в идеях для будущих проектов есть современный универсальный десктопный клиент для мгновенных сообщений. Всю эту дурь (pun intended) он поддерживать демонстративно не будет.
hMartin
Соглашусь.
Делаешь публичную апиху и навешиваешь десяток ограничений. Чувак, у тебя в МТПрото нет описания реализации UX/UI, так что или крестик или трусы.
Хочется, чтоб в США случился кейс на тему приватных/публичных АПИ, как было со скраппингом. Чтоб владельцы бренда не могли выпиливать кастомные тулзы из Гитхаба/Сторов на основании того, что якобы это как-то нарушает их права.
grishkaa
Как минимум в России и в некоторых других странах есть явно прописанное в законе об авторских правах исключение, разрешающее adversarial interoperability. То есть, что можно без согласия владельца отреверсить протокол или формат файла, если тебе это нужно для обеспечения совместимости. Но конкретно гитхаб и сторы американские, и это проблема.
hMartin
Ага, поэтому я и говорю, что надо чтоб кейс был в США. Тогда это как бы автоматически станет фичей везде, из-за того что многие таргетятся на их рынок.