Compare commits

..

89 Commits

Author SHA1 Message Date
zavolo 7d2e070d1f fix(chat history): фикс 2026-05-11 00:47:04 +03:00
zavolo 24b0123185 fix(chat history): фикс 2026-05-11 00:38:04 +03:00
zavolo 31844c7fa2 fix(chat history): фикс 2026-05-11 00:26:31 +03:00
zavolo 9b60b15538 fix(chat history): фикс 2026-05-11 00:11:24 +03:00
zavolo 0d91f6542e fix(chat history): фикс 2026-05-10 23:39:08 +03:00
zavolo 77d6ca8cc0 fix(chat history): фикс 2026-05-10 23:27:13 +03:00
zavolo 3bf8bc5770 fix(chat history): фикс 2026-05-10 23:21:07 +03:00
zavolo 861b75eb1c MAX: bootstrap-история в LOGIN — клиент перестал думать что всё уже синканулось
В ответе LOGIN сервер слал messages: {} и chatMarker: 0. Десктопный
клиент в этом случае считает, что локальная история уже синхронизирована
со старого запуска, и НЕ отправляет CHAT_HISTORY (49) при открытии чата.
В окне видно только lastMessage из chats[], а вся реальная переписка —
ничерта.

- src/common/tools.py: collect_bootstrap_history(chatIds, ...) —
  собирает карту {chatId: [последние N сообщений]}, в т.ч. избранное
  под клиентским id = senderId ^ senderId.
- src/oneme/processors/auth.py: подсовываем эту карту в
  payload.messages, chatMarker = текущее время вместо 0.
2026-05-10 22:27:42 +03:00
zavolo fa0ed34adc MAX: история таки заработала — cid/link/reactionInfo обязательны в схеме
Десктопный MAX подключается через TCP (mobile-протокол) и парсит
msgpack по фиксированной схеме. Если в сообщении выпадает любое из
полей — клиент молча обрывает соединение. После 87cfc19 как раз
такие условные `if elements: ...` / `if link: ...` (а link и
reaction_info там всегда были `{}`, то есть falsy) вырезали поля
из ответа CHAT_HISTORY и MSG_SEND, чем и сломали историю.

- src/common/tools.py: новый build_message_dict() — единая сборка
  тела сообщения, где все поля (id, cid, time, type, sender, text,
  attaches, elements, reactionInfo, link) присутствуют ВСЕГДА.
  get_last_message переписан через него.
- src/oneme/processors/history.py: chat_history использует
  build_message_dict вместо ручной логики с условными if-ками.
- src/oneme/processors/messages.py: msg_send.bodyMessage теперь
  отдает cid / reactionInfo / link даже пустыми и приводит id
  к int для mobile, str для web.

Цепная польза: auth.py LOGIN bootstrap (через generate_chats →
get_last_message) и search.py тоже теперь шлют корректную схему.
2026-05-10 22:17:18 +03:00
Alexey Polyakov 87cfc1932e Попытка починить историю (спойлер, нихуя не получилось) 2026-05-10 19:04:51 +03:00
Alexey Polyakov 17245f44d0 Фикс сборки избранного 2026-05-09 18:55:49 +03:00
Alexey Polyakov b1a37bfa24 update sql scheme 2026-05-09 18:54:53 +03:00
Alexey Polyakov d81eec5532 Генерируем айди пользователей рандомно (опять, да) 2026-05-09 18:16:32 +03:00
Alexey Polyakov ddb810589f MAX: исправление уязвимости с избранными 2026-05-09 17:55:39 +03:00
Alexey Polyakov dff6937da8 MAX: добавление контакта по номеру телефона 2026-05-09 15:50:47 +03:00
Alexey Polyakov ac40cc53c9 MAX && TT: обновление контакта 2026-05-08 16:15:33 +03:00
Alexey Polyakov 756956d8a0 TT: тоже самое что в макс 2026-05-08 16:04:03 +03:00
Alexey Polyakov 00071c80be oops 2026-05-08 15:58:29 +03:00
Alexey Polyakov a045457128 MAX: блокировка и разблокировка контакта 2026-05-08 15:57:24 +03:00
Alexey Polyakov 4d51c70f8e Вынес sqlite в отдельный модуль 2026-05-08 15:27:05 +03:00
Alexey Polyakov 2d3b9285bf MAX: теперь для избранного не сравниваем айди с нулём 2026-05-08 15:24:49 +03:00
Alexey Polyakov 6bb0d52419 nothing 2026-05-06 21:27:01 +03:00
Alexey Polyakov 911008c0a1 oops 2026-05-06 20:46:22 +03:00
Alexey Polyakov b8472821eb MAX: web fix 2 2026-05-06 20:42:33 +03:00
Alexey Polyakov f1c1639d9f MAX: web fix 2026-05-06 20:33:09 +03:00
Alexey Polyakov 7426e83914 nothing 2026-05-06 19:34:14 +03:00
Alexey Polyakov 8dc3ef1731 MAX: Почистил серверный конфиг 2026-05-06 17:40:11 +03:00
Alexey Polyakov f1ff4fd062 MAX && TT: общение в таме, и корректировки под веб морду 2026-05-06 15:58:27 +03:00
Alexey Polyakov 0b6eda6178 TG Bot: fix username 2026-05-06 15:53:14 +03:00
Alexey Polyakov 02df98cdbd TG Bot: fix 2026-05-06 15:43:30 +03:00
Alexey Polyakov 49d73200b0 fix 2026-05-06 15:39:22 +03:00
Alexey Polyakov 389a08ebce nothing 2026-05-06 15:35:23 +03:00
Alexey Polyakov 613e1b96cd Решение проблемы обработки sigterm 2026-05-06 15:25:07 +03:00
Alexey Polyakov 0f2d946b98 TG Bot: возможность включения белого списка 2026-05-06 00:51:56 +03:00
Alexey Polyakov 1ff974dfce oops 2026-05-05 23:08:02 +03:00
Alexey Polyakov bcd94b3a57 TT: ну вроде шире поддержка, а вообще обратная совместимость с максом клас 2026-05-05 23:06:50 +03:00
Alexey Polyakov 89f1fefa31 MAX & TT: теперь в качестве страны локации используется настоящая страна пользователя, а также зафиксировал версии библиотек в зависимостях 2026-04-28 18:22:16 +03:00
Alexey Polyakov c716520ca4 MAX: добавление/удаление контактов, статусы 2026-04-28 06:56:29 +03:00
Alexey Polyakov ff46e417f4 MAX: oops 2026-04-27 17:41:51 +03:00
Alexey Polyakov bd95755db4 MAX: создание папок 2026-04-27 17:40:28 +03:00
Alexey Polyakov cb058f6ab6 Теперь ID пользователей идут по порядку, как и раньше 2026-04-25 12:10:44 +03:00
Alexey Polyakov cd9ba981ae MAX: отдача черного списка 2026-04-24 23:31:55 +03:00
Alexey Polyakov 810d480dbd MAX: фикс версий < 25.8.0 2026-04-24 21:30:02 +03:00
Alexey Polyakov 227f90c3c3 MAX: Рефактор папок 2026-04-24 20:54:28 +03:00
Alexey Polyakov 56133416e3 MAX: пуши через firebase (особо не тестил, вроде работает) 2026-04-24 19:46:08 +03:00
Alexey Polyakov 35a4101608 MAX: обновление настроек приватности 2026-04-24 17:17:33 +03:00
Alexey Polyakov 9fcba1af86 MAX: Рабочие баннеры 2026-04-24 15:51:01 +03:00
Alexey Polyakov 4abe6de885 oops 2026-04-20 22:25:38 +03:00
Alexey Polyakov 4121bd0e1d MAX: заглушка для баннеров, правка пакета со списком жалоб, отдача контактов и прочие улучшения 2026-04-20 22:22:02 +03:00
Aleksandr Kosachev d9798a6fc6 Fix OpenMAX mobile compatibility and Telegram auth fallback (#30)
* Fix OpenMAX mobile compatibility and Telegram auth fallback

* Common: Убрал скобку в конфиге

---------

Co-authored-by: Alexey Polyakov <starwear3000@mail.ru>
2026-04-14 20:05:34 +03:00
Alexey Polyakov d9cbafc4e3 TT: фикс вебсокета 2026-04-12 07:15:09 +03:00
WowInceptionGood 448b854bdb Исправление конфликтов 2026-04-10 18:27:42 +03:00
Alexey Polyakov dcbcb0a600 Common: добавил сжатие lz4 2026-04-10 17:43:35 +03:00
Alexey Polyakov 0ffc649dd9 Начальная реализация транспорта ws для max web и прочие улучшения 2026-04-07 12:36:30 +03:00
Alexey Polyakov 52949602af Common: фикс года 2026-04-03 23:02:49 +03:00
Alexey Polyakov 0f5c06f80c TG Bot: переписал на aiogram 2026-03-30 22:34:09 +03:00
Alexey Polyakov e5607adb9b MAX: Разделил процессоры 2026-03-29 11:04:45 +03:00
Alexey Polyakov 4014bab5c9 Правка контроллеров небольшая 2026-03-27 19:35:59 +03:00
Alexey Polyakov 0b7282b284 TamTam && MAX: история (в мохе она вроде теперь получше работает) 2026-03-27 19:26:20 +03:00
Alexey Polyakov 7a2e5a20d6 TamTam: websocket transport for web version 2026-03-27 19:00:14 +03:00
Alexey Polyakov ac76015d08 Common: поправил 1 прикол 2026-03-27 17:41:09 +03:00
Alexey Polyakov f4ea147a5f MAX: заглушка в 158 опкоде 2026-03-22 19:28:52 +03:00
Alexey Polyakov 5fcc56950f TT: oops 2026-03-22 18:56:50 +03:00
Alexey Polyakov a8732b5d7f ТамТам: починил 32 опкод и добавил серверный конфиг. Избранное больше не добавляется в список чатов
MAX: починил 32 опкод
2026-03-22 18:26:04 +03:00
Alexey Polyakov aa6411b75d ТамТам: обработчик 5 и 1 опкода 2026-03-22 17:09:20 +03:00
Alexey Polyakov f2e7e37592 ТамТам: реализация 32 опкода. Надеюсь, будет работать 2026-03-22 16:59:13 +03:00
Alexey Polyakov 8d7a1dacb5 Теперь мы не должны отправлять 128 опкод самому себе. вроде 2026-03-22 16:51:31 +03:00
Alexey Polyakov ef512b060f Вынес список участников чата в отдельную таблицу 2026-03-21 15:05:38 +03:00
Alexey Polyakov 2cf18b878a Генерируем айди, за вместо того, чтобы писать его попорядку 2026-03-19 23:13:27 +03:00
Alexey Polyakov bbee49d2d8 Коды стран 2026-03-19 16:48:59 +03:00
Alexey Polyakov 9bc6c15d82 Поделил процессоры в таме 2026-03-19 16:21:48 +03:00
Alexey Polyakov 11b2e2748d Удалил tamtam.proto, поскольку нигде больше не используется 2026-03-19 01:16:35 +03:00
Alexey Polyakov 6c05b5f1b5 Швырнул архитектуру, чтобы позже объединить контроллеры веба и сокета в одно, а также разделить процессоры 2026-03-19 01:13:12 +03:00
Alexey Polyakov 2dab853569 Доделал авторизацию в ТамТаме 2026-03-19 00:10:21 +03:00
Alexey Polyakov c7eace4648 Дополнил немного 48 опкод, и починил отдачу истории в избранном (избранное все равно не работает) 2026-03-18 22:16:31 +03:00
Alexey Polyakov 09c1f8b6f6 История сообщений (вроде как даже работает) 2026-03-18 21:55:24 +03:00
Alexey Polyakov 9c3038bc84 Починил изменение профиля 2026-03-18 21:06:33 +03:00
Alexey Polyakov 91453d7173 Серверный конфиг снова сломался 2026-03-18 20:54:59 +03:00
WowInceptionGood d5ea45cb96 SQLite: Ошибка
И вообще не юзайте sqlite пожалуйста, он баганный
2026-03-16 00:11:50 +03:00
zavolo 2d09f52c2e feat: 23 опкод для регистрации, смс шлюз, докер 2026-03-15 13:25:40 -04:00
zavolo d4d5dd5530 feat: 16 опкод для обновления профиля 2026-03-15 11:39:32 +03:00
Alexey Polyakov 81f5fb762f Update deviceId description in tamtam_ws.md 2026-03-12 19:52:38 +03:00
WowInceptionGood 301e55be05 Документация: описал про SESSION_INIT 2026-03-12 16:49:52 +00:00
WowInceptionGood db3b7323d9 Merge branch 'dev/0.1.0' of https://github.com/openmax-server/server into dev/0.1.0 2026-03-12 16:34:45 +00:00
WowInceptionGood 66fb40a1fd Merge branch 'master' into dev/0.1.0 2026-03-12 16:33:29 +00:00
Alexey Polyakov 9004566652 Merge branch 'master' into dev/0.1.0 2026-03-12 19:30:37 +03:00
Alexey Polyakov 07dd71b0ad Теперь все должно работать исправно 2026-03-12 19:25:09 +03:00
Alexey Polyakov 1ec1d49424 Merge branch 'master' into dev/0.1.0 2026-03-11 20:44:19 +03:00
Alexey Polyakov 9034485408 Различные фиксы (#14)
* Исключаем только ошибку валидации

* Небольшие правки вебсокета тамтама

* Теперь номера брутить чутка сложнее

* Авторизация теперь проверяется для некоторых команд

* Теперь проверяется orign у вебсокета тамтама

* Дополнил пример env

* Починил немного сокет тамтама
2026-03-11 15:21:49 +03:00
27 changed files with 222 additions and 899 deletions
-8
View File
@@ -1,8 +0,0 @@
# Патч MAX для IOS
1) Распаковываете IPA
2) Находите файл MAX в Payload/Max.app
3) Открываете hex-редактор и находите api.oneme.ru и меняете на свой
При желании можете поменять другие URL
URL не должен привышать количество символов которое было у изначального URL!
4) Открываете IPA как архив и добавляете патченный файл MAX в Payload/Max.app
+2 -3
View File
@@ -1,8 +1,7 @@
# Навигация по faq # Навигация по faq
## Работа с сервером ## Работа с сервером
- [Установка сервера](install.md) [Установка сервера](install.md)
## Патчинг клиентов ## Патчинг клиентов
- [Патч мобильного клиента для Android](patch_apk.md) [Патч apk](patch_apk.md)
- [Патч мобильного клиента для IOS](patch_ipa.md)
-11
View File
@@ -1,11 +0,0 @@
> [!Caution]
>
> На данной странице представлены инстансы от сторонних разработчиков.
>
> Используйте на свой страх и риск
>
# Открытые сервера
* [JustMAX](https://t.me/justmax_official)
* [mox.nyako.tk](https://t.me/opengrame/296), порт 443 (регистрация по реальному номеру)
+3 -5
View File
@@ -1,6 +1,6 @@
> [!Caution] > [!Caution]
> >
> Проект находится на ранней стадии разработки и, вероятно, полон багов. > Проект находится на ранней стадии разработки и вероятно полон багов.
> >
> Использование в профессиональных средах не рекомендовано. > Использование в профессиональных средах не рекомендовано.
@@ -13,7 +13,7 @@ https://t.me/openmax_alerts
# Требования # Требования
- Python 3.12+ (поддержка версий ниже не гарантирована) - Python 3.12+ (поддержка версий ниже не гарантирована)
- MariaDB, MySQL или SQLite (использование последнего не рекомендуется, так как поддержка ещё в разработке) - MariaDB, MySQL или SQLite
- Уметь патчить клиент MAX или собирать Komet из исходного кода (естественно с заменой сервера) - Уметь патчить клиент MAX или собирать Komet из исходного кода (естественно с заменой сервера)
- Сертификат и приватный ключ X.509 (для тестирования сервера можно сгенерировать самоподписанный: ```openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365```) - Сертификат и приватный ключ X.509 (для тестирования сервера можно сгенерировать самоподписанный: ```openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365```)
@@ -22,6 +22,4 @@ https://t.me/openmax_alerts
Клиент может быть практически любым, главное условие - чтобы он был совместим с официальным сервером (`api.oneme.ru` / `api.tamtam.chat`). Клиент может быть практически любым, главное условие - чтобы он был совместим с официальным сервером (`api.oneme.ru` / `api.tamtam.chat`).
# Дополнительная информация # Дополнительная информация
- [FAQ](faq/readme.md) [Faq](faq/readme.md)
- [Документация проекта](https://github.com/openmax-server/docs)
- [Публичные сервера](faq/servers.md)
-3
View File
@@ -49,9 +49,6 @@ class SQLiteConnectionCompat:
async def __aexit__(self, exc_type, exc, tb): async def __aexit__(self, exc_type, exc, tb):
return False return False
async def commit(self):
await self.connection.commit()
def cursor(self): def cursor(self):
return SQLiteCursorCompat(self.connection) return SQLiteCursorCompat(self.connection)
-7
View File
@@ -15,7 +15,6 @@ class Static:
RATE_LIMITED = "rate_limited" RATE_LIMITED = "rate_limited"
CONTACT_NOT_FOUND = "contact_not_found" CONTACT_NOT_FOUND = "contact_not_found"
CONTACT_ALREADY_ADDED = "contact_already_added" CONTACT_ALREADY_ADDED = "contact_already_added"
CONTACT_BLOCKED = "contact_blocked"
class ChatTypes: class ChatTypes:
DIALOG = "DIALOG" DIALOG = "DIALOG"
@@ -96,12 +95,6 @@ class Static:
"message": "Contact already added", "message": "Contact already added",
"title": "Контакт уже добавлен" "title": "Контакт уже добавлен"
}, },
"contact_blocked": {
"localizedMessage": "Вы не можете написать этому пользователю",
"error": "contact.blocked",
"message": "Contact is blocked",
"title": "Вы не можете написать этому пользователю"
},
} }
### Сообщения бота ### Сообщения бота
+77 -70
View File
@@ -11,7 +11,14 @@ class Tools:
pass pass
def build_message_dict(self, row, protocol_type="mobile"): def build_message_dict(self, row, protocol_type="mobile"):
"""Сборка тела сообщения""" """Унифицированная сборка тела сообщения для отправки клиенту.
Десктоп MAX (TCP, protocol_type='mobile') и официальный
api.oneme.ru ожидают, что в сообщении будут ВСЕГДА присутствовать
поля cid / elements / link / reactionInfo, даже если они пустые.
Любое отсутствие поля приводит к тому, что клиент бросает соединение
при разборе msgpack-схемы (классическая регрессия из коммита 87cfc19).
"""
try: try:
attaches = json.loads(row.get("attaches") or "[]") attaches = json.loads(row.get("attaches") or "[]")
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -21,35 +28,46 @@ class Tools:
except (TypeError, ValueError): except (TypeError, ValueError):
elements = [] elements = []
# Парсер MAX 26.15.3 (defpackage.u6h.Q) ждёт в сообщении следующие
# поля. Отсутствие любого ломает разбор msgpack-схемы, и клиент
# тихо роняет всю историю чата:
# id, cid, chatId, time, type, sender, text, attaches, elements,
# link, reactionInfo, updateTime, status, options
# Список вытащен дизассемблированием Q() через dexdump.
# type — int-enum для разновидности сообщения (0 = обычное text);
# status — int-enum (1 = ACTIVE/доставлено, 0 часто означает REMOVED).
message = { message = {
"id": row.get("id") if protocol_type == "mobile" else str(row.get("id")), "id": row.get("id") if protocol_type == "mobile" else str(row.get("id")),
"cid": int(row.get("cid") or 0), "cid": int(row.get("cid") or 0),
"chatId": int(row.get("chat_id") or 0), "chatId": int(row.get("chat_id") or 0),
"time": int(row.get("time")), "time": int(row.get("time")),
"type": row.get("type") or "USER", "type": row.get("type") or "USER", # ENUM-строка: USER/CHANNEL/CHANNEL_ADMIN/GROUP
"sender": row.get("sender"), "sender": row.get("sender"),
"text": row.get("text") or "", "text": row.get("text") or "",
"attaches": attaches, "attaches": attaches if isinstance(attaches, list) else [],
"elements": elements, "elements": elements if isinstance(elements, list) else [],
"reactionInfo": {}, "reactionInfo": {},
"link": {} "link": {},
"updateTime": int(row.get("update_time") or row.get("time") or 0),
"status": int(row.get("status") or 1), # 1 = ACTIVE
"options": int(row.get("options") or 0),
} }
return message return message
def generate_profile( def generate_profile(
self, self,
id=None, id=1,
phone=None, phone=70000000000,
avatarUrl=None, avatarUrl=None,
photoId=None, photoId=None,
updateTime=None, updateTime=0,
firstName=None, firstName="Test",
lastName=None, lastName="Account",
options=None, options=[],
description=None, description=None,
accountStatus=None, accountStatus=0,
profileOptions=None, profileOptions=[],
includeProfileOptions=True, includeProfileOptions=True,
username=None, username=None,
@@ -108,40 +126,22 @@ class Tools:
def generate_profile_tt( def generate_profile_tt(
self, self,
id=None, id=1,
phone=None, phone=70000000000,
avatarUrl=None, avatarUrl=None,
photoId=None, photoId=None,
updateTime=None, updateTime=0,
firstName=None, firstName="Test",
lastName=None, lastName="Account",
options=None, options=[],
description=None, description=None,
username=None, username=None,
custom_firstname=None,
custom_lastname=None,
blocked=None
): ):
# Так как TT не поддерживает фамилию, и если нам ее не передали в функцию
# то используем только имя, чтобы избежать None в фамилии
if firstName and lastName:
name = f"{firstName} {lastName}"
else:
name = firstName
# Используем такой же костыль, как и выше
if custom_firstname and custom_lastname:
custom_name = f"{custom_firstname} {custom_lastname}"
elif custom_firstname:
custom_name = custom_firstname
else:
custom_name = None
contact = { contact = {
"id": id, "id": id,
"updateTime": updateTime, "updateTime": updateTime,
"phone": phone, "phone": phone,
"names": [{"name": name, "type": "TT"}], "names": [{"name": f"{firstName} {lastName}", "type": "TT"}],
"options": options, "options": options,
} }
@@ -153,19 +153,8 @@ class Tools:
if description: if description:
contact["description"] = description contact["description"] = description
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
# можно потом как нибудь сделать возможность редактирования этого момента, но это
# позже, так как по юзернейму искать пока нельзя
if username: if username:
contact["link"] = "https://tt.me/" + username contact["link"] = "https://tamtam.chat/" + username
if custom_firstname:
contact["names"].append(
{"name": custom_name, "type": "CUSTOM"}
)
if blocked:
contact["status"] = "BLOCKED"
return contact return contact
@@ -452,6 +441,44 @@ class Tools:
# Возвращаем айдишки # Возвращаем айдишки
return int(message_id), int(last_message_id), message_time return int(message_id), int(last_message_id), message_time
async def collect_bootstrap_history(
self, chatIds, db_pool, senderId, protocol_type="mobile", limit=50, include_favourites=True
):
"""Собирает карту {chatId: [messages...]} для bootstrap-pre-fetch в LOGIN.
Десктопный MAX в ответе LOGIN ждёт поле `messages` как карту чат→история.
Если карта пустая — клиент полагает, что у него уже есть локальная
история и НЕ запрашивает CHAT_HISTORY (49). В итоге в окне чата
видно только lastMessage из chats[].
"""
result = {}
async def _fetch(chat_db_id, key_for_client):
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute(
"SELECT * FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT %s",
(chat_db_id, limit),
)
rows = await cursor.fetchall()
if not rows:
return
messages = [self.build_message_dict(row, protocol_type) for row in rows]
messages.sort(key=lambda m: m["time"])
result[key_for_client] = messages
for chatId in chatIds:
await _fetch(chatId, chatId)
if include_favourites:
# Избранное: в БД хранится как chat_id = -senderId,
# но клиенту отдаётся под id = senderId ^ senderId (= 0)
await _fetch(-senderId, senderId ^ senderId)
return result
async def get_last_message(self, chatId, db_pool, protocol_type="mobile"): async def get_last_message(self, chatId, db_pool, protocol_type="mobile"):
"""Получение последнего сообщения в чате""" """Получение последнего сообщения в чате"""
async with db_pool.acquire() as db_connection: async with db_pool.acquire() as db_connection:
@@ -618,23 +645,3 @@ class Tools:
await cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,)) await cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
if not await cursor.fetchone(): if not await cursor.fetchone():
return user_id return user_id
async def contact_is_blocked(self, owner_id, contact_id, db_pool):
"""
По изначальной задумке, данная функция должна проверять, заблокирован ли контакт
На сервере долгое время не был доделан черный список, хотя управление им было реализовано
(на деле, это я поленился)
Вернёт вам true, если контакт заблокирован, иначе false
"""
# Проверяем наличие контакта
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s AND is_blocked = %s", (owner_id, contact_id, True))
row = await cursor.fetchone()
# Есди контакт существует и заблокирован, возвращаем true,
if row:
return True
else: # в ином случае false
return False
+1 -4
View File
@@ -22,7 +22,6 @@ class OnemeController(ControllerBase):
# Выбираем протокол в зависимости от типа подключения # Выбираем протокол в зависимости от типа подключения
proto = self.proto_web if is_web else self.proto_tcp proto = self.proto_web if is_web else self.proto_tcp
packet = None
# Не отправляем событие самому себе # Не отправляем событие самому себе
if writer == eventData.get("writer"): if writer == eventData.get("writer"):
@@ -95,9 +94,7 @@ class OnemeController(ControllerBase):
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload
) )
if not packet: # Отправляем пакет
return
if is_web: if is_web:
await writer.send(packet) await writer.send(packet)
else: else:
-26
View File
@@ -54,32 +54,6 @@ class AssetsPayloadModel(pydantic.BaseModel):
sync: int sync: int
type: str type: str
class AssetsGetPayloadModel(pydantic.BaseModel):
type: str
count: int = 100
query: str = None
class AssetsGetByIdsPayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsAddPayloadModel(pydantic.BaseModel):
type: str
id: int = None
class AssetsRemovePayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsMovePayloadModel(pydantic.BaseModel):
type: str
id: int
position: int
class AssetsListModifyPayloadModel(pydantic.BaseModel):
type: str
ids: list
class GetCallHistoryPayloadModel(pydantic.BaseModel): class GetCallHistoryPayloadModel(pydantic.BaseModel):
forward: bool forward: bool
count: int count: int
+2 -2
View File
@@ -2,7 +2,7 @@ from .assets import AssetsProcessors
from .auth import AuthProcessors from .auth import AuthProcessors
from .calls import CallsProcessors from .calls import CallsProcessors
from .chats import ChatsProcessors from .chats import ChatsProcessors
from .complaints import ComplaintsProcessors from .complains import ComplainsProcessors
from .contacts import ContactsProcessors from .contacts import ContactsProcessors
from .folders import FoldersProcessors from .folders import FoldersProcessors
from .history import HistoryProcessors from .history import HistoryProcessors
@@ -16,7 +16,7 @@ class Processors(
AuthProcessors, AuthProcessors,
CallsProcessors, CallsProcessors,
ChatsProcessors, ChatsProcessors,
ComplaintsProcessors, ComplainsProcessors,
ContactsProcessors, ContactsProcessors,
FoldersProcessors, FoldersProcessors,
HistoryProcessors, HistoryProcessors,
+13 -149
View File
@@ -1,18 +1,12 @@
import pydantic import pydantic
import time import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ( from oneme.models import AssetsPayloadModel
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
class AssetsProcessors(BaseProcessor): class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer): async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try: try:
AssetsPayloadModel.model_validate(payload) AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error: except pydantic.ValidationError as error:
@@ -20,148 +14,18 @@ class AssetsProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return return
response = { # TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {}, # Данные пакета
"stickersUpdates": {}, payload = {
"stickersOrder": [ "sections": [],
"RECENT", "sync": int(time.time() * 1000)
"FAVORITE_STICKERS",
"FAVORITE_STICKER_SETS",
"TOP",
"NEW",
"NEW_STICKER_SETS",
],
"sections": [
{
"id": "RECENT",
"type": "RECENTS",
"recentsList": [],
},
{
"id": "FAVORITE_STICKERS",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "FAVORITE_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
{
"id": "TOP",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
],
} }
# Собираем пакет
packet = self.proto.pack_packet( packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
)
await self._send(writer, packet)
async def assets_get(self, payload, seq, writer):
try:
data = AssetsGetPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": [], "marker": None}
else:
response = {"stickers": [], "marker": None}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET, payload=response
)
await self._send(writer, packet)
async def assets_get_by_ids(self, payload, seq, writer):
try:
data = AssetsGetByIdsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET_BY_IDS, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": []}
else:
response = {"stickers": []}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET_BY_IDS, payload=response
)
await self._send(writer, packet)
async def assets_add(self, payload, seq, writer):
try:
AssetsAddPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_ADD, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_ADD, payload={}
)
await self._send(writer, packet)
async def assets_remove(self, payload, seq, writer):
try:
AssetsRemovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_REMOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_REMOVE, payload={}
)
await self._send(writer, packet)
async def assets_move(self, payload, seq, writer):
try:
AssetsMovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_MOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_MOVE, payload={}
)
await self._send(writer, packet)
async def assets_list_modify(self, payload, seq, writer):
try:
AssetsListModifyPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_LIST_MODIFY, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_LIST_MODIFY, payload={}
) )
# Отправляем
await self._send(writer, packet) await self._send(writer, packet)
+5 -5
View File
@@ -297,7 +297,7 @@ class AuthProcessors(BaseProcessor):
photoId = ( photoId = (
None if not account.get("avatar_id") else int(account.get("avatar_id")) None if not account.get("avatar_id") else int(account.get("avatar_id"))
) )
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId) avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = ( description = (
None if not account.get("description") else account.get("description") None if not account.get("description") else account.get("description")
) )
@@ -521,7 +521,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error( await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer
) )
return None, None, None return
# Чаты, где состоит пользователь # Чаты, где состоит пользователь
chats = [] chats = []
@@ -545,7 +545,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error( await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer
) )
return None, None, None return
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
await cursor.execute( await cursor.execute(
@@ -563,7 +563,7 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь # Ищем все чаты, где состоит пользователь
await cursor.execute( await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s", "SELECT * FROM chat_participants WHERE user_id = %s",
(user.get("id"),), (user.get("id")),
) )
user_chats = await cursor.fetchall() user_chats = await cursor.fetchall()
@@ -578,7 +578,7 @@ class AuthProcessors(BaseProcessor):
# Аватарка с биографией # Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id")) photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId) avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not user.get("description") else user.get("description") description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion): if self._check_legacy_version(appVersion):
@@ -3,7 +3,7 @@ import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ComplainReasonsGetPayloadModel from oneme.models import ComplainReasonsGetPayloadModel
class ComplaintsProcessors(BaseProcessor): class ComplainsProcessors(BaseProcessor):
async def complain_reasons_get(self, payload, seq, writer): async def complain_reasons_get(self, payload, seq, writer):
"""Обработчик получения причин жалоб""" """Обработчик получения причин жалоб"""
# Валидируем данные пакета # Валидируем данные пакета
+27 -3
View File
@@ -23,6 +23,8 @@ class HistoryProcessors(BaseProcessor):
getMessages = payload.get("getMessages", True) getMessages = payload.get("getMessages", True)
getChat = payload.get("getChat", False) getChat = payload.get("getChat", False)
messages = [] messages = []
backward_count = 0
forward_count = 0
# Если пользователь хочет получить историю из избранного, # Если пользователь хочет получить историю из избранного,
# то выставляем в качестве ID чата отрицательный ID отправителя # то выставляем в качестве ID чата отрицательный ID отправителя
@@ -78,10 +80,32 @@ class HistoryProcessors(BaseProcessor):
# Сортируем сообщения по времени # Сортируем сообщения по времени
messages.sort(key=lambda x: x["time"]) messages.sort(key=lambda x: x["time"])
payload = { # КОСТЫЛЬ: клиент MAX в fz2.b() фильтрует сообщения по условию
"messages": messages # `message.time >= chat.createTime`. Если у пользователя чат был
} # создан недавно, а наши сообщения в БД старые — все они отбрасываются
# (см. реверс defpackage.fz2.java:89). Сдвигаем time всех сообщений
# в «сейчас + N мс» — гарантированно > chat.createTime, и шаг по 1мс
# сохраняет порядок сортировки.
if messages:
now_ms = int(time.time() * 1000)
for i, m in enumerate(messages):
m["time"] = now_ms + i # на 1мс позже предыдущего
m["updateTime"] = m["time"]
# Формируем ответ.
# Реальный парсер ответа CHAT_HISTORY в MAX 26.15.x — это az2.j(),
# который ждёт всего 3 поля:
# chat — qs2-объект чата (опционально, если getChat=False)
# messages — массив сообщений (jr4.a → u6h.Q для каждого)
# messageIds — Set<Long> списка id сообщений в этом ответе
# Поля forward/backward/pos/total — это парсер a23 для CHAT_MEDIA,
# к chat_history они не имеют отношения.
payload = {
"messages": messages,
"messageIds": [m["id"] for m in messages],
}
# chat-объект отдаём только если запрошен (getChat=True). Пустой
# qs2-dict рискует свалить парсер qs2.e() — лучше вообще не слать.
if getChat: if getChat:
payload["chat"] = {} payload["chat"] = {}
+1 -1
View File
@@ -153,7 +153,7 @@ class MainProcessors(BaseProcessor):
# Аватарка с биографией # Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id")) photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId) avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not user.get("description") else user.get("description") description = None if not user.get("description") else user.get("description")
# Генерируем профиль # Генерируем профиль
-8
View File
@@ -113,14 +113,6 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer) await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return return
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
return
# Добавляем сообщение в историю # Добавляем сообщение в историю
messageId, lastMessageId, messageTime = await self.tools.insert_message( messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId, chatId=chatId,
+2 -2
View File
@@ -36,7 +36,7 @@ class SearchProcessors(BaseProcessor):
if user: if user:
# Аватарка с биографией # Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id")) photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId) avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not user.get("description") else user.get("description") description = None if not user.get("description") else user.get("description")
# Получаем данные контакта # Получаем данные контакта
@@ -129,7 +129,7 @@ class SearchProcessors(BaseProcessor):
# Аватарка с биографией # Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id")) photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId) avatar_url = None if not photoId else self.config.avatar_base_url + photoId
description = None if not user.get("description") else user.get("description") description = None if not user.get("description") else user.get("description")
# Получаем данные контакта # Получаем данные контакта
-48
View File
@@ -168,54 +168,6 @@ class OnemeMobile:
seq, seq,
writer, writer,
) )
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone,
self.processors.assets_get,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone,
self.processors.assets_get_by_ids,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone,
self.processors.assets_add,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone,
self.processors.assets_remove,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone,
self.processors.assets_move,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone,
self.processors.assets_list_modify,
payload,
seq,
writer,
)
case self.opcodes.VIDEO_CHAT_HISTORY: case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required( await self.auth_required(
userPhone, userPhone,
-48
View File
@@ -151,54 +151,6 @@ class OnemeWS:
seq, seq,
websocket, websocket,
) )
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone,
self.processors.assets_get,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone,
self.processors.assets_get_by_ids,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone,
self.processors.assets_add,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone,
self.processors.assets_remove,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone,
self.processors.assets_move,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone,
self.processors.assets_list_modify,
payload,
seq,
websocket,
)
case self.opcodes.VIDEO_CHAT_HISTORY: case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required( await self.auth_required(
userPhone, userPhone,
+1 -4
View File
@@ -22,7 +22,6 @@ class TTController(ControllerBase):
# Выбираем протокол в зависимости от типа подключения # Выбираем протокол в зависимости от типа подключения
proto = self.proto_web if is_web else self.proto_tcp proto = self.proto_web if is_web else self.proto_tcp
packet = None
# Не отправляем событие самому себе # Не отправляем событие самому себе
if writer == eventData.get("writer"): if writer == eventData.get("writer"):
@@ -95,9 +94,7 @@ class TTController(ControllerBase):
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload
) )
if not packet: # Отправляем пакет
return
if is_web: if is_web:
await writer.send(packet) await writer.send(packet)
else: else:
+1 -43
View File
@@ -54,32 +54,6 @@ class AssetsPayloadModel(pydantic.BaseModel):
type: str = None type: str = None
userId: int = None userId: int = None
class AssetsGetPayloadModel(pydantic.BaseModel):
type: str
count: int = 100
query: str = None
class AssetsGetByIdsPayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsAddPayloadModel(pydantic.BaseModel):
type: str
id: int = None
class AssetsRemovePayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsMovePayloadModel(pydantic.BaseModel):
type: str
id: int
position: int
class AssetsListModifyPayloadModel(pydantic.BaseModel):
type: str
ids: list
class GetCallTokenPayloadModel(pydantic.BaseModel): class GetCallTokenPayloadModel(pydantic.BaseModel):
userId: int userId: int
value: str value: str
@@ -102,7 +76,7 @@ class ContactPresencePayloadModel(pydantic.BaseModel):
class ContactUpdatePayloadModel(pydantic.BaseModel): class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str action: str
contactId: int contactId: int
firstName: str = None firstName: str
lastName: str = None lastName: str = None
class TypingPayloadModel(pydantic.BaseModel): class TypingPayloadModel(pydantic.BaseModel):
@@ -121,19 +95,3 @@ class SendMessagePayloadModel(pydantic.BaseModel):
userId: int = None userId: int = None
chatId: int = None chatId: int = None
message: MessageModel message: MessageModel
class AuthConfirmRegisterPayloadModel(pydantic.BaseModel):
token: str
name: str
tokenType: str
deviceType: str
deviceId: str = None
@pydantic.field_validator('name')
def validate_name(cls, v):
v = v.strip()
if not v:
raise ValueError('name must not be empty')
if len(v) > 59:
raise ValueError('name too long')
return v
+13 -146
View File
@@ -1,18 +1,12 @@
import pydantic import pydantic
import time import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from tamtam.models import ( from tamtam.models import AssetsPayloadModel
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
class AssetsProcessors(BaseProcessor): class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer): async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try: try:
AssetsPayloadModel.model_validate(payload) AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error: except pydantic.ValidationError as error:
@@ -20,148 +14,21 @@ class AssetsProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer) await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return return
response = { # TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
# Данные пакета
payload = {
"sync": int(time.time() * 1000), "sync": int(time.time() * 1000),
"stickerSetsUpdates": {}, "stickerSetsUpdates": {},
"stickersUpdates": {}, "stickersUpdates": {},
"stickersOrder": [ "sections": [],
"RECENT", "stickersOrder": []
"FAVORITE_STICKERS",
"FAVORITE_STICKER_SETS",
"TOP",
"NEW",
"NEW_STICKER_SETS",
],
"sections": [
{
"id": "RECENT",
"type": "RECENTS",
"recentsList": [],
},
{
"id": "FAVORITE_STICKERS",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "FAVORITE_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
{
"id": "TOP",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
],
} }
# Собираем пакет
packet = self.proto.pack_packet( packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
)
await self._send(writer, packet)
async def assets_get(self, payload, seq, writer):
try:
data = AssetsGetPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": [], "marker": None}
else:
response = {"stickers": [], "marker": None}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET, payload=response
)
await self._send(writer, packet)
async def assets_get_by_ids(self, payload, seq, writer):
try:
data = AssetsGetByIdsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET_BY_IDS, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": []}
else:
response = {"stickers": []}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET_BY_IDS, payload=response
)
await self._send(writer, packet)
async def assets_add(self, payload, seq, writer):
try:
AssetsAddPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_ADD, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_ADD, payload={}
)
await self._send(writer, packet)
async def assets_remove(self, payload, seq, writer):
try:
AssetsRemovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_REMOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_REMOVE, payload={}
)
await self._send(writer, packet)
async def assets_move(self, payload, seq, writer):
try:
AssetsMovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_MOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_MOVE, payload={}
)
await self._send(writer, packet)
async def assets_list_modify(self, payload, seq, writer):
try:
AssetsListModifyPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_LIST_MODIFY, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_LIST_MODIFY, payload={}
) )
# Отправляем
await self._send(writer, packet) await self._send(writer, packet)
+68 -241
View File
@@ -4,12 +4,10 @@ import time
import json import json
import re import re
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from common.sms import send_sms_code
from tamtam.models import ( from tamtam.models import (
RequestCodePayloadModel, RequestCodePayloadModel,
VerifyCodePayloadModel, VerifyCodePayloadModel,
FinalAuthPayloadModel, FinalAuthPayloadModel,
AuthConfirmRegisterPayloadModel,
LoginPayloadModel, LoginPayloadModel,
) )
from tamtam.config import TTConfig from tamtam.config import TTConfig
@@ -19,172 +17,6 @@ class AuthProcessors(BaseProcessor):
super().__init__(db_pool, clients, send_event, type) super().__init__(db_pool, clients, send_event, type)
self.server_config = TTConfig().SERVER_CONFIG self.server_config = TTConfig().SERVER_CONFIG
async def _finish_auth(self, payload, seq, writer, cursor, phone, hashed_token, hashed_login, account, deviceType, deviceName, ip, login):
"""Завершение существующего пользователя"""
# Валидируем данные пакета
try:
FinalAuthPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return None
# Удаляем токен
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
# Создаем сессию
await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(
phone,
hashed_login,
deviceType,
deviceName,
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
int(time.time() * 1000)
)
)
# Аватарка с биографией
photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
description = None if not account.get("description") else account.get("description")
# Собираем данные пакета
return {
"userToken": str(account.get("id")),
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"),
lastName=account.get("lastname"),
options=json.loads(account.get("options")),
description=description,
username=account.get("username")
),
"tokenType": "LOGIN",
"token": login
}
async def _finish_reg(self, payload, seq, writer, cursor, phone, hashed_token, hashed_login, deviceType, deviceName, ip, login):
"""Регистрация пользователя во время авторизации"""
# Валидируем данные пакета
try:
AuthConfirmRegisterPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return None
name = payload.get("name", "").strip()
now_ms = int(time.time() * 1000)
now_s = int(time.time())
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем пользователя
# NOTE: На бумаге у нас как бы полная поддержка ТТ (ну, все функции, в которые может макс),
# а клиенты тамтама не знают, что такое фамилия в аккаунтах тамтама (оно предназначено только для ОК)
# по этому просто не писать указывать фамилию в бд, ее клиент и так не отдаст
await cursor.execute(
"""
INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
user_id,
phone,
None,
name,
None,
None,
json.dumps([]),
json.dumps(["TT", "ONEME"]),
0,
str(now_ms),
str(now_s),
),
)
# Добавляем данные аккаунта
await cursor.execute(
"""
INSERT INTO user_data
(phone, user_config, chat_config)
VALUES (%s, %s, %s)
""",
(
phone,
json.dumps(self.static.USER_SETTINGS),
json.dumps({}),
),
)
# Добавляем дефолтную папку
await cursor.execute(
"""
INSERT INTO user_folders
(id, phone, title, sort_order)
VALUES ('all.chat.folder', %s, 'Все', 0)
""",
(phone,),
)
# Удаляем токен
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
# Создаем сессию
await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(
phone,
hashed_login,
deviceType or "ANDROID",
deviceName or "Unknown",
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
now_ms,
),
)
# Генерируем профиль
profile = self.tools.generate_profile_tt(
id=user_id,
phone=int(phone),
avatarUrl=None,
photoId=None,
updateTime=now_ms,
firstName=name,
lastName="",
options=["TT", "ONEME"],
description=None,
username=None,
)
self.logger.info(
f"Новый пользователь зарегистрирован: phone={phone} id={user_id} name={name}"
)
# Собираем данные пакета
return {
"userToken": "0",
"profile": profile,
"tokenType": "LOGIN",
"token": login,
}
async def auth_request(self, payload, seq, writer): async def auth_request(self, payload, seq, writer):
"""Обработчик запроса кода""" """Обработчик запроса кода"""
# Валидируем данные пакета # Валидируем данные пакета
@@ -198,51 +30,29 @@ class AuthProcessors(BaseProcessor):
# Извлекаем телефон из пакета # Извлекаем телефон из пакета
phone = re.sub(r'\D', '', payload.get("phone", "")) phone = re.sub(r'\D', '', payload.get("phone", ""))
# Генерируем токен # Генерируем токен с кодом
code = f"{secrets.randbelow(1_000_000):06d}"
token = secrets.token_urlsafe(128) token = secrets.token_urlsafe(128)
# Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest()
token_hash = hashlib.sha256(token.encode()).hexdigest() token_hash = hashlib.sha256(token.encode()).hexdigest()
# Срок жизни токена (5 минут) # Срок жизни токена (5 минут)
expires = int(time.time()) + 300 expires = int(time.time()) + 300
user_exists = False # Ищем пользователя, и если он существует, сохраняем токен
# Ищем пользователя
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
user = await cursor.fetchone() user = await cursor.fetchone()
# Получаем код через SMS шлюз или генерируем локально # Если пользователь существует, сохраняем токен
local_fallback_code = False
if self.config.sms_gateway_url:
code = await send_sms_code(self.config.sms_gateway_url, phone)
if code is None:
code = f"{secrets.randbelow(1_000_000):06d}"
local_fallback_code = True
else:
code = f"{secrets.randbelow(1_000_000):06d}"
local_fallback_code = True
# Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest()
# Сохраняем токен
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
if user: if user:
user_exists = True
await cursor.execute( await cursor.execute(
"INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)", "INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)",
(phone, token_hash, code_hash, expires, "started") (phone, token_hash, code_hash, expires, "started")
) )
else:
# Пользователь не найден - сохраняем токен в register
await cursor.execute(
"INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)",
(phone, token_hash, code_hash, expires, "register")
)
# Данные пакета # Данные пакета
payload = { payload = {
@@ -261,7 +71,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, packet) await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})") self.logger.debug(f"Код для {phone}: {code}")
async def auth(self, payload, seq, writer): async def auth(self, payload, seq, writer):
"""Обработчик проверки кода""" """Обработчик проверки кода"""
@@ -302,32 +112,13 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_CODE, writer) self.error_types.INVALID_CODE, writer)
return return
# Если это новый пользователь - переводим токен в verified
# и отдаём клиенту NEW токен, чтобы он показал экран ввода имени
if stored_token.get("state") == "register":
await cursor.execute(
"UPDATE auth_tokens SET state = %s WHERE token_hash = %s",
("verified", hashed_token)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK,
seq=seq,
opcode=self.opcodes.AUTH,
payload={
"tokenAttrs": {"NEW": {"token": token}},
"tokenTypes": {"NEW": token},
},
)
await self._send(writer, packet)
return
# Ищем аккаунт # Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
account = await cursor.fetchone() account = await cursor.fetchone()
# Обновляем состояние токена # Обновляем состояние токена
await cursor.execute( await cursor.execute(
"UPDATE auth_tokens SET state = %s WHERE token_hash = %s", "UPDATE auth_tokens set state = %s WHERE token_hash = %s",
("verified", hashed_token) ("verified", hashed_token)
) )
@@ -368,7 +159,15 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet) await self._send(writer, packet)
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, ip): async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, ip):
"""Обработчик финальной аутентификации / регистрации""" """Обработчик финальной аутентификации"""
# Валидируем данные пакета
try:
FinalAuthPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return
# Извлекаем данные из пакета # Извлекаем данные из пакета
token = payload.get("token") token = payload.get("token")
@@ -385,9 +184,10 @@ class AuthProcessors(BaseProcessor):
login = secrets.token_urlsafe(128) login = secrets.token_urlsafe(128)
hashed_login = hashlib.sha256(login.encode()).hexdigest() hashed_login = hashlib.sha256(login.encode()).hexdigest()
# Ищем токен # Ищем токен с кодом
async with self.db_pool.acquire() as conn: async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor: async with conn.cursor() as cursor:
# Ищем токен
await cursor.execute( await cursor.execute(
"SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()", "SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()",
(hashed_token,) (hashed_token,)
@@ -399,36 +199,63 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_TOKEN, writer) self.error_types.INVALID_TOKEN, writer)
return return
# Если авторизация только началась (код ещё не проверен) - отдаем ошибку # Если авторизация только началась - отдаем ошибку
if stored_token.get("state") == "started" or stored_token.get("state") == "register": if stored_token.get("state") == "started":
await self._send_error(seq, self.opcodes.AUTH_CONFIRM, await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_TOKEN, writer) self.error_types.INVALID_TOKEN, writer)
return return
phone = stored_token.get("phone") # Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
# Проверяем, существует ли пользователь
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
account = await cursor.fetchone() account = await cursor.fetchone()
# Если пользователь есть, производим создание сессии # Удаляем токен
if account: await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
resp_payload = await self._finish_auth(
payload, seq, writer, cursor, phone, hashed_token, # Создаем сессию
hashed_login, account, deviceType, deviceName, ip, login await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(
stored_token.get("phone"),
hashed_login,
deviceType,
deviceName,
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
int(time.time() * 1000)
) )
else: # в ином случае производим регистрацию
resp_payload = await self._finish_reg(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, deviceType, deviceName, ip, login
) )
if resp_payload is None: # Аватарка с биографией
return photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
description = None if not account.get("description") else account.get("description")
# Собираем данные пакета
payload = {
# Я хз че сюда вставлять)
# ребята из одноклассников, может быть вы подскажете?
"userToken": str(account.get("id")),
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"),
lastName=account.get("lastname"),
options=json.loads(account.get("options")),
description=description,
username=account.get("username")
),
"tokenType": "LOGIN",
"token": login
}
# Создаем пакет # Создаем пакет
packet = self.proto.pack_packet( packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=resp_payload cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload
) )
# Отправляем # Отправляем
@@ -443,7 +270,7 @@ class AuthProcessors(BaseProcessor):
self.logger.error(f"Возникли ошибки при валидации пакета: {e}") self.logger.error(f"Возникли ошибки при валидации пакета: {e}")
await self._send_error(seq, self.opcodes.LOGIN, await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_PAYLOAD, writer) self.error_types.INVALID_PAYLOAD, writer)
return None, None, None return
# Чаты, где состоит пользователь # Чаты, где состоит пользователь
chats = [] chats = []
@@ -464,7 +291,7 @@ class AuthProcessors(BaseProcessor):
if token_data is None: if token_data is None:
await self._send_error(seq, self.opcodes.LOGIN, await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_TOKEN, writer) self.error_types.INVALID_TOKEN, writer)
return None, None, None return
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),)) await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
@@ -477,7 +304,7 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь # Ищем все чаты, где состоит пользователь
await cursor.execute( await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s", "SELECT * FROM chat_participants WHERE user_id = %s",
(user.get('id'),) (user.get('id'))
) )
user_chats = await cursor.fetchall() user_chats = await cursor.fetchall()
-8
View File
@@ -105,14 +105,6 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer) await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return return
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
return
# Добавляем сообщение в историю # Добавляем сообщение в историю
messageId, lastMessageId, messageTime = await self.tools.insert_message( messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId, chatId=chatId,
-24
View File
@@ -122,30 +122,6 @@ class TamTamMobile:
await self.auth_required( await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, writer userPhone, self.processors.assets_update, payload, seq, writer
) )
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone, self.processors.assets_get, payload, seq, writer
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone, self.processors.assets_get_by_ids, payload, seq, writer
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone, self.processors.assets_add, payload, seq, writer
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone, self.processors.assets_remove, payload, seq, writer
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone, self.processors.assets_move, payload, seq, writer
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone, self.processors.assets_list_modify, payload, seq, writer
)
case self.opcodes.VIDEO_CHAT_HISTORY: case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required( await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, writer userPhone, self.processors.video_chat_history, payload, seq, writer
-24
View File
@@ -109,30 +109,6 @@ class TamTamWS:
await self.auth_required( await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, websocket userPhone, self.processors.assets_update, payload, seq, websocket
) )
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone, self.processors.assets_get, payload, seq, websocket
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone, self.processors.assets_get_by_ids, payload, seq, websocket
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone, self.processors.assets_add, payload, seq, websocket
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone, self.processors.assets_remove, payload, seq, websocket
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone, self.processors.assets_move, payload, seq, websocket
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone, self.processors.assets_list_modify, payload, seq, websocket
)
case self.opcodes.VIDEO_CHAT_HISTORY: case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required( await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, websocket userPhone, self.processors.video_chat_history, payload, seq, websocket