Coverage for slidge / core / dispatcher / muc / misc.py: 93%
75 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 05:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 05:07 +0000
1import logging
2import typing
4from slixmpp import (
5 JID,
6 CoroutineCallback,
7 Iq,
8 MatchXMLMask,
9 Message,
10 Presence,
11 StanzaPath,
12)
13from slixmpp.exceptions import XMPPError
15from ..util import DispatcherMixin, exceptions_to_xmpp_errors
17if typing.TYPE_CHECKING:
18 from slidge.core.gateway import BaseGateway
21class MucMiscMixin(DispatcherMixin):
22 __slots__: list[str] = []
24 def __init__(self, xmpp: "BaseGateway") -> None:
25 super().__init__(xmpp)
26 xmpp.register_handler(
27 CoroutineCallback(
28 "ibr_remove", StanzaPath("/iq/register"), self.on_ibr_remove
29 )
30 )
32 xmpp.add_event_handler("groupchat_join", self.on_groupchat_join)
33 xmpp.add_event_handler(
34 "groupchat_direct_invite", self.on_groupchat_direct_invite
35 )
36 xmpp.add_event_handler("groupchat_subject", self.on_groupchat_subject)
37 xmpp.add_event_handler("groupchat_message_error", self.__on_group_chat_error)
38 xmpp.register_handler(
39 CoroutineCallback(
40 "muc_thread_subject",
41 MatchXMLMask(
42 "<message xmlns='jabber:component:accept' type='groupchat'><subject/><thread/></message>"
43 ),
44 self.on_thread_subject,
45 )
46 )
48 async def __on_group_chat_error(self, msg: Message) -> None:
49 condition = msg["error"].get_condition()
50 if condition not in KICKABLE_ERRORS:
51 return
53 try:
54 muc = await self.get_muc_from_stanza(msg)
55 except XMPPError as e:
56 log.debug("Not removing resource", exc_info=e)
57 return
58 mfrom = msg.get_from()
59 resource = mfrom.resource
60 try:
61 muc.remove_user_resource(resource)
62 except KeyError:
63 # this actually happens quite frequently on for both beagle and monal
64 # (not sure why?), but is of no consequence
65 log.debug("%s was not in the resources of %s", resource, muc)
66 else:
67 log.debug(
68 "Removed %s from the resources of %s because of error", resource, muc
69 )
71 @exceptions_to_xmpp_errors
72 async def on_ibr_remove(self, iq: Iq) -> None:
73 if iq.get_to() == self.xmpp.boundjid.bare:
74 return
76 if iq["type"] == "set" and iq["register"]["remove"]:
77 muc = await self.get_muc_from_stanza(iq)
78 await muc.session.on_leave_group(muc.legacy_id)
79 iq.reply().send()
80 await muc.session.bookmarks.remove(
81 muc, "You left this chat from an XMPP client."
82 )
83 return
85 raise XMPPError("feature-not-implemented")
87 @exceptions_to_xmpp_errors
88 async def on_groupchat_join(self, p: Presence) -> None:
89 if not self.xmpp.GROUPS:
90 raise XMPPError(
91 "feature-not-implemented",
92 "This gateway does not implement multi-user chats.",
93 )
94 muc = await self.get_muc_from_stanza(p)
95 await muc.join(p)
97 @exceptions_to_xmpp_errors
98 async def on_groupchat_direct_invite(self, msg: Message) -> None:
99 invite = msg["groupchat_invite"]
100 jid = JID(invite["jid"])
102 if jid.domain != self.xmpp.boundjid.bare:
103 raise XMPPError(
104 "bad-request",
105 "Legacy contacts can only be invited to legacy groups, not standard XMPP MUCs.",
106 )
108 if invite["password"]:
109 raise XMPPError(
110 "bad-request", "Password-protected groups are not supported"
111 )
113 session = await self._get_session(msg, logged=True)
114 contact = await session.contacts.by_jid(msg.get_to())
115 muc = await session.bookmarks.by_jid(jid)
117 await session.on_invitation(contact, muc, invite["reason"] or None)
119 @exceptions_to_xmpp_errors
120 async def on_groupchat_subject(self, msg: Message) -> None:
121 muc = await self.get_muc_from_stanza(msg)
122 if not muc.HAS_SUBJECT:
123 raise XMPPError(
124 "bad-request",
125 "There are no room subject in here. "
126 "Use the room configuration to update its name or description",
127 )
128 await muc.on_set_subject(msg["subject"])
130 @exceptions_to_xmpp_errors
131 async def on_thread_subject(self, msg: Message) -> None:
132 if msg["body"]:
133 return
134 _session, muc, thread = await self._get_session_recipient_thread(msg)
135 assert thread is not None
136 await muc.on_set_thread_subject(thread, msg["subject"]) # type:ignore[union-attr]
139KICKABLE_ERRORS = {
140 "gone",
141 "internal-server-error",
142 "item-not-found",
143 "jid-malformed",
144 "recipient-unavailable",
145 "redirect",
146 "remote-server-not-found",
147 "remote-server-timeout",
148 "service-unavailable",
149 "malformed error",
150}
152log = logging.getLogger(__name__)