Add MAX contacts and direct chat creation

This commit is contained in:
Курнат Андрей
2026-07-12 22:51:24 +03:00
parent 51d4c7dd1d
commit ff3f139889
16 changed files with 630 additions and 34 deletions
+71 -5
View File
@@ -17,7 +17,7 @@ from typing import Any
import aiohttp
from aiohttp import web
from pymax import Client, File, Photo, Video
from pymax import Client, ExtraConfig, File, Photo, SyncOverrides, Video
PORT = int(os.environ.get("PORT", "3002"))
@@ -253,6 +253,21 @@ def user_avatar_url(user: Any) -> str | None:
return first_text(data.get("base_url"), data.get("base_raw_url"))
def contact_result(contact: Any, me_id: int, external_chat_id: Any | None = None) -> dict[str, Any] | None:
data = dump_model(contact)
contact_id = coerce_int(data.get("id") or getattr(contact, "id", None))
if contact_id is None or contact_id <= 0 or contact_id == me_id:
return None
return {
"userId": clean_id(contact_id),
"externalChatId": clean_id(external_chat_id if external_chat_id is not None else contact_id ^ me_id),
"displayName": user_display_name(contact) or f"MAX {contact_id}",
"avatarUrl": user_avatar_url(contact),
"phoneNumber": first_text(data.get("phone")),
"status": first_text(data.get("status"), data.get("description")),
}
def chat_other_user(chat: Any, user_map: dict[str, Any], me_id: int | None) -> Any | None:
for user_id in chat_participant_ids(chat):
if me_id is not None and user_id == me_id:
@@ -391,6 +406,9 @@ class PyMaxRuntime:
session_name=SESSION_NAME,
sms_code_provider=self.code_provider,
password_provider=self.password_provider,
extra_config=ExtraConfig(
sync=SyncOverrides(chats_sync=-1, contacts_sync=-1),
),
)
@client.on_start()
@@ -505,6 +523,10 @@ def normalize_chat_kind(chat: Any) -> str:
return "MaxDialog"
def is_direct_dialog(chat: Any) -> bool:
return value_name(getattr(chat, "type", "")).upper() == "DIALOG"
def normalize_attachment_kind(att: Any) -> str:
data = dump_model(att)
raw_type = value_name(data.get("type") or data.get("_type") or getattr(att, "type", "")).lower()
@@ -729,19 +751,35 @@ async def normalize_chat_update(
async def fetch_chat_pages(client: Client, limit: int) -> list[Any]:
chats: list[Any] = []
seen_chat_ids: set[str] = set()
seen_markers: set[int] = set()
marker: int | None = None
while len(chats) < limit:
page = await client.fetch_chats(marker=marker)
page = page or []
if not page:
break
chats.extend(page)
added = 0
for chat in page:
chat_id = clean_id(getattr(chat, "id", None))
if not chat_id or chat_id in seen_chat_ids:
continue
seen_chat_ids.add(chat_id)
chats.append(chat)
added += 1
if len(chats) >= limit:
break
times = [getattr(chat, "last_event_time", None) for chat in page if getattr(chat, "last_event_time", None)]
if not times:
if not times or added == 0:
break
marker = min(times) - 1
if len(page) < 50:
next_marker = min(times) - 1
if next_marker in seen_markers or (marker is not None and next_marker >= marker):
break
seen_markers.add(next_marker)
marker = next_marker
return chats[:limit]
@@ -891,6 +929,33 @@ async def updates(_request: web.Request) -> web.Response:
return json_response(result)
@route_errors
async def contacts(_request: web.Request) -> web.Response:
client = await runtime.get_client()
me_id = get_me_user_id(client)
if me_id is None:
return json_response({"success": False, "error": "MAX profile has no user id."}, status=503)
by_user_id: dict[str, dict[str, Any]] = {}
for contact in client.contacts:
normalized = contact_result(contact, me_id) if contact is not None else None
if normalized is not None:
by_user_id[normalized["userId"]] = normalized
chats = await fetch_chat_pages(client, CHAT_FETCH_LIMIT)
direct_chats = [chat for chat in chats if is_direct_dialog(chat)]
user_map = await build_user_map(client, direct_chats)
for chat in direct_chats:
contact = chat_other_user(chat, user_map, me_id)
normalized = contact_result(contact, me_id, getattr(chat, "id", None)) if contact is not None else None
if normalized is not None:
by_user_id.setdefault(normalized["userId"], normalized)
result = list(by_user_id.values())
result.sort(key=lambda item: item["displayName"].casefold())
return json_response(result)
@route_errors
async def chat_history(request: web.Request) -> web.Response:
data = await read_json(request)
@@ -1017,6 +1082,7 @@ def create_app() -> web.Application:
app.router.add_post("/login/code", login_code)
app.router.add_get("/snapshot", snapshot)
app.router.add_get("/updates", updates)
app.router.add_get("/contacts", contacts)
app.router.add_post("/chat/history", chat_history)
app.router.add_post("/chat/resolve-url", resolve_chat_url)
app.router.add_post("/channels/search", channels_search)