diff --git a/src/adapters/jabber/command_handler.py b/src/adapters/jabber/command_handler.py index 9d3e3cb..bb7905e 100644 --- a/src/adapters/jabber/command_handler.py +++ b/src/adapters/jabber/command_handler.py @@ -1,7 +1,7 @@ import logging from src.domain.admin.entities import Admin -from src.domain.admin.ports import CommandSymbolsRepository, RoomRepository +from src.domain.admin.ports import CommandSymbolsRepository, MucJidResolver, RoomRepository from src.domain.admin.usecases import ( HandleCmd, HandleExit, @@ -38,6 +38,7 @@ class CommandHandler: bot_nick: str, cmd_symbols_repo: CommandSymbolsRepository, room_repo: RoomRepository, + jid_resolver: MucJidResolver, handle_join: HandleJoin, handle_exit: HandleExit, handle_list: HandleList, @@ -53,6 +54,7 @@ class CommandHandler: self._bot_nick = bot_nick.lower() self._cmd_symbols_repo = cmd_symbols_repo self._room_repo = room_repo + self._jid_resolver = jid_resolver self._handle_join = handle_join self._handle_exit = handle_exit self._handle_list = handle_list @@ -115,23 +117,24 @@ class CommandHandler: async def _handle_room(self, msg) -> None: room_jid = msg["from"].bare - # full JID участника в MUC: room@conf/nick → берём nick как идентификатор - # но для проверки прав нужен реальный JID — slixmpp предоставляет его через MUC sender_nick = msg["from"].resource body = msg["body"].strip() if msg["body"] else "" if not body: return + logger.debug("Сообщение в комнате %s от %s: %r", room_jid, sender_nick, body) + # Получаем реальный JID отправителя через MUC plugin - caller_jid = self._get_real_jid(msg) + caller_jid = self._get_real_jid(room_jid, sender_nick) + logger.debug("Реальный JID отправителя: %s", caller_jid) # Проверяем обращение по нику бота - bot_nick_lower = self._bot_nick body_lower = body.lower() - if body_lower.startswith(bot_nick_lower + ",") or body_lower.startswith( - bot_nick_lower + ":" + if body_lower.startswith(self._bot_nick + ",") or body_lower.startswith( + self._bot_nick + ":" ): + logger.debug("Обращение к боту по нику в %s", room_jid) await self._handle_room_help.execute(room_jid, caller_jid or sender_nick) return @@ -148,13 +151,18 @@ class CommandHandler: command = parts[0].lower() arg = parts[1].strip() if len(parts) > 1 else "" + logger.info("Команда в комнате %s от %s: %s %r", room_jid, caller_jid or sender_nick, command, arg) + # help доступен всем if command == "help": await self._handle_room_help.execute(room_jid, caller_jid or sender_nick) return # Остальные команды — только для админа комнаты - if not await self._is_room_admin(room_jid, caller_jid): + is_admin = await self._is_room_admin(room_jid, caller_jid) + logger.debug("Проверка прав: %s admin=%s в %s", caller_jid, is_admin, room_jid) + if not is_admin: + logger.info("Отклонено: %s не является админом %s", caller_jid, room_jid) return if command == "subscribe": @@ -163,6 +171,8 @@ class CommandHandler: elif command == "unsubscribe": if arg: await self._handle_unsubscribe.execute(room_jid, arg) + else: + logger.warning("subscribe без аргумента в %s", room_jid) elif command == "list": await self._handle_list_subs.execute(room_jid) @@ -170,11 +180,17 @@ class CommandHandler: elif command == "cmd": if arg: await self._handle_cmd.execute(room_jid, arg) + else: + logger.warning("cmd без аргумента в %s", room_jid) + + else: + logger.debug("Неизвестная команда %r в %s", command, room_jid) async def _parse_subscribe(self, room_jid: str, arg: str) -> None: """Парсит: [interval_minutes]""" parts = arg.split() if not parts: + logger.warning("subscribe без URL в %s", room_jid) return source = parts[0] interval = Subscription.DEFAULT_INTERVAL_MINUTES @@ -182,7 +198,8 @@ class CommandHandler: try: interval = int(parts[1]) except ValueError: - pass + logger.warning("Неверный интервал %r, используется %d", parts[1], interval) + logger.info("subscribe %s interval=%d в %s", source, interval, room_jid) await self._handle_subscribe.execute(room_jid, source, interval) async def _is_room_admin(self, room_jid: str, caller_jid: str | None) -> bool: @@ -193,14 +210,6 @@ class CommandHandler: return False return room.admin_jid == caller_jid - def _get_real_jid(self, msg) -> str | None: - """Получает реальный JID участника через MUC plugin.""" - try: - muc = msg.get_plugin("xep_0045", None) - if muc is None: - return None - room_jid = msg["from"].bare - nick = msg["from"].resource - return str(muc.get_jid_property(room_jid, nick, "jid")) - except Exception: - return None + def _get_real_jid(self, room_jid: str, nick: str) -> str | None: + """Получает реальный JID участника MUC через resolver.""" + return self._jid_resolver.get_real_jid(room_jid, nick) diff --git a/src/adapters/jabber/connection.py b/src/adapters/jabber/connection.py index 536f538..19cc5bd 100644 --- a/src/adapters/jabber/connection.py +++ b/src/adapters/jabber/connection.py @@ -5,14 +5,14 @@ from typing import Callable, Awaitable, List, Optional from slixmpp import ClientXMPP from slixmpp.exceptions import IqError, IqTimeout -from src.domain.admin.ports import JabberRoomJoiner, JabberRoomLeaver +from src.domain.admin.ports import JabberRoomJoiner, JabberRoomLeaver, MucJidResolver logger = logging.getLogger(__name__) MessageCallback = Callable[["slixmpp.stanza.Message"], Awaitable[None]] -class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver): +class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver, MucJidResolver): """ Единственный владелец XMPP-соединения. Реализует JabberRoomJoiner и JabberRoomLeaver для домена. @@ -29,7 +29,6 @@ class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver): self.add_event_handler("session_start", self._on_session_start) self.add_event_handler("message", self._on_message) - self.add_event_handler("groupchat_message", self._on_message) self.add_event_handler("failed_auth", self._on_failed_auth) def set_message_callback(self, callback: MessageCallback) -> None: @@ -53,9 +52,12 @@ class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver): logger.exception("Не удалось зайти в комнату %s при старте", room_jid) async def _on_message(self, msg) -> None: - # Игнорируем собственные сообщения + # Игнорируем собственные сообщения (1:1 чат) if msg["from"].bare == self.boundjid.bare: return + # Игнорируем собственные сообщения в MUC (отражённые сервером) + if msg["type"] == "groupchat" and msg["from"].resource == self._nick: + return if self._message_callback is not None: try: await self._message_callback(msg) @@ -95,6 +97,18 @@ class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver): logger.warning("Не удалось отправить сообщение в %s", room_jid) return False + def get_real_jid(self, room_jid: str, nick: str) -> str | None: + """Возвращает реальный JID участника MUC по нику, или None если недоступен.""" + try: + muc = self.plugin["xep_0045"] + jid = muc.get_jid_property(room_jid, nick, "jid") + result = str(jid) if jid else None + logger.debug("Реальный JID %s/%s → %s", room_jid, nick, result) + return result + except Exception: + logger.debug("Не удалось получить реальный JID для %s/%s", room_jid, nick) + return None + @property def nick(self) -> str: return self._nick diff --git a/src/domain/admin/ports.py b/src/domain/admin/ports.py index 7bbd156..a7b16a0 100644 --- a/src/domain/admin/ports.py +++ b/src/domain/admin/ports.py @@ -79,3 +79,11 @@ class JabberRoomLeaver(ABC): @abstractmethod async def leave_room(self, room_jid: str) -> None: ... + + +class MucJidResolver(ABC): + """Порт для получения реального JID участника MUC по нику.""" + + @abstractmethod + def get_real_jid(self, room_jid: str, nick: str) -> "str | None": + ... diff --git a/src/main.py b/src/main.py index 3b54588..af49876 100644 --- a/src/main.py +++ b/src/main.py @@ -90,6 +90,7 @@ async def setup() -> tuple: bot_nick=bot_nick, cmd_symbols_repo=cmd_sym_repo, room_repo=room_repo, + jid_resolver=connection, handle_join=handle_join, handle_exit=handle_exit, handle_list=handle_list,