Compare commits

...

19 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
Alexey Polyakov
87cfc1932e Попытка починить историю (спойлер, нихуя не получилось) 2026-05-10 19:04:51 +03:00
Alexey Polyakov
17245f44d0 Фикс сборки избранного 2026-05-09 18:55:49 +03:00
Alexey Polyakov
b1a37bfa24 update sql scheme 2026-05-09 18:54:53 +03:00
Alexey Polyakov
d81eec5532 Генерируем айди пользователей рандомно (опять, да) 2026-05-09 18:16:32 +03:00
Alexey Polyakov
ddb810589f MAX: исправление уязвимости с избранными 2026-05-09 17:55:39 +03:00
Alexey Polyakov
dff6937da8 MAX: добавление контакта по номеру телефона 2026-05-09 15:50:47 +03:00
27 changed files with 1098 additions and 213 deletions

View File

@@ -44,6 +44,7 @@ class Opcodes:
CONTACT_MUTUAL = 38
CONTACT_PHOTOS = 39
CONTACT_SORT = 40
CONTACT_ADD_BY_PHONE = 41
CONTACT_VERIFY = 42
REMOVE_CONTACT_PHOTO = 43
CHAT_INFO = 48

View File

@@ -6,9 +6,9 @@ class SQLQueries:
INSERT_USER = """
INSERT INTO users
(phone, telegram_id, firstname, lastname, username,
(id, phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
INSERT_USER_DATA = """

View File

@@ -49,6 +49,9 @@ class SQLiteConnectionCompat:
async def __aexit__(self, exc_type, exc, tb):
return False
async def commit(self):
await self.connection.commit()
def cursor(self):
return SQLiteCursorCompat(self.connection)

View File

@@ -15,6 +15,7 @@ class Static:
RATE_LIMITED = "rate_limited"
CONTACT_NOT_FOUND = "contact_not_found"
CONTACT_ALREADY_ADDED = "contact_already_added"
CONTACT_BLOCKED = "contact_blocked"
class ChatTypes:
DIALOG = "DIALOG"
@@ -95,6 +96,12 @@ class Static:
"message": "Contact already added",
"title": "Контакт уже добавлен"
},
"contact_blocked": {
"localizedMessage": "Вы не можете написать этому пользователю",
"error": "contact.blocked",
"message": "Contact is blocked",
"title": "Вы не можете написать этому пользователю"
},
}
### Сообщения бота

View File

@@ -1,6 +1,7 @@
import json
import random
import secrets
import time
import os
import geoip2.database
@@ -9,19 +10,46 @@ class Tools:
def __init__(self):
pass
def build_message_dict(self, row, protocol_type="mobile"):
"""Сборка тела сообщения"""
try:
attaches = json.loads(row.get("attaches") or "[]")
except (TypeError, ValueError):
attaches = []
try:
elements = json.loads(row.get("elements") or "[]")
except (TypeError, ValueError):
elements = []
message = {
"id": row.get("id") if protocol_type == "mobile" else str(row.get("id")),
"cid": int(row.get("cid") or 0),
"chatId": int(row.get("chat_id") or 0),
"time": int(row.get("time")),
"type": row.get("type") or "USER",
"sender": row.get("sender"),
"text": row.get("text") or "",
"attaches": attaches,
"elements": elements,
"reactionInfo": {},
"link": {}
}
return message
def generate_profile(
self,
id=1,
phone=70000000000,
id=None,
phone=None,
avatarUrl=None,
photoId=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
updateTime=None,
firstName=None,
lastName=None,
options=None,
description=None,
accountStatus=0,
profileOptions=[],
accountStatus=None,
profileOptions=None,
includeProfileOptions=True,
username=None,
@@ -45,6 +73,8 @@ class Tools:
],
"options": options,
"accountStatus": accountStatus,
"location": "RU",
"registrationTime": int(time.time() * 1000)
}
if avatarUrl:
@@ -78,22 +108,40 @@ class Tools:
def generate_profile_tt(
self,
id=1,
phone=70000000000,
id=None,
phone=None,
avatarUrl=None,
photoId=None,
updateTime=0,
firstName="Test",
lastName="Account",
options=[],
updateTime=None,
firstName=None,
lastName=None,
options=None,
description=None,
username=None,
custom_firstname=None,
custom_lastname=None,
blocked=None
):
# Так как TT не поддерживает фамилию, и если нам ее не передали в функцию
# то используем только имя, чтобы избежать None в фамилии
if firstName and lastName:
name = f"{firstName} {lastName}"
else:
name = firstName
# Используем такой же костыль, как и выше
if custom_firstname and custom_lastname:
custom_name = f"{custom_firstname} {custom_lastname}"
elif custom_firstname:
custom_name = custom_firstname
else:
custom_name = None
contact = {
"id": id,
"updateTime": updateTime,
"phone": phone,
"names": [{"name": f"{firstName} {lastName}", "type": "TT"}],
"names": [{"name": name, "type": "TT"}],
"options": options,
}
@@ -105,8 +153,19 @@ class Tools:
if description:
contact["description"] = description
# NOTE: официальный сервер вроде как отдавал tt.me, но клиент примет любую ссылку
# можно потом как нибудь сделать возможность редактирования этого момента, но это
# позже, так как по юзернейму искать пока нельзя
if username:
contact["link"] = "https://tamtam.chat/" + username
contact["link"] = "https://tt.me/" + username
if custom_firstname:
contact["names"].append(
{"name": custom_name, "type": "CUSTOM"}
)
if blocked:
contact["status"] = "BLOCKED"
return contact
@@ -204,27 +263,28 @@ class Tools:
if include_favourites:
# Получаем последнее сообщение из избранного
favouriteChatId = -senderId
message, messageTime = await self.get_last_message(
senderId, db_pool, protocol_type=protocol_type
favouriteChatId, db_pool, protocol_type=protocol_type
)
# ID избранного
# ID избранного для клиента
chatId = senderId ^ senderId
# Получаем последнюю активность участника (отправителя) в избранном
# Получаем последнюю активность в избранном
participants = await self.get_participant_last_activity(
senderId, [senderId], db_pool
favouriteChatId, [senderId], db_pool
)
# Получаем ID предыдущего сообщения для избранного (чат ID = senderId)
# Получаем ID предыдущего сообщения для избранного
prevMessageId = await self.get_previous_message_id(
senderId, db_pool, protocol_type=protocol_type
favouriteChatId, db_pool, protocol_type=protocol_type
)
# Хардкодим в лист чатов избранное
chats.append(
self.generate_chat(
chatId if protocol_type == "mobile" else str(chatId),
chatId,
senderId,
"DIALOG",
participants,
@@ -370,10 +430,14 @@ class Tools:
last_message_id = row.get("id") or 0 # последнее id сообщения в чате
message_time = int(time.time() * 1000) # время отправки сообщения
# Генерируем ID сообщения
message_id = int(time.time() * 1000000) * 1000 + random.randint(100, 999)
# Вносим новое сообщение в таблицу
await cursor.execute(
"INSERT INTO `messages` (chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
"INSERT INTO `messages` (id, chat_id, sender, time, text, attaches, cid, elements, type) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
(
message_id,
chatId,
senderId,
message_time,
@@ -385,8 +449,6 @@ class Tools:
),
)
message_id = cursor.lastrowid
# Возвращаем айдишки
return int(message_id), int(last_message_id), message_time
@@ -406,24 +468,8 @@ class Tools:
if not row:
return None, None
# Собираем сообщение
message = {
"id": row.get("id")
if protocol_type == "mobile"
else str(row.get("id")),
"time": int(row.get("time")),
"type": row.get("type"),
"sender": row.get("sender"),
"cid": int(row.get("cid")),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"link": {}
}
# Возвращаем
return message, int(row.get("time"))
return self.build_message_dict(row, protocol_type), int(row.get("time"))
async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
"""Получение ID предыдущего сообщения (второго с конца) в чате."""
@@ -562,3 +608,33 @@ class Tools:
return response.country.name or "Localhost Federation"
except Exception:
return "Localhost Federation"
async def generate_user_id(self, db_pool):
"""Генерация id пользователя"""
async with db_pool.acquire() as conn:
async with conn.cursor() as cursor:
while True:
user_id = secrets.randbelow(2_147_383_647) + 100_000
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

View File

@@ -22,6 +22,7 @@ class OnemeController(ControllerBase):
# Выбираем протокол в зависимости от типа подключения
proto = self.proto_web if is_web else self.proto_tcp
packet = None
# Не отправляем событие самому себе
if writer == eventData.get("writer"):
@@ -94,7 +95,9 @@ class OnemeController(ControllerBase):
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload
)
# Отправляем пакет
if not packet:
return
if is_web:
await writer.send(packet)
else:

View File

@@ -54,6 +54,32 @@ class AssetsPayloadModel(pydantic.BaseModel):
sync: int
type: str
class AssetsGetPayloadModel(pydantic.BaseModel):
type: str
count: int = 100
query: str = None
class AssetsGetByIdsPayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsAddPayloadModel(pydantic.BaseModel):
type: str
id: int = None
class AssetsRemovePayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsMovePayloadModel(pydantic.BaseModel):
type: str
id: int
position: int
class AssetsListModifyPayloadModel(pydantic.BaseModel):
type: str
ids: list
class GetCallHistoryPayloadModel(pydantic.BaseModel):
forward: bool
count: int
@@ -145,6 +171,10 @@ class ContactListPayloadModel(pydantic.BaseModel):
class ContactPresencePayloadModel(pydantic.BaseModel):
contactIds: list
class ContactAddByPhonePayloadModel(pydantic.BaseModel):
phone: str
firstName: str
class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str
contactId: int

View File

@@ -2,7 +2,7 @@ from .assets import AssetsProcessors
from .auth import AuthProcessors
from .calls import CallsProcessors
from .chats import ChatsProcessors
from .complains import ComplainsProcessors
from .complaints import ComplaintsProcessors
from .contacts import ContactsProcessors
from .folders import FoldersProcessors
from .history import HistoryProcessors
@@ -16,7 +16,7 @@ class Processors(
AuthProcessors,
CallsProcessors,
ChatsProcessors,
ComplainsProcessors,
ComplaintsProcessors,
ContactsProcessors,
FoldersProcessors,
HistoryProcessors,

View File

@@ -1,12 +1,18 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import AssetsPayloadModel
from oneme.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try:
AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
@@ -14,18 +20,148 @@ class AssetsProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
# Данные пакета
payload = {
"sections": [],
"sync": int(time.time() * 1000)
response = {
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {},
"stickersUpdates": {},
"stickersOrder": [
"RECENT",
"FAVORITE_STICKERS",
"FAVORITE_STICKER_SETS",
"TOP",
"NEW",
"NEW_STICKER_SETS",
],
"sections": [
{
"id": "RECENT",
"type": "RECENTS",
"recentsList": [],
},
{
"id": "FAVORITE_STICKERS",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "FAVORITE_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
{
"id": "TOP",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
],
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
)
await self._send(writer, packet)
async def assets_get(self, payload, seq, writer):
try:
data = AssetsGetPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": [], "marker": None}
else:
response = {"stickers": [], "marker": None}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET, payload=response
)
await self._send(writer, packet)
async def assets_get_by_ids(self, payload, seq, writer):
try:
data = AssetsGetByIdsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET_BY_IDS, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": []}
else:
response = {"stickers": []}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET_BY_IDS, payload=response
)
await self._send(writer, packet)
async def assets_add(self, payload, seq, writer):
try:
AssetsAddPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_ADD, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_ADD, payload={}
)
await self._send(writer, packet)
async def assets_remove(self, payload, seq, writer):
try:
AssetsRemovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_REMOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_REMOVE, payload={}
)
await self._send(writer, packet)
async def assets_move(self, payload, seq, writer):
try:
AssetsMovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_MOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_MOVE, payload={}
)
await self._send(writer, packet)
async def assets_list_modify(self, payload, seq, writer):
try:
AssetsListModifyPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_LIST_MODIFY, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_LIST_MODIFY, payload={}
)
# Отправляем
await self._send(writer, packet)

View File

@@ -297,7 +297,7 @@ class AuthProcessors(BaseProcessor):
photoId = (
None if not account.get("avatar_id") else int(account.get("avatar_id"))
)
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = (
None if not account.get("description") else account.get("description")
)
@@ -397,15 +397,19 @@ class AuthProcessors(BaseProcessor):
now_ms = int(time.time() * 1000)
now_s = int(time.time())
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем пользователя
await cursor.execute(
"""
INSERT INTO users
(phone, telegram_id, firstname, lastname, username,
(id, phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
user_id,
phone,
None,
first_name,
@@ -419,8 +423,6 @@ class AuthProcessors(BaseProcessor):
),
)
user_id = cursor.lastrowid
# Добавляем данные аккаунта
await cursor.execute(
"""
@@ -519,7 +521,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_PAYLOAD, writer
)
return
return None, None, None
# Чаты, где состоит пользователь
chats = []
@@ -543,7 +545,7 @@ class AuthProcessors(BaseProcessor):
await self._send_error(
seq, self.opcodes.LOGIN, self.error_types.INVALID_TOKEN, writer
)
return
return None, None, None
# Ищем аккаунт пользователя в бд
await cursor.execute(
@@ -561,7 +563,7 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь
await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get("id")),
(user.get("id"),),
)
user_chats = await cursor.fetchall()
@@ -576,7 +578,7 @@ class AuthProcessors(BaseProcessor):
# Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
if self._check_legacy_version(appVersion):

View File

@@ -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):
"""Обработчик получения причин жалоб"""
# Валидируем данные пакета

View File

@@ -2,7 +2,7 @@ import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ContactListPayloadModel, ContactPresencePayloadModel, ContactUpdatePayloadModel
from oneme.models import ContactAddByPhonePayloadModel, ContactListPayloadModel, ContactPresencePayloadModel, ContactUpdatePayloadModel
class ContactsProcessors(BaseProcessor):
async def contact_list(self, payload, seq, writer, userId):
@@ -112,6 +112,23 @@ class ContactsProcessors(BaseProcessor):
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname, is_blocked) VALUES (%s, %s, %s, %s, FALSE)",
(userId, contactId, firstName, lastName)
)
# Создаем диалог, если его нет
chatId = userId ^ contactId
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
if not chat:
await cursor.execute(
"INSERT INTO chats (id, owner, type) VALUES (%s, %s, %s)",
(chatId, userId, "DIALOG")
)
for uid in [int(userId), int(contactId)]:
await cursor.execute(
"INSERT INTO chat_participants (chat_id, user_id) VALUES (%s, %s)",
(chatId, uid)
)
# а если уже существует, отправляем ошибку
else:
await self._send_error(seq, self.opcodes.CONTACT_UPDATE, self.error_types.CONTACT_ALREADY_ADDED, writer)
@@ -277,6 +294,97 @@ class ContactsProcessors(BaseProcessor):
await self._send(writer, packet)
async def contact_add_by_phone(self, payload, seq, writer, userId):
"""Добавление контакта по номеру телефона"""
# Валидируем данные пакета
try:
ContactAddByPhonePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.CONTACT_ADD_BY_PHONE, self.error_types.INVALID_PAYLOAD, writer)
return
phone = payload.get("phone")
firstName = payload.get("firstName")
lastName = payload.get("lastName")
# Ищем пользователя по номеру телефона
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE phone = %s", (int(phone),))
user = await cursor.fetchone()
if not user:
await self._send_error(seq, self.opcodes.CONTACT_ADD_BY_PHONE, self.error_types.CONTACT_NOT_FOUND, writer)
return
contactId = user.get("id")
# Проверяем, не добавлен ли уже контакт
await cursor.execute(
"SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s",
(userId, contactId)
)
existing_contact = await cursor.fetchone()
is_new = existing_contact is None
if is_new:
# Добавляем контакт
await cursor.execute(
"INSERT INTO contacts (owner_id, contact_id, custom_firstname, custom_lastname) VALUES (%s, %s, %s, %s)",
(userId, contactId, firstName, lastName)
)
# Создаем диалог, если его нет
chatId = userId ^ contactId
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
if not chat:
await cursor.execute(
"INSERT INTO chats (id, owner, type) VALUES (%s, %s, %s)",
(chatId, userId, "DIALOG")
)
for uid in [int(userId), int(contactId)]:
await cursor.execute(
"INSERT INTO chat_participants (chat_id, user_id) VALUES (%s, %s)",
(chatId, uid)
)
# Генерируем профиль
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
contact = self.tools.generate_profile(
id=user.get("id"),
phone=int(user.get("phone")),
avatarUrl=avatar_url,
photoId=photoId,
updateTime=int(user.get("updatetime")),
firstName=user.get("firstname"),
lastName=user.get("lastname"),
options=json.loads(user.get("options")),
accountStatus=int(user.get("accountstatus")),
description=user.get("description"),
includeProfileOptions=False,
custom_firstname=firstName,
custom_lastname=lastName,
username=user.get("username"),
)
response_payload = {
"new": is_new,
"contact": contact
}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CONTACT_ADD_BY_PHONE, payload=response_payload
)
await self._send(writer, packet)
async def contact_presence(self, payload, seq, writer):
"""Обработчик получения статуса контактов"""
# Валидируем данные пакета

View File

@@ -1,5 +1,6 @@
import pydantic
import json
import time
from classes.baseprocessor import BaseProcessor
from oneme.models import ChatHistoryPayloadModel
@@ -20,19 +21,21 @@ class HistoryProcessors(BaseProcessor):
backward = payload.get("backward", 0)
from_time = payload.get("from", 0)
getMessages = payload.get("getMessages", True)
getChat = payload.get("getChat", False)
messages = []
# Если пользователь хочет получить историю из избранного,
# то выставляем в качестве ID чата его ID
if chatId == (senderId ^ senderId):
chatId = senderId
# то выставляем в качестве ID чата отрицательный ID отправителя
isFavourite = chatId == (senderId ^ senderId)
if isFavourite:
chatId = -senderId
# Проверяем, существует ли чат
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Проверяем состоит ли пользователь в чате,
# только в случае того, если это не избранное
if chatId != senderId:
if not isFavourite:
await cursor.execute("SELECT * FROM chats WHERE id = %s", (chatId,))
chat = await cursor.fetchone()
@@ -58,21 +61,8 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall()
for row in result:
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию
messages.append({
"id": row.get("id") if self.type == 'mobile' else str(row.get('id')),
"time": int(row.get("time")),
"type": row.get("type"),
"sender": row.get("sender"),
"cid": int(row.get("cid")),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"link": {},
#"options": 1,
})
messages.append(self.tools.build_message_dict(row, self.type))
backward_count = len(result)
if forward > 0:
await cursor.execute(
"SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s",
@@ -82,28 +72,19 @@ class HistoryProcessors(BaseProcessor):
result = await cursor.fetchall()
for row in result:
messages.append({
"id": row.get("id") if self.type == 'mobile' else str(row.get('id')),
"time": int(row.get("time")),
"type": row.get("type"),
"sender": row.get("sender"),
"cid": int(row.get("cid")),
"text": row.get("text"),
"attaches": json.loads(row.get("attaches")),
"elements": json.loads(row.get("elements")),
"reactionInfo": {},
"link": {}
#"options": 1,
})
messages.append(self.tools.build_message_dict(row, self.type))
forward_count = len(result)
# Сортируем сообщения по времени
messages.sort(key=lambda x: x["time"])
# Формируем ответ
payload = {
"messages": messages
}
if getChat:
payload["chat"] = {}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.CHAT_HISTORY, payload=payload

View File

@@ -153,7 +153,7 @@ class MainProcessors(BaseProcessor):
# Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
# Генерируем профиль

View File

@@ -88,9 +88,9 @@ class MessagesProcessors(BaseProcessor):
chatId = userId ^ senderId
# Если клиент хочет отправить сообщение в избранное,
# то выставляем в качестве ID чата ID отправителя
# то выставляем в качестве ID чата отрицательный ID отправителя
if chatId == (senderId ^ senderId):
chatId = senderId
chatId = -senderId
participants = [senderId]
else:
# Если все таки клиент хочет отправить сообщение в нормальный чат,
@@ -113,6 +113,14 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
return
# Добавляем сообщение в историю
messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId,
@@ -125,16 +133,21 @@ class MessagesProcessors(BaseProcessor):
db_pool=self.db_pool
)
# Готовое тело сообщения
# Готовое тело сообщения. Поля cid / elements / reactionInfo / link
# должны присутствовать ВСЕГДА (даже пустые) — десктопный MAX
# ожидает фиксированную msgpack-схему и обрывает соединение
# при отсутствии любого из них (см. регрессию из 87cfc19).
bodyMessage = {
"id": messageId,
"id": messageId if self.type == "mobile" else str(messageId),
"cid": int(cid or 0),
"time": messageTime,
"type": "USER",
"sender": senderId,
"cid": cid,
"text": text,
"attaches": attaches,
"elements": elements
"attaches": attaches if isinstance(attaches, list) else [],
"elements": elements if isinstance(elements, list) else [],
"reactionInfo": {},
"link": {},
}
# Отправляем событие всем участникам чата
@@ -143,7 +156,7 @@ class MessagesProcessors(BaseProcessor):
participant,
{
"eventType": "new_msg",
"chatId": 0 if chatId == (senderId ^ senderId) else chatId,
"chatId": 0 if chatId == -senderId else chatId,
"message": bodyMessage,
"prevMessageId": lastMessageId,
"time": messageTime,
@@ -153,7 +166,7 @@ class MessagesProcessors(BaseProcessor):
# Данные пакета
payload = {
"chatId": 0 if chatId == senderId else chatId,
"chatId": 0 if chatId == -senderId else chatId,
"message": bodyMessage,
"unread": 0,
"mark": messageTime

View File

@@ -36,7 +36,7 @@ class SearchProcessors(BaseProcessor):
if user:
# Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
# Получаем данные контакта
@@ -129,7 +129,7 @@ class SearchProcessors(BaseProcessor):
# Аватарка с биографией
photoId = None if not user.get("avatar_id") else int(user.get("avatar_id"))
avatar_url = None if not photoId else self.config.avatar_base_url + photoId
avatar_url = None if not photoId else self.config.avatar_base_url + str(photoId)
description = None if not user.get("description") else user.get("description")
# Получаем данные контакта
@@ -223,12 +223,12 @@ class SearchProcessors(BaseProcessor):
)
)
else:
# Получаем последнее сообщение из чата
# Получаем последнее сообщение из избранного
message, messageTime = await self.tools.get_last_message(
senderId, self.db_pool, protocol_type=self.type
-senderId, self.db_pool, protocol_type=self.type
)
# ID избранного
# ID избранного для клиента
chatId = senderId ^ senderId
# Добавляем чат в список

View File

@@ -168,6 +168,54 @@ class OnemeMobile:
seq,
writer,
)
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone,
self.processors.assets_get,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone,
self.processors.assets_get_by_ids,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone,
self.processors.assets_add,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone,
self.processors.assets_remove,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone,
self.processors.assets_move,
payload,
seq,
writer,
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone,
self.processors.assets_list_modify,
payload,
seq,
writer,
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone,
@@ -311,6 +359,15 @@ class OnemeMobile:
writer,
userId,
)
case self.opcodes.CONTACT_ADD_BY_PHONE:
await self.auth_required(
userPhone,
self.processors.contact_add_by_phone,
payload,
seq,
writer,
userId,
)
case self.opcodes.CONTACT_PRESENCE:
await self.auth_required(
userPhone,

View File

@@ -151,6 +151,54 @@ class OnemeWS:
seq,
websocket,
)
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone,
self.processors.assets_get,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone,
self.processors.assets_get_by_ids,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone,
self.processors.assets_add,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone,
self.processors.assets_remove,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone,
self.processors.assets_move,
payload,
seq,
websocket,
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone,
self.processors.assets_list_modify,
payload,
seq,
websocket,
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone,
@@ -294,6 +342,15 @@ class OnemeWS:
websocket,
userId,
)
case self.opcodes.CONTACT_ADD_BY_PHONE:
await self.auth_required(
userPhone,
self.processors.contact_add_by_phone,
payload,
seq,
websocket,
userId,
)
case self.opcodes.CONTACT_PRESENCE:
await self.auth_required(
userPhone,

View File

@@ -22,6 +22,7 @@ class TTController(ControllerBase):
# Выбираем протокол в зависимости от типа подключения
proto = self.proto_web if is_web else self.proto_tcp
packet = None
# Не отправляем событие самому себе
if writer == eventData.get("writer"):
@@ -94,7 +95,9 @@ class TTController(ControllerBase):
cmd=0, seq=1, opcode=self.opcodes.NOTIF_PRESENCE, payload=payload
)
# Отправляем пакет
if not packet:
return
if is_web:
await writer.send(packet)
else:

View File

@@ -54,6 +54,32 @@ class AssetsPayloadModel(pydantic.BaseModel):
type: str = None
userId: int = None
class AssetsGetPayloadModel(pydantic.BaseModel):
type: str
count: int = 100
query: str = None
class AssetsGetByIdsPayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsAddPayloadModel(pydantic.BaseModel):
type: str
id: int = None
class AssetsRemovePayloadModel(pydantic.BaseModel):
type: str
ids: list
class AssetsMovePayloadModel(pydantic.BaseModel):
type: str
id: int
position: int
class AssetsListModifyPayloadModel(pydantic.BaseModel):
type: str
ids: list
class GetCallTokenPayloadModel(pydantic.BaseModel):
userId: int
value: str
@@ -76,7 +102,7 @@ class ContactPresencePayloadModel(pydantic.BaseModel):
class ContactUpdatePayloadModel(pydantic.BaseModel):
action: str
contactId: int
firstName: str
firstName: str = None
lastName: str = None
class TypingPayloadModel(pydantic.BaseModel):
@@ -95,3 +121,19 @@ class SendMessagePayloadModel(pydantic.BaseModel):
userId: int = None
chatId: int = None
message: MessageModel
class AuthConfirmRegisterPayloadModel(pydantic.BaseModel):
token: str
name: str
tokenType: str
deviceType: str
deviceId: str = None
@pydantic.field_validator('name')
def validate_name(cls, v):
v = v.strip()
if not v:
raise ValueError('name must not be empty')
if len(v) > 59:
raise ValueError('name too long')
return v

View File

@@ -1,12 +1,18 @@
import pydantic
import time
from classes.baseprocessor import BaseProcessor
from tamtam.models import AssetsPayloadModel
from tamtam.models import (
AssetsPayloadModel,
AssetsGetPayloadModel,
AssetsGetByIdsPayloadModel,
AssetsAddPayloadModel,
AssetsRemovePayloadModel,
AssetsMovePayloadModel,
AssetsListModifyPayloadModel,
)
class AssetsProcessors(BaseProcessor):
async def assets_update(self, payload, seq, writer):
"""Обработчик запроса ассетов клиента на сервере"""
# Валидируем данные пакета
try:
AssetsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
@@ -14,21 +20,148 @@ class AssetsProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.ASSETS_UPDATE, self.error_types.INVALID_PAYLOAD, writer)
return
# TODO: сейчас это заглушка, а попозже нужно сделать полноценную реализацию
# Данные пакета
payload = {
response = {
"sync": int(time.time() * 1000),
"stickerSetsUpdates": {},
"stickersUpdates": {},
"sections": [],
"stickersOrder": []
"stickersOrder": [
"RECENT",
"FAVORITE_STICKERS",
"FAVORITE_STICKER_SETS",
"TOP",
"NEW",
"NEW_STICKER_SETS",
],
"sections": [
{
"id": "RECENT",
"type": "RECENTS",
"recentsList": [],
},
{
"id": "FAVORITE_STICKERS",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "FAVORITE_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
{
"id": "TOP",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW",
"type": "STICKERS",
"stickers": [],
"marker": None,
},
{
"id": "NEW_STICKER_SETS",
"type": "STICKER_SETS",
"stickerSets": [],
"marker": None,
},
],
}
# Собираем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_UPDATE, payload=response
)
await self._send(writer, packet)
async def assets_get(self, payload, seq, writer):
try:
data = AssetsGetPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": [], "marker": None}
else:
response = {"stickers": [], "marker": None}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET, payload=response
)
await self._send(writer, packet)
async def assets_get_by_ids(self, payload, seq, writer):
try:
data = AssetsGetByIdsPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_GET_BY_IDS, self.error_types.INVALID_PAYLOAD, writer)
return
asset_type = data.type
if asset_type == "STICKER_SET":
response = {"stickerSets": []}
else:
response = {"stickers": []}
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_GET_BY_IDS, payload=response
)
await self._send(writer, packet)
async def assets_add(self, payload, seq, writer):
try:
AssetsAddPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_ADD, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_ADD, payload={}
)
await self._send(writer, packet)
async def assets_remove(self, payload, seq, writer):
try:
AssetsRemovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_REMOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_REMOVE, payload={}
)
await self._send(writer, packet)
async def assets_move(self, payload, seq, writer):
try:
AssetsMovePayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_MOVE, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_MOVE, payload={}
)
await self._send(writer, packet)
async def assets_list_modify(self, payload, seq, writer):
try:
AssetsListModifyPayloadModel.model_validate(payload)
except pydantic.ValidationError as error:
self.logger.error(f"Возникли ошибки при валидации пакета: {error}")
await self._send_error(seq, self.opcodes.ASSETS_LIST_MODIFY, self.error_types.INVALID_PAYLOAD, writer)
return
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.ASSETS_LIST_MODIFY, payload={}
)
# Отправляем
await self._send(writer, packet)

View File

@@ -4,10 +4,12 @@ import time
import json
import re
from classes.baseprocessor import BaseProcessor
from common.sms import send_sms_code
from tamtam.models import (
RequestCodePayloadModel,
VerifyCodePayloadModel,
FinalAuthPayloadModel,
AuthConfirmRegisterPayloadModel,
LoginPayloadModel,
)
from tamtam.config import TTConfig
@@ -17,6 +19,172 @@ class AuthProcessors(BaseProcessor):
super().__init__(db_pool, clients, send_event, type)
self.server_config = TTConfig().SERVER_CONFIG
async def _finish_auth(self, payload, seq, writer, cursor, phone, hashed_token, hashed_login, account, deviceType, deviceName, ip, login):
"""Завершение существующего пользователя"""
# Валидируем данные пакета
try:
FinalAuthPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return None
# Удаляем токен
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
# Создаем сессию
await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(
phone,
hashed_login,
deviceType,
deviceName,
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
int(time.time() * 1000)
)
)
# Аватарка с биографией
photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
description = None if not account.get("description") else account.get("description")
# Собираем данные пакета
return {
"userToken": str(account.get("id")),
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"),
lastName=account.get("lastname"),
options=json.loads(account.get("options")),
description=description,
username=account.get("username")
),
"tokenType": "LOGIN",
"token": login
}
async def _finish_reg(self, payload, seq, writer, cursor, phone, hashed_token, hashed_login, deviceType, deviceName, ip, login):
"""Регистрация пользователя во время авторизации"""
# Валидируем данные пакета
try:
AuthConfirmRegisterPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return None
name = payload.get("name", "").strip()
now_ms = int(time.time() * 1000)
now_s = int(time.time())
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем пользователя
# NOTE: На бумаге у нас как бы полная поддержка ТТ (ну, все функции, в которые может макс),
# а клиенты тамтама не знают, что такое фамилия в аккаунтах тамтама (оно предназначено только для ОК)
# по этому просто не писать указывать фамилию в бд, ее клиент и так не отдаст
await cursor.execute(
"""
INSERT INTO users
(id, phone, telegram_id, firstname, lastname, username,
profileoptions, options, accountstatus, updatetime, lastseen)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(
user_id,
phone,
None,
name,
None,
None,
json.dumps([]),
json.dumps(["TT", "ONEME"]),
0,
str(now_ms),
str(now_s),
),
)
# Добавляем данные аккаунта
await cursor.execute(
"""
INSERT INTO user_data
(phone, user_config, chat_config)
VALUES (%s, %s, %s)
""",
(
phone,
json.dumps(self.static.USER_SETTINGS),
json.dumps({}),
),
)
# Добавляем дефолтную папку
await cursor.execute(
"""
INSERT INTO user_folders
(id, phone, title, sort_order)
VALUES ('all.chat.folder', %s, 'Все', 0)
""",
(phone,),
)
# Удаляем токен
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
# Создаем сессию
await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(
phone,
hashed_login,
deviceType or "ANDROID",
deviceName or "Unknown",
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
now_ms,
),
)
# Генерируем профиль
profile = self.tools.generate_profile_tt(
id=user_id,
phone=int(phone),
avatarUrl=None,
photoId=None,
updateTime=now_ms,
firstName=name,
lastName="",
options=["TT", "ONEME"],
description=None,
username=None,
)
self.logger.info(
f"Новый пользователь зарегистрирован: phone={phone} id={user_id} name={name}"
)
# Собираем данные пакета
return {
"userToken": "0",
"profile": profile,
"tokenType": "LOGIN",
"token": login,
}
async def auth_request(self, payload, seq, writer):
"""Обработчик запроса кода"""
# Валидируем данные пакета
@@ -30,29 +198,51 @@ class AuthProcessors(BaseProcessor):
# Извлекаем телефон из пакета
phone = re.sub(r'\D', '', payload.get("phone", ""))
# Генерируем токен с кодом
code = f"{secrets.randbelow(1_000_000):06d}"
# Генерируем токен
token = secrets.token_urlsafe(128)
# Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest()
token_hash = hashlib.sha256(token.encode()).hexdigest()
# Срок жизни токена (5 минут)
expires = int(time.time()) + 300
# Ищем пользователя, и если он существует, сохраняем токен
user_exists = False
# Ищем пользователя
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
user = await cursor.fetchone()
# Если пользователь существует, сохраняем токен
# Получаем код через SMS шлюз или генерируем локально
local_fallback_code = False
if self.config.sms_gateway_url:
code = await send_sms_code(self.config.sms_gateway_url, phone)
if code is None:
code = f"{secrets.randbelow(1_000_000):06d}"
local_fallback_code = True
else:
code = f"{secrets.randbelow(1_000_000):06d}"
local_fallback_code = True
# Хешируем
code_hash = hashlib.sha256(code.encode()).hexdigest()
# Сохраняем токен
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
if user:
user_exists = True
await cursor.execute(
"INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)",
(phone, token_hash, code_hash, expires, "started")
)
else:
# Пользователь не найден - сохраняем токен в register
await cursor.execute(
"INSERT INTO auth_tokens (phone, token_hash, code_hash, expires, state) VALUES (%s, %s, %s, %s, %s)",
(phone, token_hash, code_hash, expires, "register")
)
# Данные пакета
payload = {
@@ -71,7 +261,7 @@ class AuthProcessors(BaseProcessor):
# Отправляем
await self._send(writer, packet)
self.logger.debug(f"Код для {phone}: {code}")
self.logger.debug(f"Код для {phone}: {code} (существующий={user_exists})")
async def auth(self, payload, seq, writer):
"""Обработчик проверки кода"""
@@ -112,13 +302,32 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_CODE, writer)
return
# Если это новый пользователь - переводим токен в verified
# и отдаём клиенту NEW токен, чтобы он показал экран ввода имени
if stored_token.get("state") == "register":
await cursor.execute(
"UPDATE auth_tokens SET state = %s WHERE token_hash = %s",
("verified", hashed_token)
)
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK,
seq=seq,
opcode=self.opcodes.AUTH,
payload={
"tokenAttrs": {"NEW": {"token": token}},
"tokenTypes": {"NEW": token},
},
)
await self._send(writer, packet)
return
# Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
account = await cursor.fetchone()
# Обновляем состояние токена
await cursor.execute(
"UPDATE auth_tokens set state = %s WHERE token_hash = %s",
"UPDATE auth_tokens SET state = %s WHERE token_hash = %s",
("verified", hashed_token)
)
@@ -159,15 +368,7 @@ class AuthProcessors(BaseProcessor):
await self._send(writer, packet)
async def auth_confirm(self, payload, seq, writer, deviceType, deviceName, ip):
"""Обработчик финальной аутентификации"""
# Валидируем данные пакета
try:
FinalAuthPayloadModel.model_validate(payload)
except Exception as e:
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_PAYLOAD, writer)
return
"""Обработчик финальной аутентификации / регистрации"""
# Извлекаем данные из пакета
token = payload.get("token")
@@ -184,10 +385,9 @@ class AuthProcessors(BaseProcessor):
login = secrets.token_urlsafe(128)
hashed_login = hashlib.sha256(login.encode()).hexdigest()
# Ищем токен с кодом
# Ищем токен
async with self.db_pool.acquire() as conn:
async with conn.cursor() as cursor:
# Ищем токен
await cursor.execute(
"SELECT * FROM auth_tokens WHERE token_hash = %s AND expires > UNIX_TIMESTAMP()",
(hashed_token,)
@@ -199,63 +399,36 @@ class AuthProcessors(BaseProcessor):
self.error_types.INVALID_TOKEN, writer)
return
# Если авторизация только началась - отдаем ошибку
if stored_token.get("state") == "started":
# Если авторизация только началась (код ещё не проверен) - отдаем ошибку
if stored_token.get("state") == "started" or stored_token.get("state") == "register":
await self._send_error(seq, self.opcodes.AUTH_CONFIRM,
self.error_types.INVALID_TOKEN, writer)
return
# Ищем аккаунт
await cursor.execute("SELECT * FROM users WHERE phone = %s", (stored_token.get("phone"),))
phone = stored_token.get("phone")
# Проверяем, существует ли пользователь
await cursor.execute("SELECT * FROM users WHERE phone = %s", (phone,))
account = await cursor.fetchone()
# Удаляем токен
await cursor.execute("DELETE FROM auth_tokens WHERE token_hash = %s", (hashed_token,))
# Создаем сессию
await cursor.execute(
"INSERT INTO tokens (phone, token_hash, device_type, device_name, location, time) VALUES (%s, %s, %s, %s, %s, %s)",
(
stored_token.get("phone"),
hashed_login,
deviceType,
deviceName,
self.tools.get_geo(
ip=ip, db_path=self.config.geo_db_path
),
int(time.time() * 1000)
# Если пользователь есть, производим создание сессии
if account:
resp_payload = await self._finish_auth(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, account, deviceType, deviceName, ip, login
)
else: # в ином случае производим регистрацию
resp_payload = await self._finish_reg(
payload, seq, writer, cursor, phone, hashed_token,
hashed_login, deviceType, deviceName, ip, login
)
)
# Аватарка с биографией
photo_id = None if not account.get("avatar_id") else int(account.get("avatar_id"))
avatar_url = None if not photo_id else self.config.avatar_base_url + str(photo_id)
description = None if not account.get("description") else account.get("description")
# Собираем данные пакета
payload = {
# Я хз че сюда вставлять)
# ребята из одноклассников, может быть вы подскажете?
"userToken": str(account.get("id")),
"profile": self.tools.generate_profile_tt(
id=account.get("id"),
phone=int(account.get("phone")),
avatarUrl=avatar_url,
photoId=photo_id,
updateTime=int(account.get("updatetime")),
firstName=account.get("firstname"),
lastName=account.get("lastname"),
options=json.loads(account.get("options")),
description=description,
username=account.get("username")
),
"tokenType": "LOGIN",
"token": login
}
if resp_payload is None:
return
# Создаем пакет
packet = self.proto.pack_packet(
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=payload
cmd=self.proto.CMD_OK, seq=seq, opcode=self.opcodes.AUTH_CONFIRM, payload=resp_payload
)
# Отправляем
@@ -270,7 +443,7 @@ class AuthProcessors(BaseProcessor):
self.logger.error(f"Возникли ошибки при валидации пакета: {e}")
await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_PAYLOAD, writer)
return
return None, None, None
# Чаты, где состоит пользователь
chats = []
@@ -291,7 +464,7 @@ class AuthProcessors(BaseProcessor):
if token_data is None:
await self._send_error(seq, self.opcodes.LOGIN,
self.error_types.INVALID_TOKEN, writer)
return
return None, None, None
# Ищем аккаунт пользователя в бд
await cursor.execute("SELECT * FROM users WHERE phone = %s", (token_data.get("phone"),))
@@ -304,7 +477,7 @@ class AuthProcessors(BaseProcessor):
# Ищем все чаты, где состоит пользователь
await cursor.execute(
"SELECT * FROM chat_participants WHERE user_id = %s",
(user.get('id'))
(user.get('id'),)
)
user_chats = await cursor.fetchall()

View File

@@ -105,6 +105,14 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
return
# Добавляем сообщение в историю
messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId,
@@ -135,7 +143,7 @@ class MessagesProcessors(BaseProcessor):
participant,
{
"eventType": "new_msg",
"chatId": 0 if chatId == senderId else chatId,
"chatId": chatId,
"message": bodyMessage,
"prevMessageId": lastMessageId,
"time": messageTime,
@@ -145,7 +153,7 @@ class MessagesProcessors(BaseProcessor):
# Данные пакета
payload = {
"chatId": 0 if chatId == senderId else chatId,
"chatId": chatId,
"message": bodyMessage,
"unread": 0,
"mark": messageTime

View File

@@ -122,6 +122,30 @@ class TamTamMobile:
await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, writer
)
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone, self.processors.assets_get, payload, seq, writer
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone, self.processors.assets_get_by_ids, payload, seq, writer
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone, self.processors.assets_add, payload, seq, writer
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone, self.processors.assets_remove, payload, seq, writer
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone, self.processors.assets_move, payload, seq, writer
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone, self.processors.assets_list_modify, payload, seq, writer
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, writer

View File

@@ -109,6 +109,30 @@ class TamTamWS:
await self.auth_required(
userPhone, self.processors.assets_update, payload, seq, websocket
)
case self.opcodes.ASSETS_GET:
await self.auth_required(
userPhone, self.processors.assets_get, payload, seq, websocket
)
case self.opcodes.ASSETS_GET_BY_IDS:
await self.auth_required(
userPhone, self.processors.assets_get_by_ids, payload, seq, websocket
)
case self.opcodes.ASSETS_ADD:
await self.auth_required(
userPhone, self.processors.assets_add, payload, seq, websocket
)
case self.opcodes.ASSETS_REMOVE:
await self.auth_required(
userPhone, self.processors.assets_remove, payload, seq, websocket
)
case self.opcodes.ASSETS_MOVE:
await self.auth_required(
userPhone, self.processors.assets_move, payload, seq, websocket
)
case self.opcodes.ASSETS_LIST_MODIFY:
await self.auth_required(
userPhone, self.processors.assets_list_modify, payload, seq, websocket
)
case self.opcodes.VIDEO_CHAT_HISTORY:
await self.auth_required(
userPhone, self.processors.video_chat_history, payload, seq, websocket

View File

@@ -88,10 +88,14 @@ class TelegramBot:
username = (message.from_user.username or f"user{int(time.time() * 1000)}")[:60]
try:
# Генерируем ID пользователя
user_id = await self.tools.generate_user_id(self.db_pool)
# Создаем юзера
await cursor.execute(
self.sql_queries.INSERT_USER,
(
user_id, # id
new_phone, # phone
tg_id, # telegram_id
firstname, # firstname

View File

@@ -1,7 +1,7 @@
CREATE TABLE `users` (
`id` INT NOT NULL AUTO_INCREMENT,
`id` INT NOT NULL,
`phone` VARCHAR(20) UNIQUE,
`telegram_id` VARCHAR(64) UNIQUE,
`telegram_id` VARCHAR(64),
`firstname` VARCHAR(59) NOT NULL,
`lastname` VARCHAR(59),
`description` VARCHAR(400),
@@ -51,7 +51,7 @@ CREATE TABLE `chats` (
);
CREATE TABLE `messages` (
`id` INT NOT NULL AUTO_INCREMENT,
`id` BIGINT NOT NULL,
`chat_id` INT NOT NULL,
`sender` INT NOT NULL,
`time` VARCHAR(32) NOT NULL,