Сперва я рассказывал простые вещи о 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-схемах.


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? Тогда следите за обновлениями в нашем блоге на Хабре!

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


Комментарии (7)


  1. Kenya-West
    02.11.2023 20:01

    Извините, что на правах оффтопа. Статья хорошая, просто подгорело немного с ToS Телеграма.

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

    Я бы в список добавил:

    1. Обязательное внедрение поддержки Premium и возможности его оплаты. Без роялти, просто добавь кнопку и иди на**й;

    2. Обязательная мимикрия UI официальных приложений Telegram в той же "весовой категории", то есть той же платформы;

    3. Обязательная мимикрия всех фич официального приложения (завезли углубленное управление кэшем - будь добр, реализуй) в течение трех месяцев;

    4. Обязательный перенос доски с issue на bugs.telegram.org и интеграция с ней в т. ч. в стороннем клиенте. Какая же у них доска дерьмовая, если честно...

    5. Отсылка артефактов релизов в том числе и на серверы Telegram.

      Паша, спасибо за то, что по-тихой душишь Unigram!


    1. ohno1052
      02.11.2023 20:01

      а не пойти ли им куда подальше, что мешает сохдавает новые аккаунты и снова получать доступ к api, или даже автоматизировать этот процесс? например клиент, который реализует эти самые запрещенные возможности/удаляет рекламу и пр. и работает по подписке, на деньги от которой покупаются телефоны для новых аккаунтов


      1. Firemoon Автор
        02.11.2023 20:01

        Спамеры, скорее всего, именно так и делают: где-то находят номера телефонов, покупают премиум, чтобы забанили не сразу, и спамят. Отличная тактика, когда аккаунт не жалко.

        Если вы будете создавать кучу аккаунтов, чтобы получить APP_ID для вашего приложения, которое нарушает ToS и из-за этого отлетает в бан, то команда Telegram, кажется, может отследить, что приложением пользуется какой-то конкретный основной аккаунт и забанят «основу».

        Если, конечно, я правильно понял Вашу мысль.


    1. grishkaa
      02.11.2023 20:01
      +1

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

      Ага, особенно если ходить под api_id одного из официальных приложений. Я искренне не понимаю, зачем вообще свой получать, давая им техническую возможность забанить твоё приложение в случае чего.

      С Павлом я знаком лично, но его одержимость искусственными клиентскими ограничениями как, по его мнению, формой приватности — это достаточно новый феномен, который всех бесит. Из самого бесячего:

      • Запрет на скриншоты, сохранение фото/видео и копирование текста

      • Удаление сообщений и чатов у собеседника, да так, что от них вообще следа не остаётся

      • "Самоуничтожающиеся" фотки и видео

      • Скрытие last seen и статуса тайпинга в списке чатов

      • Если в чате включен "медленный режим", тебе в принципе не будет давать отправить сообщение до истечения таймера, хотя можно было бы положить его в очередь и отправить автоматически, когда таймер истечёт

      У меня в идеях для будущих проектов есть современный универсальный десктопный клиент для мгновенных сообщений. Всю эту дурь (pun intended) он поддерживать демонстративно не будет.


      1. hMartin
        02.11.2023 20:01
        +1

        Соглашусь.

        Делаешь публичную апиху и навешиваешь десяток ограничений. Чувак, у тебя в МТПрото нет описания реализации UX/UI, так что или крестик или трусы.

        Хочется, чтоб в США случился кейс на тему приватных/публичных АПИ, как было со скраппингом. Чтоб владельцы бренда не могли выпиливать кастомные тулзы из Гитхаба/Сторов на основании того, что якобы это как-то нарушает их права.


        1. grishkaa
          02.11.2023 20:01
          +1

          Как минимум в России и в некоторых других странах есть явно прописанное в законе об авторских правах исключение, разрешающее adversarial interoperability. То есть, что можно без согласия владельца отреверсить протокол или формат файла, если тебе это нужно для обеспечения совместимости. Но конкретно гитхаб и сторы американские, и это проблема.


          1. hMartin
            02.11.2023 20:01

            Ага, поэтому я и говорю, что надо чтоб кейс был в США. Тогда это как бы автоматически станет фичей везде, из-за того что многие таргетятся на их рынок.