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

This commit is contained in:
Alexey Polyakov
2026-05-13 15:58:02 +03:00
parent 7d2e070d1f
commit 87f22a3feb
7 changed files with 53 additions and 90 deletions

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

@@ -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
@@ -441,44 +423,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 +589,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

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

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

@@ -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"] = {}

View File

@@ -113,6 +113,14 @@ class MessagesProcessors(BaseProcessor):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
return
# Проверяем блокировку собеседника
if chat.get("type") == "DIALOG":
contactid = [p for p in participants if p != int(senderId)][0]
# Проверяем, заблокировал ли отправитель собеседника
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
return
# Добавляем сообщение в историю
messageId, lastMessageId, messageTime = await self.tools.insert_message(
chatId=chatId,

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,