Документація tg_client
Цей документ описує всі модулі пакету tg_client, окрім директорії
graphQL/. У центрі уваги — робота з TDLib, керування сесіями userbot-ів,
авторизація, взаємодія з Redis/WebSocket та сервіси для діалогів і медіа.
1. Огляд архітектури
Пакет складається з кількох горизонтальних шарів:
- Моделі описують userbot-ів і кешовані кастомні емодзі.
- _tg_utils містить конфігурацію TDLib та менеджер сесій.
- auth реалізує WebSocket-флоу для авторизації userbot-а.
- dialogs — основний стек для роботи з чатами: сервіси, обробники оновлень, лістенери та WebSocket-консюмери.
Основні залежності
- TDLib (через
python-telegram) - Redis для шини подій/команд
- Django ORM + Channels
- S3/MinIO для медіафайлів
Ключові сервіси
TDLibHelper— фасад для APISessionManager— контроль сесійTDLibDialogService— парсер історії- WS-консюмери для auth та списку чатів
2. Структура директорій
| Каталог | Призначення |
|---|---|
tg_client/models.py |
Моделі UserBot та CustomEmoji. |
tg_client/_tg_utils/ |
Конфіг TDLib (config.py) і SessionManager для блокувань та життєвого циклу клієнтів. |
tg_client/auth/ |
Channels-консюмер TDLibAuthConsumer для проходження login/code/password. |
tg_client/dialogs/services/ |
Бізнес-логіка: TDLibHelper, парсер повідомлень, збереження медіа, хелпер UTF-16. |
tg_client/dialogs/handlers/ |
Обробка updateNewMessage з TDLib та пуш у Redis. |
tg_client/dialogs/listeners/ |
Головний лістенер, що стартує userbot-клієнти, слухає Redis-команди й шле відповіді. |
tg_client/dialogs/ws/ |
WebSocket API для фронтенду (наприклад, list_chats/consumers.py). |
3. Моделі Django
UserBot
- Зберігає
phone,api_id,api_hash,session_path,session_strта статусis_authenticated. - Пов'язаний із
accounts.UserProfileтаcompany.Company. - Поля
tg_nickname,username,premiumоновлюються після авторизації. - Метод
save()автогенеруєsession_pathяк/app/session_accounts/<pk>_<phone>/.
CustomEmoji
- Кешує кастомні емодзі (JSON Lottie або WEBM) для повторного використання.
- Використовується
TDLibHelperпід час форматування тексту.
4. _tg_utils
config.py
- Визначає шлях до
libtdjson.so,API_ID,API_HASHтаdatabase_encryption_key. DEV_MODEдозволяє жорстко задати тестові значення; у проді всі секрети тягнуться черезget_secret().
session_manager.py
SessionManagerгарантує, що лише один процес працює з TDLib-сесією.- Метод
get_client()— асинхронний контекст, що блокує сесію, створюєTelegram-клієнт, повертає його й потім безпечно відключає (client.stop()). create_new_session()ініціює клієнт для авторизації (використовується у WebSocket auth).check_session_health()запускає quick health-check (get_me()).- Функція
get_session_manager()забезпечує сінглтон менеджера.
5. WebSocket-авторизація (tg_client/auth)
TDLibAuthConsumer керує повним циклом логіну userbot-а:
- start — ініціалізує
SessionManager, створює TDLib-клієнт, запускаєlogin(blocking=False). - send_code — передає код підтвердження (AuthorizationState.WAIT_CODE).
- send_password — вводить 2FA (AuthorizationState.WAIT_PASSWORD).
- Після
READYвитягує профіль черезget_me(), оновлюєUserBot, закриває сесію.
Комунікація завжди ведеться через JSON-повідомлення Channels, Redis тут не залучений.
Ендпойнт WebSocket: wss://<host>/ws/tg-auth/ (див. tg_client/routing.py).
Приклади клієнтських запитів
1. Старт авторизації:
{
"action": "start",
"userbot_id": 12,
"phone": "+380955870336"
}
2. Передача одноразового коду (стає доступною після події code_required):
{
"action": "send_code",
"code": "12345"
}
3. Введення 2FA-пароля (коли бекенд надіслав password_required):
{
"action": "send_password",
"password": "my-secret"
}
Приклади відповідей сервера
{"type": "connected", "message": "WebSocket connected"}
{"type": "status", "message": "Connecting to Telegram..."}
{"type": "code_required", "message": "Confirmation code sent"}
{"type": "authorized", "message": "Authorization completed", "username": "akva_bot", ...}
Після закриття з'єднання сесія примусово завершується, тому повторні спроби починаються з нового start.
6. Сервіси діалогів
TDLibHelper (dialogs/services/tg_requests.py)
- Уніфікує виклики TDLib через
tg_call()(повертає{ok, result, error}). - Містить кешовані методи
get_me(),get_chat(),get_user(). get_chats(limit, offset_order, offset_chat_id, chat_list)підтримує пагінацію;chat_listопційно дозволяє отримати архів/папку;build_chat_item()повертаєorder(рядок) іpositions.get_history(),get_message(),view_messages(),send_message(),forward_message(),reply_message(),get_contacts()охоплюють усі основні операції.- Блок MEDIA завантажує файли через TDLib, передає їх у
save_media(), повертає presigned URL або метадані. - Методи для преміум-емодзі:
get_full_info_premium_emoji(),get_premium_emoji_asset(), форматування тексту з кастомними емодзі (formated_msg(),utf16_index_to_py_index()). send_ws()— хелпер для пушу відповіді у Redis-каналchat_list_updates.
TDLibDialogService
- Фасад
get_dialog()тягне історію, дані чату та парсить повідомлення. - Позначає вхідні повідомлення прочитаними через
helper.view_messages(). - Витягує дані відправника, ріплаїв, пересилань, обробляє photo/emoji/unsupported контент.
- Повертає вже відформатований payload для фронтенду.
save_to_cloud.py
save_media()— завантаження локального файлу у MinIO/S3 черезdefault_storage.generate_presigned_url()створює короткоживуче посилання на об'єкт.
utf_index_to_py_index.py
Конвертує UTF-16 індекси (які повертає TDLib) у Python-індекси, щоб коректно працювати з емодзі/сурогатами.
7. Лістенери та обробники
main_listener.py
start_userbot_listener()відкриває TDLib-клієнт черезSessionManager, реєструєupdateNewMessageта запускаєlisten_for_commands().listen_for_commands()слухає Redis-каналtg_commands, приймає payload та виконує команди черезTDLibHelper.- Підтримувані команди:
get_chats,search_user,get_user,open_dialog(опц.message_thread_id),send_message,forward_message,reply_message,edit_message,pin_message,unpin_message,download_file,archive_chat,unarchive_chat,delete_chat,leave_chat,chat_in_folder,pin_chat,import_contacts,get_prem_path. - Результати надсилаються у
chat_list_updatesчерезsend_ws(), щоб їх підхопив WebSocket-клієнт.
handle_new_update.py
- Прямий обробник
updateNewMessage, що приходить із TDLib. - Використовує
TDLibDialogService.parse_message(), щоб отримати повний опис повідомлення (ID, текст, ентиті, reply/forward/медіа). - Публікує результат у
chat_list_updatesразом ізuserbot_idтаunread_count, тож push-події мають такий самий формат, як і історіяopen_dialog.
8. WebSocket API для діалогів
TDLibListChatsConsumer
- Після
open_clientпідписується наchat_list_updatesі може одночасно слухати кілька userbot-ів черезuserbot_ids(масив) або одиночнийuserbot_id. get_chats→ відправляє команду у Redis зlimit,offset_order,offset_chat_id, опц.chat_list(наприклад{"@type":"chatListArchive"}або рядок"chatListArchive").search_user,get_user,open_dialog,get_prem_path— тонкі проксі в Redis.open_dialogзmessage_thread_idвідкриває історію конкретного топіка; безmessage_thread_idу форумних чатах повертає список топіків у форматі повідомлень (is_topic,message_thread_id,topic_name,topic_icon_emoji_id).send_message— базове відправлення тексту.forward_message— пересилає повідомлення між чатами без редагування контенту.delete_messages— видаляє повідомлення в чаті ("revoke": false -- > видаляє для себе, true -- > для всіх).reply_message— створює відповідь із посиланням наreply_to_message_id.edit_message— редагує текст повідомлення заmessage_id.pin_message/unpin_message— прикріплює або відкріплює повідомлення в чаті.download_file— завантажує файл заfile_id(потрібен для якісніших фото).archive_chat/unarchive_chat— переносить чат в архів або повертає у головний список.delete_chat— видаляє історію та прибирає чат зі списку (опц.remove_from_chat_list,revoke).leave_chat— вихід з чату/каналу заchat_id.chat_in_folder— керує станом чату в папці (added/removed/pin/unpin).pin_chat— пін/анпін чату в заданому списку (опц.chat_list, дефолт — main).import_contacts— викликаєTDLibHelper.get_contacts(), повертає результат у відповідному WS-івенті.- Відсіює повідомлення за
userbot_id, щоб користувач отримував лише свій стрім. - Медіа приходять поступово через подію
media_ready(файл/кастомні емодзі/іконка топіка), щоб UI міг оновлюватись без блокування основного потоку.
| Action (WS) | Очікувані поля | Результат із Redis |
|---|---|---|
get_chats |
userbot_id, опц. limit, offset_order, offset_chat_id, chat_list |
Послідовність {type: "get_chats", payload: chat_item} |
open_dialog |
chat_id, from_message_id, опц. message_thread_id, опц. include_pinned, опц. pinned_limit |
Потік повідомлень (message) + pinned_message + dialog_end + media_ready; для форумів без message_thread_id повертає топіки |
send_message |
chat_id, text |
send_message або send_message_error |
get_prem_path |
emoji_id |
Посилання на лотті/відео кастомного емодзі |
forward_message |
to_chat_ids, from_chat_id, message_ids |
forward_message або forward_message_error |
delete_messages |
chat_id, from_chat_id, message_ids, revoke |
delete_messages або delete_messages_error |
reply_message |
chat_id, reply_to_message_id, text |
reply_message або reply_message_error |
edit_message |
chat_id, message_id, text |
edit_message або edit_message_error |
pin_message |
chat_id, message_id, опц. disable_notification, only_for_self |
pin_message або pin_message_error |
unpin_message |
chat_id, message_id |
unpin_message або unpin_message_error |
download_file |
file_id, опц. source, chat_id, msg_id, message_thread_id, extra |
download_file або download_file_error + media_ready |
archive_chat |
chat_id |
archive_chat або archive_chat_error |
unarchive_chat |
chat_id |
unarchive_chat або unarchive_chat_error |
delete_chat |
chat_id, опц. remove_from_chat_list, опц. revoke |
delete_chat або delete_chat_error |
leave_chat |
chat_id |
leave_chat або leave_chat_error |
chat_in_folder |
chat_id, chat_folder_id, опц. added, removed, pin, unpin (дефолт added=true) |
chat_in_folder або chat_in_folder_error |
pin_chat |
chat_id, опц. is_pinned, опц. chat_list |
pin_chat або pin_chat_error |
import_contacts |
userbot_id, phone |
import_contacts або import_contacts_error |
Події з Redis
message— основний payload повідомлення або топіка (у форматі повідомлення).pinned_message— закріплені повідомлення з чату (якщо from_message_id==0 і message_thread_id==None).dialog_end— ознака завершення відвантаження історії.media_ready— поступове дозавантаження медіа:{kind: "file"|"custom_emoji", file_id|emoji_id, path|emoji_json|emoji_webm, chat_id, msg_id, message_thread_id}.dialog_error— помилка під часopen_dialog.user_status— оновлення онлайна:{user_id, status, last_seen}.chat_action— typing/recording:{chat_id|user_id, message_thread_id, sender_id, action, progress}.chat_position— зміна ордеру/позицій:{chat_id, order|position|positions}.
Приклади payload-ів
/ws/chats/
{"action": "open_client", "userbot_ids": [1, 2], "user_id": 1}
{"action": "get_chats", "userbot_id": 1, "user_id": 1}
{"action": "get_chats", "userbot_id": 1, "chat_list": "chatListArchive"}
{"action": "open_dialog", "userbot_id": 1, "from_message_id": "0", "chat_id": "123"}
{"action": "open_dialog", "userbot_id": 1, "from_message_id": "0", "chat_id": "123", "message_thread_id": "456"}
// Відправити повідомлення
{"action": "send_message", "userbot_id": 1, "chat_id": "123", "text": "Привіт"}
// Переслати повідомлення
{"action": "forward_message", "userbot_id": 1, "to_chat_ids": ["488386685", "456456685"], "from_chat_id": "2123445553", "message_ids": ["984071798784", "1231231231"]}
// Видалити повідомлення в чаті
{"action": "delete_messages", "userbot_id": 1, "chat_id": "456456685", "message_ids": ["984071798784"], "revoke": false}
// Reply на конкретне повідомлення
{"action": "reply_message", "userbot_id": 1, "chat_id": "2123445553", "reply_to_message_id": "984121081856", "text": "Відповідаю"}
// Редагувати повідомлення
{"action": "edit_message", "userbot_id": 1, "chat_id": "2123445553", "message_id": "984121081856", "text": "Оновлений текст"}
// Прикріпити повідомлення + only_for_self: true (повідомлення буде прикріплене лише для вас)
{"action": "pin_message", "userbot_id": 1, "chat_id": "2123445553", "message_id": "984121081856"}
// Відкріпити повідомлення
{"action": "unpin_message", "userbot_id": 1, "chat_id": "2123445553", "message_id": "984121081856"}
// Завантажити файл за file_id (наприклад, високу якість фото)
{"action": "download_file", "userbot_id": 1, "file_id": 123456, "chat_id": "2123445553", "msg_id": "984121081856"}
// Архівувати чат
{"action": "archive_chat", "userbot_id": 1, "chat_id": "123"}
// Повернути чат з архіву
{"action": "unarchive_chat", "userbot_id": 1, "chat_id": "123"}
// Видалити чат (прибрати зі списку) revoke = true (видалити історію для всіх), якщо remove_from_chat_list false, то просто очистить історію, але залишить чат у списку
{"action": "delete_chat", "userbot_id": 1, "chat_id": "123", "remove_from_chat_list": true, "revoke": true}
// Вийти з каналу/чату
{"action": "leave_chat", "userbot_id": 1, "chat_id": "123"}
// Додати чат у папку (added дефолтно true)
{"action": "chat_in_folder", "userbot_id": 1, "chat_id": "123", "chat_folder_id": 35}
// Видалити чат з папки (added вказувати false, removed=true)
{"action": "chat_in_folder", "userbot_id": 1, "chat_id": "123", "chat_folder_id": 35, "removed": true}
// Пін чату в папці (added можна не вказувати)
{"action": "chat_in_folder", "userbot_id": 1, "chat_id": "123", "chat_folder_id": 35, "pin": true}
// Анпін чату в папці (added можна не вказувати)
{"action": "chat_in_folder", "userbot_id": 1, "chat_id": "123", "chat_folder_id": 35, "unpin": true}
// Пін/анпін чату в списку (дефолтно main)
{"action": "pin_chat", "userbot_id": 1, "chat_id": "123", "is_pinned": true}
9. Потік даних
Команди від фронтенду
- React/SPA шле action у WebSocket (
dialogs/ws). - WS-консюмер публікує JSON у Redis-канал
tg_commands. listen_for_commands()читає повідомлення, викликаєTDLibHelper.- Результат (або помилка) повертається через
send_ws()у каналchat_list_updates. - WS-консюмер відправляє payload назад клієнту.
Push-оновлення
- TDLib кидає
updateNewMessage. handle_new_updateпарсить повідомлення черезTDLibDialogServiceта формує payload із тими ж полями, що повертаєopen_dialog.- Подія з
userbot_id,unread_countта деталями месиджу публікується уchat_list_updatesй летить на фронт. - Якщо у повідомленні є файл/кастомне емодзі/іконка топіка, окремо приходить
media_readyз метаданими (поступова доставка медіа).
10. Типові сценарії
Пагінація чатів
Беріть order і chat_id з останнього елемента відповіді. Передавайте їх як
offset_order/offset_chat_id у наступному get_chats, аби TDLib видав наступний блок.
Завантаження медіа
TDLibHelper.download_file() викликає downloadFile+getFile, чекає завершення, зберігає у S3/MinIO та повертає presigned URL. Встановіть return_meta=True, якщо потрібно локальний шлях.
Парсинг історії
TDLibDialogService.get_dialog() приймає chat_id та from_message_id. Якщо історія порожня, поверне is_empty=True + дані чату.
Робота з кастомними емодзі
При першому використанні емодзі helper завантажує asset, зберігає у CustomEmoji, повертає JSON/WebM. Повторні звернення беруться з БД.
11. Рекомендації з розширення
- Нові команди достатньо додати у
listeners/main_listener.py(обробка) та у відповідний WS-консюмер. - Тримайте
SessionManagerєдиною точкою входу: не створюйтеTelegramнапряму. - GraphQL-шар може повторно використовувати ці сервіси; достатньо звертатись до нього як до внутрішнього API.