Compare commits

...

13 Commits

Author SHA1 Message Date
Alexey Polyakov
c0e23840b5 maybe fixes 2026-05-14 17:30:57 +03:00
Alexey Polyakov
c5721f3f9e Вроде как нормальные заглушки под ассеты 2026-05-13 18:56:27 +03:00
Alexey Polyakov
03cffc24aa TT: регистрация через клиент 2026-05-13 18:42:00 +03:00
Alexey Polyakov
87f22a3feb MAX & TT: теперь полноценный чёрный список 2026-05-13 15:58:02 +03:00
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
22 changed files with 909 additions and 219 deletions

View File

@@ -49,6 +49,9 @@ 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)

View File

@@ -15,6 +15,7 @@ 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"
@@ -95,6 +96,12 @@ class Static:
"message": "Contact already added", "message": "Contact already added",
"title": "Контакт уже добавлен" "title": "Контакт уже добавлен"
}, },
"contact_blocked": {
"localizedMessage": "Вы не можете написать этому пользователю",
"error": "contact.blocked",
"message": "Contact is blocked",
"title": "Вы не можете написать этому пользователю"
},
} }
### Сообщения бота ### Сообщения бота

View File

@@ -10,19 +10,46 @@ class Tools:
def __init__(self): def __init__(self):
pass 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( def generate_profile(
self, self,
id=1, id=None,
phone=70000000000, phone=None,
avatarUrl=None, avatarUrl=None,
photoId=None, photoId=None,
updateTime=0, updateTime=None,
firstName="Test", firstName=None,
lastName="Account", lastName=None,
options=[], options=None,
description=None, description=None,
accountStatus=0, accountStatus=None,
profileOptions=[], profileOptions=None,
includeProfileOptions=True, includeProfileOptions=True,
username=None, username=None,
@@ -81,22 +108,40 @@ class Tools:
def generate_profile_tt( def generate_profile_tt(
self, self,
id=1, id=None,
phone=70000000000, phone=None,
avatarUrl=None, avatarUrl=None,
photoId=None, photoId=None,
updateTime=0, updateTime=None,
firstName="Test", firstName=None,
lastName="Account", lastName=None,
options=[], options=None,
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": f"{firstName} {lastName}", "type": "TT"}], "names": [{"name": name, "type": "TT"}],
"options": options, "options": options,
} }
@@ -108,8 +153,19 @@ class Tools:
if description: if description:
contact["description"] = description contact["description"] = description
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
# можно потом как нибудь сделать возможность редактирования этого момента, но это
# позже, так как по юзернейму искать пока нельзя
if username: 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 return contact
@@ -412,35 +468,8 @@ class Tools:
if not row: if not row:
return None, None 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"): async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
"""Получение ID предыдущего сообщения (второго с конца) в чате.""" """Получение ID предыдущего сообщения (второго с конца) в чате."""
@@ -589,3 +618,23 @@ 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

View File

@@ -22,6 +22,7 @@ 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"):
@@ -94,7 +95,9 @@ 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:

View File

@@ -54,6 +54,32 @@ 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

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 .complains import ComplainsProcessors from .complaints import ComplaintsProcessors
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,
ComplainsProcessors, ComplaintsProcessors,
ContactsProcessors, ContactsProcessors,
FoldersProcessors, FoldersProcessors,
HistoryProcessors, HistoryProcessors,

View File

@@ -1,31 +1,167 @@
import pydantic import pydantic
import time import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import AssetsPayloadModel from oneme.models import (
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:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}") self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
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
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
# Данные пакета response = {
payload = { "sync": int(time.time() * 1000),
"sections": [], "stickerSetsUpdates": {},
"sync": int(time.time() * 1000) "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( 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):
await self._send(writer, packet) 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)

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 + photoId avatar_url = None if not photoId else self.config.avatar_base_url + str(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 return None, None, None
# Чаты, где состоит пользователь # Чаты, где состоит пользователь
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 return None, None, None
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
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 + 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") description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion): if self._check_legacy_version(appVersion):

View File

@@ -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 ComplainsProcessors(BaseProcessor): class ComplaintsProcessors(BaseProcessor):
async def complain_reasons_get(self, payload, seq, writer): async def complain_reasons_get(self, payload, seq, writer):
"""Обработчик получения причин жалоб""" """Обработчик получения причин жалоб"""
# Валидируем данные пакета # Валидируем данные пакета

View File

@@ -1,5 +1,6 @@
import pydantic import pydantic
import json import json
import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from oneme.models import ChatHistoryPayloadModel from oneme.models import ChatHistoryPayloadModel
@@ -20,6 +21,7 @@ class HistoryProcessors(BaseProcessor):
backward = payload.get("backward", 0) backward = payload.get("backward", 0)
from_time = payload.get("from", 0) from_time = payload.get("from", 0)
getMessages = payload.get("getMessages", True) getMessages = payload.get("getMessages", True)
getChat = payload.get("getChat", False)
messages = [] messages = []
# Если пользователь хочет получить историю из избранного, # Если пользователь хочет получить историю из избранного,
@@ -59,35 +61,8 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall() result = await cursor.fetchall()
for row in result: for row in result:
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию messages.append(self.tools.build_message_dict(row, self.type))
message = { backward_count = len(result)
"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)
if forward > 0: if forward > 0:
await cursor.execute( await cursor.execute(
"SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s", "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() result = await cursor.fetchall()
for row in result: for row in result:
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию messages.append(self.tools.build_message_dict(row, self.type))
message = { forward_count = len(result)
"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.sort(key=lambda x: x["time"]) messages.sort(key=lambda x: x["time"])
# Формируем ответ
payload = { payload = {
"messages": messages "messages": messages
} }
if getChat:
payload["chat"] = {}
# Собираем пакет # Собираем пакет
packet = self.proto.pack_packet( packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_HISTORY, payload=payload cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_HISTORY, payload=payload

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 + 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") description = None if not user.get("description") else user.get("description")
# Генерируем профиль # Генерируем профиль

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) 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,
@@ -125,16 +133,21 @@ class MessagesProcessors(BaseProcessor):
db_pool=self.db_pool db_pool=self.db_pool
) )
# Готовое тело сообщения # Готовое тело сообщения. Поля cid / elements / reactionInfo / link
# должны присутствовать ВСЕГДА (даже пустые) — десктопный MAX
# ожидает фиксированную msgpack-схему и обрывает соединение
# при отсутствии любого из них (см. регрессию из 87cfc19).
bodyMessage = { bodyMessage = {
"id": messageId, "id": messageId if self.type == "mobile" else str(messageId),
"cid": int(cid or 0),
"time": messageTime, "time": messageTime,
"type": "USER", "type": "USER",
"sender": senderId, "sender": senderId,
"cid": cid,
"text": text, "text": text,
"attaches": attaches, "attaches": attaches if isinstance(attaches, list) else [],
"elements": elements "elements": elements if isinstance(elements, list) else [],
"reactionInfo": {},
"link": {},
} }
# Отправляем событие всем участникам чата # Отправляем событие всем участникам чата

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 + 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") 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 + 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") description = None if not user.get("description") else user.get("description")
# Получаем данные контакта # Получаем данные контакта

View File

@@ -168,6 +168,54 @@ 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,

View File

@@ -151,6 +151,54 @@ 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,

View File

@@ -22,6 +22,7 @@ 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"):
@@ -94,7 +95,9 @@ 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:

View File

@@ -54,6 +54,32 @@ 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
@@ -76,7 +102,7 @@ class ContactPresencePayloadModel(pydantic.BaseModel):
class ContactUpdatePayloadModel(pydantic.BaseModel): class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str action: str
contactId: int contactId: int
firstName: str firstName: str = None
lastName: str = None lastName: str = None
class TypingPayloadModel(pydantic.BaseModel): class TypingPayloadModel(pydantic.BaseModel):
@@ -94,4 +120,20 @@ class MessageModel(pydantic.BaseModel):
class SendMessagePayloadModel(pydantic.BaseModel): 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

View File

@@ -1,34 +1,167 @@
import pydantic import pydantic
import time import time
from classes.baseprocessor import BaseProcessor from classes.baseprocessor import BaseProcessor
from tamtam.models import AssetsPayloadModel from tamtam.models import (
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:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}") self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
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
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
# Данные пакета response = {
payload = {
"sync": int(time.time() * 1000), "sync": int(time.time() * 1000),
"stickerSetsUpdates": {}, "stickerSetsUpdates": {},
"stickersUpdates": {}, "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( 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):
await self._send(writer, packet) 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)

View File

@@ -4,10 +4,12 @@ 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
@@ -17,6 +19,172 @@ 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):
"""Обработчик запроса кода""" """Обработчик запроса кода"""
# Валидируем данные пакета # Валидируем данные пакета
@@ -30,29 +198,51 @@ 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 = {
@@ -71,7 +261,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем # Отправляем
await self._send(writer, packet) 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): async def auth(self, payload, seq, writer):
"""Обработчик проверки кода""" """Обработчик проверки кода"""
@@ -112,13 +302,32 @@ 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)
) )
@@ -159,15 +368,7 @@ 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")
@@ -184,10 +385,9 @@ 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,)
@@ -199,63 +399,36 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_TOKEN, writer) self.error_types.INVALID_TOKEN, writer)
return 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, 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()
# Удаляем токен # Если пользователь есть, производим создание сессии
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,)) if account:
resp_payload = await self._finish_auth(
# Создаем сессию payload, seq, writer, cursor, phone, hashed_token,
await cursor.execute( hashed_login, account, deviceType, deviceName, ip, login
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)", )
( else: # в ином случае производим регистрацию
stored_token.get("phone"), resp_payload = await self._finish_reg(
hashed_login, payload, seq, writer, cursor, phone, hashed_token,
deviceType, hashed_login, deviceType, deviceName, ip, login
deviceName,
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
int(time.time() * 1000)
) )
)
# Аватарка с биографией if resp_payload is None:
photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id")) return
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=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}") 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 return None, None, None
# Чаты, где состоит пользователь # Чаты, где состоит пользователь
chats = [] chats = []
@@ -291,7 +464,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 return None, None, None
# Ищем аккаунт пользователя в бд # Ищем аккаунт пользователя в бд
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"),))
@@ -303,8 +476,8 @@ 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()

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) 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,

View File

@@ -122,6 +122,30 @@ 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

View File

@@ -109,6 +109,30 @@ 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