mirror of
https://github.com/openmax-server/server.git
synced 2026-05-22 19:41:41 +03:00
Compare commits
9 Commits
87cfc1932e
...
7d2e070d1f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d2e070d1f | ||
|
|
24b0123185 | ||
|
|
31844c7fa2 | ||
|
|
9b60b15538 | ||
|
|
0d91f6542e | ||
|
|
77d6ca8cc0 | ||
|
|
3bf8bc5770 | ||
|
|
861b75eb1c | ||
|
|
fa0ed34adc |
@@ -10,6 +10,51 @@ class Tools:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
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):
|
||||||
|
attaches = []
|
||||||
|
try:
|
||||||
|
elements = json.loads(row.get("elements") or "[]")
|
||||||
|
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
|
||||||
|
"sender": row.get("sender"),
|
||||||
|
"text": row.get("text") or "",
|
||||||
|
"attaches": attaches if isinstance(attaches, list) else [],
|
||||||
|
"elements": elements if isinstance(elements, list) else [],
|
||||||
|
"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),
|
||||||
|
}
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
def generate_profile(
|
def generate_profile(
|
||||||
self,
|
self,
|
||||||
id=1,
|
id=1,
|
||||||
@@ -396,6 +441,44 @@ 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:
|
||||||
@@ -412,35 +495,8 @@ class Tools:
|
|||||||
if not row:
|
if not row:
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
message = {
|
|
||||||
"sender": row.get("sender"),
|
|
||||||
"id": row.get("id")
|
|
||||||
if protocol_type == "mobile"
|
|
||||||
else str(row.get("id")),
|
|
||||||
"time": int(row.get("time")),
|
|
||||||
"text": row.get("text"),
|
|
||||||
"type": row.get("type"),
|
|
||||||
"attaches": json.loads(row.get("attaches"))
|
|
||||||
}
|
|
||||||
|
|
||||||
elements = json.loads(row.get("elements"))
|
|
||||||
link = {}
|
|
||||||
reaction_info = {}
|
|
||||||
|
|
||||||
if elements:
|
|
||||||
message["elements"] = elements
|
|
||||||
|
|
||||||
if link:
|
|
||||||
message["link"] = link
|
|
||||||
|
|
||||||
if reaction_info:
|
|
||||||
message["reactionInfo"] = reaction_info
|
|
||||||
|
|
||||||
if protocol_type == "web":
|
|
||||||
message["cid"] = int(row.get("cid"))
|
|
||||||
|
|
||||||
# Возвращаем
|
# Возвращаем
|
||||||
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"):
|
async def get_previous_message_id(self, chatId, db_pool, protocol_type="mobile"):
|
||||||
"""Получение ID предыдущего сообщения (второго с конца) в чате."""
|
"""Получение ID предыдущего сообщения (второго с конца) в чате."""
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import pydantic
|
import pydantic
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
from classes.baseprocessor import BaseProcessor
|
from classes.baseprocessor import BaseProcessor
|
||||||
from oneme.models import ChatHistoryPayloadModel
|
from oneme.models import ChatHistoryPayloadModel
|
||||||
|
|
||||||
@@ -20,7 +21,10 @@ class HistoryProcessors(BaseProcessor):
|
|||||||
backward = payload.get("backward", 0)
|
backward = payload.get("backward", 0)
|
||||||
from_time = payload.get("from", 0)
|
from_time = payload.get("from", 0)
|
||||||
getMessages = payload.get("getMessages", True)
|
getMessages = payload.get("getMessages", True)
|
||||||
|
getChat = payload.get("getChat", False)
|
||||||
messages = []
|
messages = []
|
||||||
|
backward_count = 0
|
||||||
|
forward_count = 0
|
||||||
|
|
||||||
# Если пользователь хочет получить историю из избранного,
|
# Если пользователь хочет получить историю из избранного,
|
||||||
# то выставляем в качестве ID чата отрицательный ID отправителя
|
# то выставляем в качестве ID чата отрицательный ID отправителя
|
||||||
@@ -59,35 +63,8 @@ class HistoryProcessors(BaseProcessor):
|
|||||||
result = await cursor.fetchall()
|
result = await cursor.fetchall()
|
||||||
|
|
||||||
for row in result:
|
for row in result:
|
||||||
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию
|
messages.append(self.tools.build_message_dict(row, self.type))
|
||||||
message = {
|
backward_count = len(result)
|
||||||
"sender": row.get("sender"),
|
|
||||||
"id": row.get("id")
|
|
||||||
if self.type == "mobile"
|
|
||||||
else str(row.get("id")),
|
|
||||||
"time": int(row.get("time")),
|
|
||||||
"text": row.get("text"),
|
|
||||||
"type": row.get("type"),
|
|
||||||
"attaches": json.loads(row.get("attaches"))
|
|
||||||
}
|
|
||||||
|
|
||||||
elements = json.loads(row.get("elements"))
|
|
||||||
link = {}
|
|
||||||
reaction_info = {}
|
|
||||||
|
|
||||||
if elements:
|
|
||||||
message["elements"] = elements
|
|
||||||
|
|
||||||
if link:
|
|
||||||
message["link"] = link
|
|
||||||
|
|
||||||
if reaction_info:
|
|
||||||
message["reactionInfo"] = reaction_info
|
|
||||||
|
|
||||||
if self.type == "web":
|
|
||||||
message["cid"] = int(row.get("cid"))
|
|
||||||
|
|
||||||
messages.append(message)
|
|
||||||
if forward > 0:
|
if forward > 0:
|
||||||
await cursor.execute(
|
await cursor.execute(
|
||||||
"SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s",
|
"SELECT * FROM messages WHERE chat_id = %s AND time > %s ORDER BY time ASC LIMIT %s",
|
||||||
@@ -97,43 +74,40 @@ class HistoryProcessors(BaseProcessor):
|
|||||||
result = await cursor.fetchall()
|
result = await cursor.fetchall()
|
||||||
|
|
||||||
for row in result:
|
for row in result:
|
||||||
# TODO: Сборку тела сообщения нужно вынести в отдельную функцию
|
messages.append(self.tools.build_message_dict(row, self.type))
|
||||||
message = {
|
forward_count = len(result)
|
||||||
"sender": row.get("sender"),
|
|
||||||
"id": row.get("id")
|
|
||||||
if self.type == "mobile"
|
|
||||||
else str(row.get("id")),
|
|
||||||
"time": int(row.get("time")),
|
|
||||||
"text": row.get("text"),
|
|
||||||
"type": row.get("type"),
|
|
||||||
"attaches": json.loads(row.get("attaches"))
|
|
||||||
}
|
|
||||||
|
|
||||||
elements = json.loads(row.get("elements"))
|
|
||||||
link = {}
|
|
||||||
reaction_info = {}
|
|
||||||
|
|
||||||
if elements:
|
|
||||||
message["elements"] = elements
|
|
||||||
|
|
||||||
if link:
|
|
||||||
message["link"] = link
|
|
||||||
|
|
||||||
if reaction_info:
|
|
||||||
message["reactionInfo"] = reaction_info
|
|
||||||
|
|
||||||
if self.type == "web":
|
|
||||||
message["cid"] = int(row.get("cid"))
|
|
||||||
|
|
||||||
messages.append(message)
|
|
||||||
|
|
||||||
# Сортируем сообщения по времени
|
# Сортируем сообщения по времени
|
||||||
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:
|
||||||
|
payload["chat"] = {}
|
||||||
|
|
||||||
# Собираем пакет
|
# Собираем пакет
|
||||||
packet = self.proto.pack_packet(
|
packet = self.proto.pack_packet(
|
||||||
|
|||||||
@@ -125,16 +125,21 @@ class MessagesProcessors(BaseProcessor):
|
|||||||
db_pool=self.db_pool
|
db_pool=self.db_pool
|
||||||
)
|
)
|
||||||
|
|
||||||
# Готовое тело сообщения
|
# Готовое тело сообщения. Поля cid / elements / reactionInfo / link
|
||||||
|
# должны присутствовать ВСЕГДА (даже пустые) — десктопный MAX
|
||||||
|
# ожидает фиксированную msgpack-схему и обрывает соединение
|
||||||
|
# при отсутствии любого из них (см. регрессию из 87cfc19).
|
||||||
bodyMessage = {
|
bodyMessage = {
|
||||||
"id": messageId,
|
"id": messageId if self.type == "mobile" else str(messageId),
|
||||||
|
"cid": int(cid or 0),
|
||||||
"time": messageTime,
|
"time": messageTime,
|
||||||
"type": "USER",
|
"type": "USER",
|
||||||
"sender": senderId,
|
"sender": senderId,
|
||||||
"cid": cid,
|
|
||||||
"text": text,
|
"text": text,
|
||||||
"attaches": attaches,
|
"attaches": attaches if isinstance(attaches, list) else [],
|
||||||
"elements": elements
|
"elements": elements if isinstance(elements, list) else [],
|
||||||
|
"reactionInfo": {},
|
||||||
|
"link": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Отправляем событие всем участникам чата
|
# Отправляем событие всем участникам чата
|
||||||
|
|||||||
Reference in New Issue
Block a user