Compare commits

..

14 Commits

Author SHA1 Message Date
Alexey Polyakov fc6ec5eec5 W.I.P.: OpenMAX 0.1.0 (#28)
* Различные фиксы (#14)

* Исключаем только ошибку валидации

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

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

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

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

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

* Починил немного сокет тамтама

* Теперь все должно работать исправно

* Документация: описал про SESSION_INIT

* Update deviceId description in tamtam_ws.md

* feat: 16 опкод для обновления профиля

* feat: 23 опкод для регистрации, смс шлюз, докер

* SQLite: Ошибка

И вообще не юзайте sqlite пожалуйста, он баганный

* Серверный конфиг снова сломался

* Починил изменение профиля

* История сообщений (вроде как даже работает)

* Дополнил немного 48 опкод, и починил отдачу истории в избранном (избранное все равно не работает)

* Доделал авторизацию в ТамТаме

* Швырнул архитектуру, чтобы позже объединить контроллеры веба и сокета в одно, а также разделить процессоры

* Удалил tamtam.proto, поскольку нигде больше не используется

* Поделил процессоры в таме

* Коды стран

* Генерируем айди, за вместо того, чтобы писать его попорядку

* Вынес список участников чата в отдельную таблицу

* Теперь мы не должны отправлять 128 опкод самому себе. вроде

* ТамТам: реализация 32 опкода. Надеюсь, будет работать

* ТамТам: обработчик 5 и 1 опкода

* ТамТам: починил 32 опкод и добавил серверный конфиг. Избранное больше не добавляется в список чатов
MAX: починил 32 опкод

* TT: oops

* MAX: заглушка в 158 опкоде

* Common: поправил 1 прикол

* TamTam: websocket transport for web version

* TamTam && MAX: история (в мохе она вроде теперь получше работает)

* Правка контроллеров небольшая

* MAX: Разделил процессоры

* TG Bot: переписал на aiogram

* Common: фикс года

* Начальная реализация транспорта ws для max web и прочие улучшения

* Common: добавил сжатие lz4

* Исправление конфликтов

* TT: фикс вебсокета

* 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>

* MAX: заглушка для баннеров, правка пакета со списком жалоб, отдача контактов и прочие улучшения

* oops

* MAX: Рабочие баннеры

* MAX: обновление настроек приватности

* MAX: пуши через firebase (особо не тестил, вроде работает)

* MAX: Рефактор папок

* MAX: фикс версий < 25.8.0

* MAX: отдача черного списка

* Теперь ID пользователей идут по порядку, как и раньше

* MAX: создание папок

* MAX: oops

* MAX: добавление/удаление контактов, статусы

* MAX & TT: теперь в качестве страны локации используется настоящая страна пользователя, а также зафиксировал версии библиотек в зависимостях

* TT: ну вроде шире поддержка, а вообще обратная совместимость с максом клас

* oops

* TG Bot: возможность включения белого списка

* Решение проблемы обработки sigterm

* nothing

* fix

* TG Bot: fix

* TG Bot: fix username

* MAX && TT: общение в таме, и корректировки под веб морду

* MAX: Почистил серверный конфиг

* nothing

* MAX: web fix

* MAX: web fix 2

* oops

* nothing

* MAX: теперь для избранного не сравниваем айди с нулём

* Вынес sqlite в отдельный модуль

* MAX: блокировка и разблокировка контакта

* oops

* TT: тоже самое что в макс

* MAX && TT: обновление контакта

* MAX: добавление контакта по номеру телефона

* MAX: исправление уязвимости с избранными

* Генерируем айди пользователей рандомно (опять, да)

* update sql scheme

* Фикс сборки избранного

* Попытка починить историю (спойлер, нихуя не получилось)

* 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 тоже теперь шлют корректную схему.

* 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.

* fix(chat history): фикс

* fix(chat history): фикс

* fix(chat history): фикс

* fix(chat history): фикс

* fix(chat history): фикс

* fix(chat history): фикс

* fix(chat history): фикс

* MAX & TT: теперь полноценный чёрный список

* TT: регистрация через клиент

* Вроде как нормальные заглушки под ассеты

* maybe fixes

---------

Co-authored-by: WowInceptionGood <143893762+WowInceptionGood@users.noreply.github.com>
Co-authored-by: zavolo <me@kirillka.ru>
Co-authored-by: Aleksandr Kosachev <38947781+gohryt@users.noreply.github.com>
2026-07-08 00:11:48 +03:00
WowInceptionGood c53c3cd195 Документация 2026-03-27 17:34:24 +03:00
Alexey Polyakov 77da4a1b61 И так тоже будет лучше 2026-03-22 08:41:49 +03:00
WowInceptionGood f9b6b2c75e README: Думаю так лучше 2026-03-22 00:07:10 +03:00
WowInceptionGood 930a95453f Документация будет перенесена в другой репозиторий в ближайшее время
+ Обновил README ещё.
2026-03-22 00:06:19 +03:00
relyay 2776051b17 Grammar fix in readme.md (#27) 2026-03-16 18:59:08 +03:00
WowInceptionGood 1d4cb2d248 Из-за недавних ошибок 2026-03-16 00:12:27 +03:00
WowInceptionGood ba6191d7e3 faq: Используйте сторонние сервера на свой страх и риск 2026-03-15 21:36:13 +03:00
Alexey Polyakov 48c74153ec Update server link format in faq/servers.md 2026-03-15 21:32:29 +03:00
Alexey Polyakov ea9e4761bc Add mox.nyako.tk server information 2026-03-15 21:28:38 +03:00
Anatoliy Esherkin e6d693efbd Create servers.md (#23) 2026-03-15 11:04:44 +03:00
Anatoliy Esherkin dae26f02a8 Открытые сервера (#25) 2026-03-15 11:04:32 +03:00
Anatoliy Esherkin ca652aab03 Патч IPA (#24)
* Патч IPA

* Update patch_ipa.md

* Update readme.md

---------

Co-authored-by: Alexey Polyakov <starwear3000@mail.ru>
2026-03-15 11:03:47 +03:00
artemchik12 2ce4ac64e2 Что делать на случай ошибки при сборке (#20)
* Что делать на случай ошибки при сборке

* Update patch_apk.md

* Update patch_apk.md

---------

Co-authored-by: Alexey Polyakov <starwear3000@mail.ru>
2026-03-13 18:28:21 +03:00
27 changed files with 937 additions and 225 deletions
+8
View File
@@ -0,0 +1,8 @@
# Патч MAX для IOS
1) Распаковываете IPA
2) Находите файл MAX в Payload/Max.app
3) Открываете hex-редактор и находите api.oneme.ru и меняете на свой
При желании можете поменять другие URL
URL не должен привышать количество символов которое было у изначального URL!
4) Открываете IPA как архив и добавляете патченный файл MAX в Payload/Max.app
+3 -2
View File
@@ -1,7 +1,8 @@
# Навигация по faq
## Работа с сервером
[Установка сервера](install.md)
- [Установка сервера](install.md)
## Патчинг клиентов
[Патч apk](patch_apk.md)
- [Патч мобильного клиента для Android](patch_apk.md)
- [Патч мобильного клиента для IOS](patch_ipa.md)
+11
View File
@@ -0,0 +1,11 @@
> [!Caution]
>
> На данной странице представлены инстансы от сторонних разработчиков.
>
> Используйте на свой страх и риск
>
# Открытые сервера
* [JustMAX](https://t.me/justmax_official)
* [mox.nyako.tk](https://t.me/opengrame/296), порт 443 (регистрация по реальному номеру)
+5 -3
View File
@@ -1,6 +1,6 @@
> [!Caution]
>
> Проект находится на ранней стадии разработки и вероятно полон багов.
> Проект находится на ранней стадии разработки и, вероятно, полон багов.
>
> Использование в профессиональных средах не рекомендовано.
@@ -13,7 +13,7 @@ https://t.me/openmax_alerts
# Требования
- Python 3.12+ (поддержка версий ниже не гарантирована)
- MariaDB, MySQL или SQLite
- MariaDB, MySQL или SQLite (использование последнего не рекомендуется, так как поддержка ещё в разработке)
- Уметь патчить клиент MAX или собирать Komet из исходного кода (естественно с заменой сервера)
- Сертификат и приватный ключ X.509 (для тестирования сервера можно сгенерировать самоподписанный: ```openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365```)
@@ -22,4 +22,6 @@ https://t.me/openmax_alerts
Клиент может быть практически любым, главное условие - чтобы он был совместим с официальным сервером (`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,6 +49,9 @@ class SQLiteConnectionCompat:
async def __aexit__(self, exc_type, exc, tb):
return False
async def commit(self):
await self.connection.commit()
def cursor(self):
return SQLiteCursorCompat(self.connection)
+7
View File
@@ -15,6 +15,7 @@ class Static:
RATE_LIMITED = "rate_limited"
CONTACT_NOT_FOUND = "contact_not_found"
CONTACT_ALREADY_ADDED = "contact_already_added"
CONTACT_BLOCKED = "contact_blocked"
class ChatTypes:
DIALOG = "DIALOG"
@@ -95,6 +96,12 @@ class Static:
"message": "Contact already added",
"title": "Контакт уже добавлен"
},
"contact_blocked": {
"localizedMessage": "Вы не можете написать этому пользователю",
"error": "contact.blocked",
"message": "Contact is blocked",
"title": "Вы не можете написать этому пользователю"
},
}
### Сообщения бота
+93 -44
View File
@@ -10,19 +10,46 @@ class Tools:
def __init__(self):
pass
def build_message_dict(self, row, protocol_type="mobile"):
"""Сборка тела сообщения"""
try:
attaches = json.loads(row.get("attaches") or "[]")
except (TypeError, ValueError):
attaches = []
try:
elements = json.loads(row.get("elements") or "[]")
except (TypeError, ValueError):
elements = []
message = {
"id": row.get("id") if protocol_type == "mobile" else str(row.get("id")),
"cid": int(row.get("cid") or 0),
"chatId": int(row.get("chat_id") or 0),
"time": int(row.get("time")),
"type": row.get("type") or "USER",
"sender": row.get("sender"),
"text": row.get("text") or "",
"attaches": attaches,
"elements": elements,
"reactionInfo": {},
"link": {}
}
return message
def generate_profile(
self,
id=1,
phone=70000000000,
id=None,
phone=None,
avatarUrl=None,
photoId=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
updateTime=None,
firstName=None,
lastName=None,
options=None,
description=None,
accountStatus=0,
profileOptions=[],
accountStatus=None,
profileOptions=None,
includeProfileOptions=True,
username=None,
@@ -81,22 +108,40 @@ class Tools:
def generate_profile_tt(
self,
id=1,
phone=70000000000,
id=None,
phone=None,
avatarUrl=None,
photoId=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
updateTime=None,
firstName=None,
lastName=None,
options=None,
description=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 = {
"id": id,
"updateTime": updateTime,
"phone": phone,
"names": [{"name": f"{firstName} {lastName}", "type": "TT"}],
"names": [{"name": name, "type": "TT"}],
"options": options,
}
@@ -108,8 +153,19 @@ class Tools:
if description:
contact["description"] = description
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
# можно потом как нибудь сделать возможность редактирования этого момента, но это
# позже, так как по юзернейму искать пока нельзя
if username:
contact["link"] = "https://tamtam.chat/" + username
contact["link"] = "https://tt.me/" + username
if custom_firstname:
contact["names"].append(
{"name": custom_name, "type": "CUSTOM"}
)
if blocked:
contact["status"] = "BLOCKED"
return contact
@@ -412,35 +468,8 @@ class Tools:
if not row:
return None, None
message = {
"sender": row.get("sender"),
"id": row.get("id")
if protocol_type == "mobile"
else str(row.get("id")),
"time": int(row.get("time")),
"text": row.get("text"),
"type": row.get("type"),
"attaches": json.loads(row.get("attaches"))
}
elements = json.loads(row.get("elements"))
link = {}
reaction_info = {}
if elements:
message["elements"] = elements
if link:
message["link"] = link
if reaction_info:
message["reactionInfo"] = reaction_info
if protocol_type == "web":
message["cid"] = int(row.get("cid"))
# Возвращаем
return message, int(row.get("time"))
return self.build_message_dict(row, protocol_type), int(row.get("time"))
async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
"""Получение ID предыдущего сообщения (второго с конца) в чате."""
@@ -589,3 +618,23 @@ class Tools:
await cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
if not await cursor.fetchone():
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
+4 -1
View File
@@ -22,6 +22,7 @@ class OnemeController(ControllerBase):
# Выбираем протокол в зависимости от типа подключения
proto = self.proto_web if is_web else self.proto_tcp
packet = None
# Не отправляем событие самому себе
if writer == eventData.get("writer"):
@@ -94,7 +95,9 @@ class OnemeController(ControllerBase):
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload
)
# Отправляем пакет
if not packet:
return
if is_web:
await writer.send(packet)
else:
+26
View File
@@ -54,6 +54,32 @@ class AssetsPayloadModel(pydantic.BaseModel):
sync: int
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):
forward: bool
count: int
+2 -2
View File
@@ -2,7 +2,7 @@ from .assets import AssetsProcessors
from .auth import AuthProcessors
from .calls import CallsProcessors
from .chats import ChatsProcessors
from .complains import ComplainsProcessors
from .complaints import ComplaintsProcessors
from .contacts import ContactsProcessors
from .folders import FoldersProcessors
from .history import HistoryProcessors
@@ -16,7 +16,7 @@ class Processors(
AuthProcessors,
CallsProcessors,
ChatsProcessors,
ComplainsProcessors,
ComplaintsProcessors,
ContactsProcessors,
FoldersProcessors,
HistoryProcessors,
+149 -13
View File
@@ -1,12 +1,18 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import AssetsPayloadModel
from oneme.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try:
AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
@@ -14,18 +20,148 @@ class AssetsProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
# Данные пакета
payload = {
"sections": [],
"sync": int(time.time() * 1000)
response = {
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {},
"stickersUpdates": {},
"stickersOrder": [
"RECENT",
"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(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
)
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)
+5 -5
View File
@@ -297,7 +297,7 @@ class AuthProcessors(BaseProcessor):
photoId = (
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 + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = (
None if not account.get("description") else account.get("description")
)
@@ -521,7 +521,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer
)
return
return None, None, None
# Чаты, где состоит пользователь
chats = []
@@ -545,7 +545,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer
)
return
return None, None, None
# Ищем аккаунт пользователя в бд
await cursor.execute(
@@ -563,7 +563,7 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь
await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get("id")),
(user.get("id"),),
)
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"))
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion):
@@ -3,7 +3,7 @@ import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ComplainReasonsGetPayloadModel
class ComplainsProcessors(BaseProcessor):
class ComplaintsProcessors(BaseProcessor):
async def complain_reasons_get(self, payload, seq, writer):
"""Обработчик получения причин жалоб"""
# Валидируем данные пакета
+9 -59
View File
@@ -1,5 +1,6 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ChatHistoryPayloadModel
@@ -20,6 +21,7 @@ class HistoryProcessors(BaseProcessor):
backward = payload.get("backward", 0)
from_time = payload.get("from", 0)
getMessages = payload.get("getMessages", True)
getChat = payload.get("getChat", False)
messages = []
# Если пользователь хочет получить историю из избранного,
@@ -59,35 +61,8 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall()
for row in result:
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию
message = {
"sender": row.get("sender"),
"id": row.get("id")
if self.type == "mobile"
else str(row.get("id")),
"time": int(row.get("time")),
"text": row.get("text"),
"type": row.get("type"),
"attaches": json.loads(row.get("attaches"))
}
elements = json.loads(row.get("elements"))
link = {}
reaction_info = {}
if elements:
message["elements"] = elements
if link:
message["link"] = link
if reaction_info:
message["reactionInfo"] = reaction_info
if self.type == "web":
message["cid"] = int(row.get("cid"))
messages.append(message)
messages.append(self.tools.build_message_dict(row, self.type))
backward_count = len(result)
if forward > 0:
await cursor.execute(
"SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s",
@@ -97,44 +72,19 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall()
for row in result:
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию
message = {
"sender": row.get("sender"),
"id": row.get("id")
if self.type == "mobile"
else str(row.get("id")),
"time": int(row.get("time")),
"text": row.get("text"),
"type": row.get("type"),
"attaches": json.loads(row.get("attaches"))
}
elements = json.loads(row.get("elements"))
link = {}
reaction_info = {}
if elements:
message["elements"] = elements
if link:
message["link"] = link
if reaction_info:
message["reactionInfo"] = reaction_info
if self.type == "web":
message["cid"] = int(row.get("cid"))
messages.append(message)
messages.append(self.tools.build_message_dict(row, self.type))
forward_count = len(result)
# Сортируем сообщения по времени
messages.sort(key=lambda x: x["time"])
# Формируем ответ
payload = {
"messages": messages
}
if getChat:
payload["chat"] = {}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_HISTORY, payload=payload
+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"))
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
# Генерируем профиль
+18 -5
View File
@@ -113,6 +113,14 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
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(
chatId=chatId,
@@ -125,16 +133,21 @@ class MessagesProcessors(BaseProcessor):
db_pool=self.db_pool
)
# Готовое тело сообщения
# Готовое тело сообщения. Поля cid / elements / reactionInfo / link
# должны присутствовать ВСЕГДА (даже пустые) — десктопный MAX
# ожидает фиксированную msgpack-схему и обрывает соединение
# при отсутствии любого из них (см. регрессию из 87cfc19).
bodyMessage = {
"id": messageId,
"id": messageId if self.type == "mobile" else str(messageId),
"cid": int(cid or 0),
"time": messageTime,
"type": "USER",
"sender": senderId,
"cid": cid,
"text": text,
"attaches": attaches,
"elements": elements
"attaches": attaches if isinstance(attaches, list) else [],
"elements": elements if isinstance(elements, list) else [],
"reactionInfo": {},
"link": {},
}
# Отправляем событие всем участникам чата
+2 -2
View File
@@ -36,7 +36,7 @@ class SearchProcessors(BaseProcessor):
if user:
# Аватарка с биографией
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 + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
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"))
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
# Получаем данные контакта
+48
View File
@@ -168,6 +168,54 @@ class OnemeMobile:
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:
await self.auth_required(
userPhone,
+48
View File
@@ -151,6 +151,54 @@ class OnemeWS:
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:
await self.auth_required(
userPhone,
+4 -1
View File
@@ -22,6 +22,7 @@ class TTController(ControllerBase):
# Выбираем протокол в зависимости от типа подключения
proto = self.proto_web if is_web else self.proto_tcp
packet = None
# Не отправляем событие самому себе
if writer == eventData.get("writer"):
@@ -94,7 +95,9 @@ class TTController(ControllerBase):
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload
)
# Отправляем пакет
if not packet:
return
if is_web:
await writer.send(packet)
else:
+43 -1
View File
@@ -54,6 +54,32 @@ class AssetsPayloadModel(pydantic.BaseModel):
type: str = 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):
userId: int
value: str
@@ -76,7 +102,7 @@ class ContactPresencePayloadModel(pydantic.BaseModel):
class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str
contactId: int
firstName: str
firstName: str = None
lastName: str = None
class TypingPayloadModel(pydantic.BaseModel):
@@ -95,3 +121,19 @@ class SendMessagePayloadModel(pydantic.BaseModel):
userId: int = None
chatId: int = None
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
+146 -13
View File
@@ -1,12 +1,18 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import AssetsPayloadModel
from tamtam.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try:
AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
@@ -14,21 +20,148 @@ class AssetsProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
# Данные пакета
payload = {
response = {
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {},
"stickersUpdates": {},
"sections": [],
"stickersOrder": []
"stickersOrder": [
"RECENT",
"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(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
)
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)
+241 -68
View File
@@ -4,10 +4,12 @@ import time
import json
import re
from classes.baseprocessor import BaseProcessor
from common.sms import send_sms_code
from tamtam.models import (
RequestCodePayloadModel,
VerifyCodePayloadModel,
FinalAuthPayloadModel,
AuthConfirmRegisterPayloadModel,
LoginPayloadModel,
)
from tamtam.config import TTConfig
@@ -17,6 +19,172 @@ class AuthProcessors(BaseProcessor):
super().__init__(db_pool, clients, send_event, type)
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):
"""Обработчик запроса кода"""
# Валидируем данные пакета
@@ -30,29 +198,51 @@ class AuthProcessors(BaseProcessor):
# Извлекаем телефон из пакета
phone = re.sub(r'\D', '', payload.get("phone", ""))
# Генерируем токен с кодом
code = f"{secrets.randbelow(1_000_000):06d}"
# Генерируем токен
token = secrets.token_urlsafe(128)
# Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest()
token_hash = hashlib.sha256(token.encode()).hexdigest()
# Срок жизни токена (5 минут)
expires = int(time.time()) + 300
# Ищем пользователя, и если он существует, сохраняем токен
user_exists = False
# Ищем пользователя
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
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:
user_exists = True
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, "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 = {
@@ -71,7 +261,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code}")
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
async def auth(self, payload, seq, writer):
"""Обработчик проверки кода"""
@@ -112,13 +302,32 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_CODE, writer)
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"),))
account = await cursor.fetchone()
# Обновляем состояние токена
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)
)
@@ -159,15 +368,7 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet)
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")
@@ -184,10 +385,9 @@ class AuthProcessors(BaseProcessor):
login = secrets.token_urlsafe(128)
hashed_login = hashlib.sha256(login.encode()).hexdigest()
# Ищем токен с кодом
# Ищем токен
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Ищем токен
await cursor.execute(
"SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()",
(hashed_token,)
@@ -199,63 +399,36 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_TOKEN, writer)
return
# Если авторизация только началась - отдаем ошибку
if stored_token.get("state") == "started":
# Если авторизация только началась (код ещё не проверен) - отдаем ошибку
if stored_token.get("state") == "started" or stored_token.get("state") == "register":
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_TOKEN, writer)
return
# Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
phone = stored_token.get("phone")
# Проверяем, существует ли пользователь
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
account = await cursor.fetchone()
# Удаляем токен
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)",
(
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)
# Если пользователь есть, производим создание сессии
if account:
resp_payload = await self._finish_auth(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, account, deviceType, deviceName, ip, login
)
else: # в ином случае производим регистрацию
resp_payload = await self._finish_reg(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, deviceType, deviceName, ip, login
)
# Аватарка с биографией
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
}
if resp_payload is None:
return
# Создаем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=resp_payload
)
# Отправляем
@@ -270,7 +443,7 @@ class AuthProcessors(BaseProcessor):
self.logger.error(f"Возникли ошибки при валидации пакета: {e}")
await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_PAYLOAD, writer)
return
return None, None, None
# Чаты, где состоит пользователь
chats = []
@@ -291,7 +464,7 @@ class AuthProcessors(BaseProcessor):
if token_data is None:
await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_TOKEN, writer)
return
return None, None, None
# Ищем аккаунт пользователя в бд
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
@@ -304,7 +477,7 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь
await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get('id'))
(user.get('id'),)
)
user_chats = await cursor.fetchall()
+8
View File
@@ -105,6 +105,14 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
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(
chatId=chatId,
+24
View File
@@ -122,6 +122,30 @@ class TamTamMobile:
await self.auth_required(
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:
await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, writer
+24
View File
@@ -109,6 +109,30 @@ class TamTamWS:
await self.auth_required(
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:
await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, websocket