diff --git a/src/common/static.py b/src/common/static.py index 5af8a81..99c4af6 100644 --- a/src/common/static.py +++ b/src/common/static.py @@ -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": "Вы не можете написать этому пользователю" + }, } ### Сообщения бота diff --git a/src/common/tools.py b/src/common/tools.py index 4174951..3c6029b 100644 --- a/src/common/tools.py +++ b/src/common/tools.py @@ -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 \ No newline at end of file diff --git a/src/oneme/processors/__init__.py b/src/oneme/processors/__init__.py index 1261263..2019255 100644 --- a/src/oneme/processors/__init__.py +++ b/src/oneme/processors/__init__.py @@ -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, diff --git a/src/oneme/processors/complains.py b/src/oneme/processors/complaints.py similarity index 96% rename from src/oneme/processors/complains.py rename to src/oneme/processors/complaints.py index 1b5eb80..f1532db 100644 --- a/src/oneme/processors/complains.py +++ b/src/oneme/processors/complaints.py @@ -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): """Обработчик получения причин жалоб""" # Валидируем данные пакета diff --git a/src/oneme/processors/history.py b/src/oneme/processors/history.py index 4d40e8a..d6d54bc 100644 --- a/src/oneme/processors/history.py +++ b/src/oneme/processors/history.py @@ -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 списка 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"] = {} diff --git a/src/oneme/processors/messages.py b/src/oneme/processors/messages.py index b007eab..1890f25 100644 --- a/src/oneme/processors/messages.py +++ b/src/oneme/processors/messages.py @@ -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, diff --git a/src/tamtam/processors/messages.py b/src/tamtam/processors/messages.py index c9cd2df..f64c23d 100644 --- a/src/tamtam/processors/messages.py +++ b/src/tamtam/processors/messages.py @@ -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,