Files
max2telegram/src/telegram_max_commands.py
T
2026-04-09 16:34:44 +03:00

259 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from typing import Any
from pymax import MaxClient
from telegram_api import TelegramClient
logger = logging.getLogger(__name__)
def _is_private_chat(message: dict[str, Any]) -> bool:
chat = message.get("chat")
if not isinstance(chat, dict):
return False
return str(chat.get("type") or "").strip().casefold() == "private"
def _sender_id(message: dict[str, Any]) -> str:
sender = message.get("from")
if not isinstance(sender, dict):
return ""
return str(sender.get("id") or "")
def _chat_id(message: dict[str, Any]) -> str:
chat = message.get("chat")
if not isinstance(chat, dict):
return ""
return str(chat.get("id") or "")
def _infer_max_chat_type(chat_obj: Any) -> str:
"""
Возвращает один из: direct / группа / канал
Best-effort: структура объектов pymax может отличаться между версиями.
"""
# 1) Явные признаки по полям.
for name in ("type", "chat_type", "kind"):
value = getattr(chat_obj, name, None)
if value is None:
continue
v = str(value).strip().casefold()
if v in {"channel", "канал"}:
return "канал"
if v in {"group", "supergroup", "группа"}:
return "группа"
if v in {"direct", "dm", "private"}:
return "direct"
# 2) Флаги.
for true_names, mapped in (
(("is_channel", "channel"), "канал"),
(("is_group", "group"), "группа"),
(("is_direct", "direct"), "direct"),
):
for n in true_names:
try:
if getattr(chat_obj, n, False) is True:
return mapped
except Exception:
continue
# 3) Эвристика по количеству участников.
for n in ("members_count", "participants", "participants_count", "member_count"):
try:
value = getattr(chat_obj, n, None)
except Exception:
value = None
if isinstance(value, int):
return "группа" if value > 2 else "direct"
return "direct"
def _max_chat_title(chat_obj: Any) -> str:
for n in ("title", "name", "chat_title"):
try:
value = getattr(chat_obj, n, None)
except Exception:
value = None
if isinstance(value, str) and value.strip():
return value.strip()
# запасной вариант
cid = getattr(chat_obj, "id", None)
return f"chat {cid}" if cid is not None else "chat"
def _normalize(value: str) -> str:
return str(value or "").strip().casefold()
async def _refresh_chats_best_effort(max_client: MaxClient) -> None:
# group.py: fetch_chats(marker=None) заполняет max_client.chats
try:
fetch = getattr(max_client, "fetch_chats", None)
if fetch is not None:
await fetch()
except Exception:
logger.debug("MAX fetch_chats failed (non-fatal)", exc_info=True)
def _find_chat_by_title(max_client: MaxClient, title: str) -> Any | None:
wanted = _normalize(title)
if not wanted:
return None
chats = list(getattr(max_client, "chats", []) or [])
for c in chats:
if _normalize(_max_chat_title(c)) == wanted:
return c
return None
async def _join_by_link(max_client: MaxClient, link: str) -> str:
"""
PyMax: вступление в группу по ссылке — join_group(link).
См. исходники: https://github.com/MaxApiTeam/PyMax/blob/041dedeb9f9461b3360e2881a8a18a767de74871/src/pymax/mixins/group.py#L260
"""
method = getattr(max_client, "join_group", None)
if method is None:
return "Не смог присоединиться: у клиента MAX нет метода join_group(link)."
try:
await method(link)
return "Ок: вступил по ссылке."
except Exception as e:
logger.exception("MAX join_group failed")
return f"Ошибка при присоединении: {e}"
async def handle_control_command(
message: dict[str, Any],
*,
max_client: MaxClient,
telegram: TelegramClient,
) -> str | None:
"""
Возвращает текст ответа, если команда обработана.
Если None — не команда/не наш случай.
"""
text = str(message.get("text") or "").strip()
if not text.startswith("/"):
return None
if not _is_private_chat(message):
return None
if _sender_id(message) != str(telegram.fallback_user_id):
return None
cmd, *rest = text.split(maxsplit=1)
cmd = cmd.split("@", 1)[0].strip().casefold()
arg = rest[0].strip() if rest else ""
if cmd == "/help":
return (
"Команды управления MAX (только для fallback_user_id в личке):\n"
"/help — справка\n"
"/list — список активных чатов MAX\n"
"/join <LINK> — присоединиться к группе/каналу по ссылке\n"
"/leave <НАЗВАНИЕ> — покинуть указанный канал\n"
"/last_messages <НАЗВАНИЕ> — последние 10 сообщений из канала"
)
if cmd == "/list":
await _refresh_chats_best_effort(max_client)
chats = list(getattr(max_client, "chats", []) or [])
if not chats:
return "Список чатов пуст (или клиент MAX ещё не успел их загрузить)."
lines: list[str] = []
for c in chats:
title = _max_chat_title(c)
ctype = _infer_max_chat_type(c)
lines.append(f"- {title} ({ctype})")
return "Активные чаты MAX:\n" + "\n".join(lines)
if cmd == "/join":
if not arg:
return "Использование: /join <LINK>"
return await _join_by_link(max_client, arg)
if cmd == "/leave":
if not arg:
return "Использование: /leave <НАЗВАНИЕ>"
await _refresh_chats_best_effort(max_client)
chat = _find_chat_by_title(max_client, arg)
if chat is None:
return f"Чат/канал не найден в активных: {arg}"
chat_id = getattr(chat, "id", None)
if chat_id is None:
return "Не смог определить id чата."
ctype = _infer_max_chat_type(chat)
if ctype != "канал":
return f"'{_max_chat_title(chat)}' — это не канал (тип: {ctype})."
method = getattr(max_client, "leave_channel", None)
if method is None:
return "У клиента MAX нет метода leave_channel(chat_id)."
try:
await method(int(chat_id))
return f"Ок: покинул канал '{_max_chat_title(chat)}'."
except Exception as e:
logger.exception("MAX leave_channel failed")
return f"Ошибка при выходе: {e}"
if cmd == "/last_messages":
if not arg:
return "Использование: /last_messages <НАЗВАНИЕ>"
await _refresh_chats_best_effort(max_client)
chat = _find_chat_by_title(max_client, arg)
if chat is None:
return f"Канал не найден в активных: {arg}"
chat_id = getattr(chat, "id", None)
if chat_id is None:
return "Не смог определить id канала."
ctype = _infer_max_chat_type(chat)
if ctype != "канал":
return f"'{_max_chat_title(chat)}' — это не канал (тип: {ctype})."
fetch_history = getattr(max_client, "fetch_history", None)
if fetch_history is None:
return "У клиента MAX нет метода fetch_history(chat_id, backward, forward)."
try:
history = await fetch_history(chat_id=int(chat_id), backward=10, forward=0)
except TypeError:
history = await fetch_history(int(chat_id), 10, 0)
except Exception as e:
logger.exception("MAX fetch_history failed")
return f"Ошибка при получении истории: {e}"
messages = list(history or [])
if not messages:
return f"В канале '{_max_chat_title(chat)}' нет сообщений (или история недоступна)."
lines: list[str] = []
for m in messages[:10]:
text_value = str(getattr(m, "text", "") or "").strip()
mid = getattr(m, "id", None)
if text_value:
lines.append(f"- {mid}: {text_value}")
else:
lines.append(f"- {mid}: <без текста>")
return f"Последние сообщения из '{_max_chat_title(chat)}':\n" + "\n".join(lines)
return (
"Неизвестная команда.\n"
"Набери /help чтобы увидеть доступные команды."
)