mirror of
https://github.com/openmax-server/server.git
synced 2026-05-22 19:41:41 +03:00
Compare commits
3 Commits
7d2e070d1f
...
c5721f3f9e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5721f3f9e | ||
|
|
03cffc24aa | ||
|
|
87f22a3feb |
@@ -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": "Вы не можете написать этому пользователю"
|
||||
},
|
||||
}
|
||||
|
||||
### Сообщения бота
|
||||
|
||||
@@ -11,14 +11,7 @@ class Tools:
|
||||
pass
|
||||
|
||||
def build_message_dict(self, row, protocol_type="mobile"):
|
||||
"""Унифицированная сборка тела сообщения для отправки клиенту.
|
||||
|
||||
Десктоп MAX (TCP, protocol_type='mobile') и официальный
|
||||
api.oneme.ru ожидают, что в сообщении будут ВСЕГДА присутствовать
|
||||
поля cid / elements / link / reactionInfo, даже если они пустые.
|
||||
Любое отсутствие поля приводит к тому, что клиент бросает соединение
|
||||
при разборе msgpack-схемы (классическая регрессия из коммита 87cfc19).
|
||||
"""
|
||||
"""Сборка тела сообщения"""
|
||||
try:
|
||||
attaches = json.loads(row.get("attaches") or "[]")
|
||||
except (TypeError, ValueError):
|
||||
@@ -28,29 +21,18 @@ class Tools:
|
||||
except (TypeError, ValueError):
|
||||
elements = []
|
||||
|
||||
# Парсер MAX 26.15.3 (defpackage.u6h.Q) ждёт в сообщении следующие
|
||||
# поля. Отсутствие любого ломает разбор msgpack-схемы, и клиент
|
||||
# тихо роняет всю историю чата:
|
||||
# id, cid, chatId, time, type, sender, text, attaches, elements,
|
||||
# link, reactionInfo, updateTime, status, options
|
||||
# Список вытащен дизассемблированием Q() через dexdump.
|
||||
# type — int-enum для разновидности сообщения (0 = обычное text);
|
||||
# status — int-enum (1 = ACTIVE/доставлено, 0 часто означает REMOVED).
|
||||
message = {
|
||||
"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", # ENUM-строка: USER/CHANNEL/CHANNEL_ADMIN/GROUP
|
||||
"type": row.get("type") or "USER",
|
||||
"sender": row.get("sender"),
|
||||
"text": row.get("text") or "",
|
||||
"attaches": attaches if isinstance(attaches, list) else [],
|
||||
"elements": elements if isinstance(elements, list) else [],
|
||||
"attaches": attaches,
|
||||
"elements": elements,
|
||||
"reactionInfo": {},
|
||||
"link": {},
|
||||
"updateTime": int(row.get("update_time") or row.get("time") or 0),
|
||||
"status": int(row.get("status") or 1), # 1 = ACTIVE
|
||||
"options": int(row.get("options") or 0),
|
||||
"link": {}
|
||||
}
|
||||
|
||||
return message
|
||||
@@ -136,12 +118,29 @@ class Tools:
|
||||
options=[],
|
||||
description=None,
|
||||
username=None,
|
||||
custom_firstname=None,
|
||||
custom_lastname=None
|
||||
):
|
||||
# Так как TT не поддерживает фамилию, и если нам ее не передали в функцию
|
||||
# то используем только имя, чтобы избежать None в фамилии
|
||||
if firstName and lastName:
|
||||
name = f"{firstName} {lastName}",
|
||||
else:
|
||||
name = firstName
|
||||
|
||||
# Используем такой же костыль, как и выше
|
||||
if custom_firstname:
|
||||
custom_name = custom_firstname
|
||||
elif custom_firstname and custom_lastname:
|
||||
custom_name = f"{custom_firstname} {custom_lastname}"
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -153,8 +152,16 @@ 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"}
|
||||
)
|
||||
|
||||
return contact
|
||||
|
||||
@@ -441,44 +448,6 @@ class Tools:
|
||||
# Возвращаем айдишки
|
||||
return int(message_id), int(last_message_id), message_time
|
||||
|
||||
async def collect_bootstrap_history(
|
||||
self, chatIds, db_pool, senderId, protocol_type="mobile", limit=50, include_favourites=True
|
||||
):
|
||||
"""Собирает карту {chatId: [messages...]} для bootstrap-pre-fetch в LOGIN.
|
||||
|
||||
Десктопный MAX в ответе LOGIN ждёт поле `messages` как карту чат→история.
|
||||
Если карта пустая — клиент полагает, что у него уже есть локальная
|
||||
история и НЕ запрашивает CHAT_HISTORY (49). В итоге в окне чата
|
||||
видно только lastMessage из chats[].
|
||||
"""
|
||||
result = {}
|
||||
|
||||
async def _fetch(chat_db_id, key_for_client):
|
||||
async with db_pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"SELECT * FROM `messages` WHERE chat_id = %s ORDER BY time DESC LIMIT %s",
|
||||
(chat_db_id, limit),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
if not rows:
|
||||
return
|
||||
|
||||
messages = [self.build_message_dict(row, protocol_type) for row in rows]
|
||||
messages.sort(key=lambda m: m["time"])
|
||||
result[key_for_client] = messages
|
||||
|
||||
for chatId in chatIds:
|
||||
await _fetch(chatId, chatId)
|
||||
|
||||
if include_favourites:
|
||||
# Избранное: в БД хранится как chat_id = -senderId,
|
||||
# но клиенту отдаётся под id = senderId ^ senderId (= 0)
|
||||
await _fetch(-senderId, senderId ^ senderId)
|
||||
|
||||
return result
|
||||
|
||||
async def get_last_message(self, chatId, db_pool, protocol_type="mobile"):
|
||||
"""Получение последнего сообщения в чате"""
|
||||
async with db_pool.acquire() as db_connection:
|
||||
@@ -645,3 +614,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
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
"""Обработчик получения причин жалоб"""
|
||||
# Валидируем данные пакета
|
||||
@@ -23,8 +23,6 @@ class HistoryProcessors(BaseProcessor):
|
||||
getMessages = payload.get("getMessages", True)
|
||||
getChat = payload.get("getChat", False)
|
||||
messages = []
|
||||
backward_count = 0
|
||||
forward_count = 0
|
||||
|
||||
# Если пользователь хочет получить историю из избранного,
|
||||
# то выставляем в качестве ID чата отрицательный ID отправителя
|
||||
@@ -80,32 +78,10 @@ class HistoryProcessors(BaseProcessor):
|
||||
# Сортируем сообщения по времени
|
||||
messages.sort(key=lambda x: x["time"])
|
||||
|
||||
# КОСТЫЛЬ: клиент MAX в fz2.b() фильтрует сообщения по условию
|
||||
# `message.time >= chat.createTime`. Если у пользователя чат был
|
||||
# создан недавно, а наши сообщения в БД старые — все они отбрасываются
|
||||
# (см. реверс defpackage.fz2.java:89). Сдвигаем time всех сообщений
|
||||
# в «сейчас + N мс» — гарантированно > chat.createTime, и шаг по 1мс
|
||||
# сохраняет порядок сортировки.
|
||||
if messages:
|
||||
now_ms = int(time.time() * 1000)
|
||||
for i, m in enumerate(messages):
|
||||
m["time"] = now_ms + i # на 1мс позже предыдущего
|
||||
m["updateTime"] = m["time"]
|
||||
|
||||
# Формируем ответ.
|
||||
# Реальный парсер ответа CHAT_HISTORY в MAX 26.15.x — это az2.j(),
|
||||
# который ждёт всего 3 поля:
|
||||
# chat — qs2-объект чата (опционально, если getChat=False)
|
||||
# messages — массив сообщений (jr4.a → u6h.Q для каждого)
|
||||
# messageIds — Set<Long> списка id сообщений в этом ответе
|
||||
# Поля forward/backward/pos/total — это парсер a23 для CHAT_MEDIA,
|
||||
# к chat_history они не имеют отношения.
|
||||
payload = {
|
||||
"messages": messages,
|
||||
"messageIds": [m["id"] for m in messages],
|
||||
"messages": messages
|
||||
}
|
||||
# chat-объект отдаём только если запрошен (getChat=True). Пустой
|
||||
# qs2-dict рискует свалить парсер qs2.e() — лучше вообще не слать.
|
||||
|
||||
if getChat:
|
||||
payload["chat"] = {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
# Отправляем
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user