mirror of
https://github.com/openmax-server/server.git
synced 2026-05-22 19:41:41 +03:00
MAX & TT: теперь полноценный чёрный список
This commit is contained in:
@@ -15,6 +15,7 @@ class Static:
|
|||||||
RATE_LIMITED = "rate_limited"
|
RATE_LIMITED = "rate_limited"
|
||||||
CONTACT_NOT_FOUND = "contact_not_found"
|
CONTACT_NOT_FOUND = "contact_not_found"
|
||||||
CONTACT_ALREADY_ADDED = "contact_already_added"
|
CONTACT_ALREADY_ADDED = "contact_already_added"
|
||||||
|
CONTACT_BLOCKED = "contact_blocked"
|
||||||
|
|
||||||
class ChatTypes:
|
class ChatTypes:
|
||||||
DIALOG = "DIALOG"
|
DIALOG = "DIALOG"
|
||||||
@@ -95,6 +96,12 @@ class Static:
|
|||||||
"message": "Contact already added",
|
"message": "Contact already added",
|
||||||
"title": "Контакт уже добавлен"
|
"title": "Контакт уже добавлен"
|
||||||
},
|
},
|
||||||
|
"contact_blocked": {
|
||||||
|
"localizedMessage": "Вы не можете написать этому пользователю",
|
||||||
|
"error": "contact.blocked",
|
||||||
|
"message": "Contact is blocked",
|
||||||
|
"title": "Вы не можете написать этому пользователю"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
### Сообщения бота
|
### Сообщения бота
|
||||||
|
|||||||
@@ -11,14 +11,7 @@ class Tools:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def build_message_dict(self, row, protocol_type="mobile"):
|
def build_message_dict(self, row, protocol_type="mobile"):
|
||||||
"""Унифицированная сборка тела сообщения для отправки клиенту.
|
"""Сборка тела сообщения"""
|
||||||
|
|
||||||
Десктоп MAX (TCP, protocol_type='mobile') и официальный
|
|
||||||
api.oneme.ru ожидают, что в сообщении будут ВСЕГДА присутствовать
|
|
||||||
поля cid / elements / link / reactionInfo, даже если они пустые.
|
|
||||||
Любое отсутствие поля приводит к тому, что клиент бросает соединение
|
|
||||||
при разборе msgpack-схемы (классическая регрессия из коммита 87cfc19).
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
attaches = json.loads(row.get("attaches") or "[]")
|
attaches = json.loads(row.get("attaches") or "[]")
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
@@ -28,29 +21,18 @@ class Tools:
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
elements = []
|
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 = {
|
message = {
|
||||||
"id": row.get("id") if protocol_type == "mobile" else str(row.get("id")),
|
"id": row.get("id") if protocol_type == "mobile" else str(row.get("id")),
|
||||||
"cid": int(row.get("cid") or 0),
|
"cid": int(row.get("cid") or 0),
|
||||||
"chatId": int(row.get("chat_id") or 0),
|
"chatId": int(row.get("chat_id") or 0),
|
||||||
"time": int(row.get("time")),
|
"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"),
|
"sender": row.get("sender"),
|
||||||
"text": row.get("text") or "",
|
"text": row.get("text") or "",
|
||||||
"attaches": attaches if isinstance(attaches, list) else [],
|
"attaches": attaches,
|
||||||
"elements": elements if isinstance(elements, list) else [],
|
"elements": elements,
|
||||||
"reactionInfo": {},
|
"reactionInfo": {},
|
||||||
"link": {},
|
"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),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return message
|
return message
|
||||||
@@ -441,44 +423,6 @@ class Tools:
|
|||||||
# Возвращаем айдишки
|
# Возвращаем айдишки
|
||||||
return int(message_id), int(last_message_id), message_time
|
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 def get_last_message(self, chatId, db_pool, protocol_type="mobile"):
|
||||||
"""Получение последнего сообщения в чате"""
|
"""Получение последнего сообщения в чате"""
|
||||||
async with db_pool.acquire() as db_connection:
|
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,))
|
await cursor.execute("SELECT id FROM users WHERE id = %s", (user_id,))
|
||||||
if not await cursor.fetchone():
|
if not await cursor.fetchone():
|
||||||
return user_id
|
return user_id
|
||||||
|
|
||||||
|
async def contact_is_blocked(self, owner_id, contact_id, db_pool):
|
||||||
|
"""
|
||||||
|
По изначальной задумке, данная функция должна проверять, заблокирован ли контакт
|
||||||
|
На сервере долгое время не был доделан черный список, хотя управление им было реализовано
|
||||||
|
(на деле, это я поленился)
|
||||||
|
|
||||||
|
Вернёт вам true, если контакт заблокирован, иначе false
|
||||||
|
"""
|
||||||
|
# Проверяем наличие контакта
|
||||||
|
async with db_pool.acquire() as conn:
|
||||||
|
async with conn.cursor() as cursor:
|
||||||
|
await cursor.execute("SELECT * FROM contacts WHERE owner_id = %s AND contact_id = %s AND is_blocked = %s", (owner_id, contact_id, True))
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
|
||||||
|
# Есди контакт существует и заблокирован, возвращаем true,
|
||||||
|
if row:
|
||||||
|
return True
|
||||||
|
else: # в ином случае false
|
||||||
|
return False
|
||||||
@@ -2,7 +2,7 @@ from .assets import AssetsProcessors
|
|||||||
from .auth import AuthProcessors
|
from .auth import AuthProcessors
|
||||||
from .calls import CallsProcessors
|
from .calls import CallsProcessors
|
||||||
from .chats import ChatsProcessors
|
from .chats import ChatsProcessors
|
||||||
from .complains import ComplainsProcessors
|
from .complaints import ComplaintsProcessors
|
||||||
from .contacts import ContactsProcessors
|
from .contacts import ContactsProcessors
|
||||||
from .folders import FoldersProcessors
|
from .folders import FoldersProcessors
|
||||||
from .history import HistoryProcessors
|
from .history import HistoryProcessors
|
||||||
@@ -16,7 +16,7 @@ class Processors(
|
|||||||
AuthProcessors,
|
AuthProcessors,
|
||||||
CallsProcessors,
|
CallsProcessors,
|
||||||
ChatsProcessors,
|
ChatsProcessors,
|
||||||
ComplainsProcessors,
|
ComplaintsProcessors,
|
||||||
ContactsProcessors,
|
ContactsProcessors,
|
||||||
FoldersProcessors,
|
FoldersProcessors,
|
||||||
HistoryProcessors,
|
HistoryProcessors,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import time
|
|||||||
from classes.baseprocessor import BaseProcessor
|
from classes.baseprocessor import BaseProcessor
|
||||||
from oneme.models import ComplainReasonsGetPayloadModel
|
from oneme.models import ComplainReasonsGetPayloadModel
|
||||||
|
|
||||||
class ComplainsProcessors(BaseProcessor):
|
class ComplaintsProcessors(BaseProcessor):
|
||||||
async def complain_reasons_get(self, payload, seq, writer):
|
async def complain_reasons_get(self, payload, seq, writer):
|
||||||
"""Обработчик получения причин жалоб"""
|
"""Обработчик получения причин жалоб"""
|
||||||
# Валидируем данные пакета
|
# Валидируем данные пакета
|
||||||
@@ -23,8 +23,6 @@ class HistoryProcessors(BaseProcessor):
|
|||||||
getMessages = payload.get("getMessages", True)
|
getMessages = payload.get("getMessages", True)
|
||||||
getChat = payload.get("getChat", False)
|
getChat = payload.get("getChat", False)
|
||||||
messages = []
|
messages = []
|
||||||
backward_count = 0
|
|
||||||
forward_count = 0
|
|
||||||
|
|
||||||
# Если пользователь хочет получить историю из избранного,
|
# Если пользователь хочет получить историю из избранного,
|
||||||
# то выставляем в качестве ID чата отрицательный ID отправителя
|
# то выставляем в качестве ID чата отрицательный ID отправителя
|
||||||
@@ -80,32 +78,10 @@ class HistoryProcessors(BaseProcessor):
|
|||||||
# Сортируем сообщения по времени
|
# Сортируем сообщения по времени
|
||||||
messages.sort(key=lambda x: x["time"])
|
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 = {
|
payload = {
|
||||||
"messages": messages,
|
"messages": messages
|
||||||
"messageIds": [m["id"] for m in messages],
|
|
||||||
}
|
}
|
||||||
# chat-объект отдаём только если запрошен (getChat=True). Пустой
|
|
||||||
# qs2-dict рискует свалить парсер qs2.e() — лучше вообще не слать.
|
|
||||||
if getChat:
|
if getChat:
|
||||||
payload["chat"] = {}
|
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)
|
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Проверяем блокировку собеседника
|
||||||
|
if chat.get("type") == "DIALOG":
|
||||||
|
contactid = [p for p in participants if p != int(senderId)][0]
|
||||||
|
# Проверяем, заблокировал ли отправитель собеседника
|
||||||
|
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
|
||||||
|
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
|
||||||
|
return
|
||||||
|
|
||||||
# Добавляем сообщение в историю
|
# Добавляем сообщение в историю
|
||||||
messageId, lastMessageId, messageTime = await self.tools.insert_message(
|
messageId, lastMessageId, messageTime = await self.tools.insert_message(
|
||||||
chatId=chatId,
|
chatId=chatId,
|
||||||
|
|||||||
@@ -105,6 +105,14 @@ class MessagesProcessors(BaseProcessor):
|
|||||||
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
|
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CHAT_NOT_ACCESS, writer)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Проверяем блокировку собеседника
|
||||||
|
if chat.get("type") == "DIALOG":
|
||||||
|
contactid = [p for p in participants if p != int(senderId)][0]
|
||||||
|
# Проверяем, заблокировал ли отправитель собеседника
|
||||||
|
if await self.tools.contact_is_blocked(contactid, senderId, db_pool):
|
||||||
|
await self._send_error(seq, self.opcodes.MSG_SEND, self.error_types.CONTACT_BLOCKED, writer)
|
||||||
|
return
|
||||||
|
|
||||||
# Добавляем сообщение в историю
|
# Добавляем сообщение в историю
|
||||||
messageId, lastMessageId, messageTime = await self.tools.insert_message(
|
messageId, lastMessageId, messageTime = await self.tools.insert_message(
|
||||||
chatId=chatId,
|
chatId=chatId,
|
||||||
|
|||||||
Reference in New Issue
Block a user