This commit is contained in:
bvn13 2026-03-22 19:38:51 +03:00
parent d6ae214cfb
commit 1d0c3b2e6e
4 changed files with 56 additions and 24 deletions

View File

@ -1,7 +1,7 @@
import logging import logging
from src.domain.admin.entities import Admin 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 ( from src.domain.admin.usecases import (
HandleCmd, HandleCmd,
HandleExit, HandleExit,
@ -38,6 +38,7 @@ class CommandHandler:
bot_nick: str, bot_nick: str,
cmd_symbols_repo: CommandSymbolsRepository, cmd_symbols_repo: CommandSymbolsRepository,
room_repo: RoomRepository, room_repo: RoomRepository,
jid_resolver: MucJidResolver,
handle_join: HandleJoin, handle_join: HandleJoin,
handle_exit: HandleExit, handle_exit: HandleExit,
handle_list: HandleList, handle_list: HandleList,
@ -53,6 +54,7 @@ class CommandHandler:
self._bot_nick = bot_nick.lower() self._bot_nick = bot_nick.lower()
self._cmd_symbols_repo = cmd_symbols_repo self._cmd_symbols_repo = cmd_symbols_repo
self._room_repo = room_repo self._room_repo = room_repo
self._jid_resolver = jid_resolver
self._handle_join = handle_join self._handle_join = handle_join
self._handle_exit = handle_exit self._handle_exit = handle_exit
self._handle_list = handle_list self._handle_list = handle_list
@ -115,23 +117,24 @@ class CommandHandler:
async def _handle_room(self, msg) -> None: async def _handle_room(self, msg) -> None:
room_jid = msg["from"].bare room_jid = msg["from"].bare
# full JID участника в MUC: room@conf/nick → берём nick как идентификатор
# но для проверки прав нужен реальный JID — slixmpp предоставляет его через MUC
sender_nick = msg["from"].resource sender_nick = msg["from"].resource
body = msg["body"].strip() if msg["body"] else "" body = msg["body"].strip() if msg["body"] else ""
if not body: if not body:
return return
logger.debug("Сообщение в комнате %s от %s: %r", room_jid, sender_nick, body)
# Получаем реальный JID отправителя через MUC plugin # Получаем реальный 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() body_lower = body.lower()
if body_lower.startswith(bot_nick_lower + ",") or body_lower.startswith( if body_lower.startswith(self._bot_nick + ",") or body_lower.startswith(
bot_nick_lower + ":" self._bot_nick + ":"
): ):
logger.debug("Обращение к боту по нику в %s", room_jid)
await self._handle_room_help.execute(room_jid, caller_jid or sender_nick) await self._handle_room_help.execute(room_jid, caller_jid or sender_nick)
return return
@ -148,13 +151,18 @@ class CommandHandler:
command = parts[0].lower() command = parts[0].lower()
arg = parts[1].strip() if len(parts) > 1 else "" 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 доступен всем # help доступен всем
if command == "help": if command == "help":
await self._handle_room_help.execute(room_jid, caller_jid or sender_nick) await self._handle_room_help.execute(room_jid, caller_jid or sender_nick)
return 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 return
if command == "subscribe": if command == "subscribe":
@ -163,6 +171,8 @@ class CommandHandler:
elif command == "unsubscribe": elif command == "unsubscribe":
if arg: if arg:
await self._handle_unsubscribe.execute(room_jid, arg) await self._handle_unsubscribe.execute(room_jid, arg)
else:
logger.warning("subscribe без аргумента в %s", room_jid)
elif command == "list": elif command == "list":
await self._handle_list_subs.execute(room_jid) await self._handle_list_subs.execute(room_jid)
@ -170,11 +180,17 @@ class CommandHandler:
elif command == "cmd": elif command == "cmd":
if arg: if arg:
await self._handle_cmd.execute(room_jid, 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: async def _parse_subscribe(self, room_jid: str, arg: str) -> None:
"""Парсит: <url> [interval_minutes]""" """Парсит: <url> [interval_minutes]"""
parts = arg.split() parts = arg.split()
if not parts: if not parts:
logger.warning("subscribe без URL в %s", room_jid)
return return
source = parts[0] source = parts[0]
interval = Subscription.DEFAULT_INTERVAL_MINUTES interval = Subscription.DEFAULT_INTERVAL_MINUTES
@ -182,7 +198,8 @@ class CommandHandler:
try: try:
interval = int(parts[1]) interval = int(parts[1])
except ValueError: 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) await self._handle_subscribe.execute(room_jid, source, interval)
async def _is_room_admin(self, room_jid: str, caller_jid: str | None) -> bool: async def _is_room_admin(self, room_jid: str, caller_jid: str | None) -> bool:
@ -193,14 +210,6 @@ class CommandHandler:
return False return False
return room.admin_jid == caller_jid return room.admin_jid == caller_jid
def _get_real_jid(self, msg) -> str | None: def _get_real_jid(self, room_jid: str, nick: str) -> str | None:
"""Получает реальный JID участника через MUC plugin.""" """Получает реальный JID участника MUC через resolver."""
try: return self._jid_resolver.get_real_jid(room_jid, nick)
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

View File

@ -5,14 +5,14 @@ from typing import Callable, Awaitable, List, Optional
from slixmpp import ClientXMPP from slixmpp import ClientXMPP
from slixmpp.exceptions import IqError, IqTimeout 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__) logger = logging.getLogger(__name__)
MessageCallback = Callable[["slixmpp.stanza.Message"], Awaitable[None]] MessageCallback = Callable[["slixmpp.stanza.Message"], Awaitable[None]]
class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver): class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver, MucJidResolver):
""" """
Единственный владелец XMPP-соединения. Единственный владелец XMPP-соединения.
Реализует JabberRoomJoiner и JabberRoomLeaver для домена. Реализует 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("session_start", self._on_session_start)
self.add_event_handler("message", self._on_message) 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) self.add_event_handler("failed_auth", self._on_failed_auth)
def set_message_callback(self, callback: MessageCallback) -> None: def set_message_callback(self, callback: MessageCallback) -> None:
@ -53,9 +52,12 @@ class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver):
logger.exception("Не удалось зайти в комнату %s при старте", room_jid) logger.exception("Не удалось зайти в комнату %s при старте", room_jid)
async def _on_message(self, msg) -> None: async def _on_message(self, msg) -> None:
# Игнорируем собственные сообщения # Игнорируем собственные сообщения (1:1 чат)
if msg["from"].bare == self.boundjid.bare: if msg["from"].bare == self.boundjid.bare:
return return
# Игнорируем собственные сообщения в MUC (отражённые сервером)
if msg["type"] == "groupchat" and msg["from"].resource == self._nick:
return
if self._message_callback is not None: if self._message_callback is not None:
try: try:
await self._message_callback(msg) await self._message_callback(msg)
@ -95,6 +97,18 @@ class JabberConnection(ClientXMPP, JabberRoomJoiner, JabberRoomLeaver):
logger.warning("Не удалось отправить сообщение в %s", room_jid) logger.warning("Не удалось отправить сообщение в %s", room_jid)
return False 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 @property
def nick(self) -> str: def nick(self) -> str:
return self._nick return self._nick

View File

@ -79,3 +79,11 @@ class JabberRoomLeaver(ABC):
@abstractmethod @abstractmethod
async def leave_room(self, room_jid: str) -> None: 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":
...

View File

@ -90,6 +90,7 @@ async def setup() -> tuple:
bot_nick=bot_nick, bot_nick=bot_nick,
cmd_symbols_repo=cmd_sym_repo, cmd_symbols_repo=cmd_sym_repo,
room_repo=room_repo, room_repo=room_repo,
jid_resolver=connection,
handle_join=handle_join, handle_join=handle_join,
handle_exit=handle_exit, handle_exit=handle_exit,
handle_list=handle_list, handle_list=handle_list,