Coverage for slidge/core/dispatcher/util.py: 95%
123 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-22 03:37 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-22 03:37 +0000
1import logging
2from collections.abc import Awaitable, Callable
3from functools import wraps
4from typing import TYPE_CHECKING, TypeVar
6from slixmpp import JID, Iq, Message, Presence
7from slixmpp.exceptions import XMPPError
8from slixmpp.xmlstream import StanzaBase
10from ...contact.roster import ContactIsUser
11from ...util.types import (
12 AnyMUC,
13 AnyRecipient,
14 AnySession,
15 RecipientType,
16)
18if TYPE_CHECKING:
19 from slidge.util.types import AnyGateway
22class Ignore(BaseException):
23 pass
26class DispatcherMixin:
27 __slots__: list[str] = []
28 xmpp: "AnyGateway"
30 def __init__(self, xmpp: "AnyGateway") -> None:
31 self.xmpp = xmpp # type:ignore[misc]
33 async def _get_session(
34 self,
35 stanza: Message | Presence | Iq,
36 timeout: int | None = 10,
37 wait_for_ready: bool = True,
38 logged: bool = False,
39 ) -> AnySession:
40 xmpp = self.xmpp
41 if stanza.get_from().server == xmpp.boundjid.bare:
42 log.debug("Ignoring echo")
43 raise Ignore
44 if (
45 isinstance(stanza, Message)
46 and stanza.get_type() == "chat"
47 and stanza.get_to() == xmpp.boundjid.bare
48 ):
49 log.debug("Ignoring message to component")
50 raise Ignore
51 session = await self._get_session_from_jid(
52 stanza.get_from(), timeout, wait_for_ready, logged
53 )
54 if isinstance(stanza, Message) and _ignore(session, stanza):
55 raise Ignore
56 return session
58 async def _get_session_from_jid(
59 self,
60 jid: JID,
61 timeout: int | None = 10,
62 wait_for_ready: bool = True,
63 logged: bool = False,
64 ) -> AnySession:
65 session = self.xmpp.get_session_from_jid(jid)
66 if session is None:
67 raise XMPPError("registration-required")
68 if logged:
69 session.raise_if_not_logged()
70 if wait_for_ready:
71 await session.wait_for_ready(timeout)
72 return session
74 async def get_muc_from_stanza(self, iq: Iq | Message | Presence) -> AnyMUC:
75 ito = iq.get_to()
76 if ito == self.xmpp.boundjid.bare:
77 raise XMPPError("bad-request", text="This is only handled for MUCs")
79 session = await self._get_session(iq, logged=True)
80 muc = await session.bookmarks.by_jid(ito)
81 return muc # type:ignore[no-any-return]
83 def _xmpp_msg_id_to_legacy(
84 self,
85 xmpp_id: str,
86 recipient: AnyRecipient,
87 origin: bool = False,
88 ) -> str:
89 with self.xmpp.store.session() as orm:
90 sent = self.xmpp.store.id_map.get_legacy(
91 orm, recipient.stored.id, xmpp_id, recipient.is_group, origin
92 )
93 if sent is not None:
94 return sent
96 return xmpp_id
98 async def _get_recipient_and_thread(
99 self, msg: Message
100 ) -> tuple[AnyRecipient, str | None]:
101 session = await self._get_session(msg)
102 e: AnyRecipient = await get_recipient(session, msg)
103 legacy_thread = await self._xmpp_to_legacy_thread(session, msg, e)
104 return e, legacy_thread
106 async def _xmpp_to_legacy_thread(
107 self, session: AnySession, msg: Message, recipient: RecipientType
108 ) -> str | None:
109 if not self.xmpp.THREADS:
110 return None
112 xmpp_thread = msg["thread"]
113 if not xmpp_thread:
114 return None
116 with session.xmpp.store.session() as orm:
117 legacy_thread_str = session.xmpp.store.id_map.get_thread(
118 orm, recipient.stored.id, xmpp_thread, recipient.is_group
119 )
120 if legacy_thread_str is not None:
121 return legacy_thread_str
122 async with session.thread_creation_lock:
123 if "reply" in msg and "id" in msg["reply"]:
124 root_legacy_id = self._xmpp_msg_id_to_legacy(
125 msg["reply"]["id"], recipient
126 )
127 else:
128 root_legacy_id = self._xmpp_msg_id_to_legacy(
129 xmpp_thread,
130 recipient,
131 )
132 legacy_thread = await recipient.create_thread(
133 xmpp_thread,
134 root_legacy_id,
135 )
136 with session.xmpp.store.session() as orm:
137 session.xmpp.store.id_map.set_thread(
138 orm,
139 recipient.stored.id,
140 str(legacy_thread),
141 xmpp_thread,
142 recipient.is_group,
143 )
144 orm.commit()
145 return legacy_thread
148def _ignore(session: AnySession, msg: Message) -> bool:
149 i = msg.get_id()
150 if i.startswith("slidge-carbon-"):
151 return True
152 if i not in session.ignore_messages:
153 return False
154 session.log.debug("Ignored sent carbon: %s", i)
155 session.ignore_messages.remove(i)
156 return True
159async def get_recipient(session: AnySession, m: Message) -> AnyRecipient:
160 session.raise_if_not_logged()
161 if m.get_type() == "groupchat":
162 muc = await session.bookmarks.by_jid(m.get_to())
163 r = m.get_from().resource
164 if r not in muc.get_user_resources():
165 session.create_task(muc.kick_resource(r), name=f"kick of {r} from {muc}")
166 raise XMPPError("not-acceptable", "You are not connected to this chat")
167 return muc # type:ignore[no-any-return]
168 else:
169 return await session.contacts.by_jid(m.get_to()) # type:ignore[no-any-return]
172SelfType = TypeVar("SelfType")
173StanzaType = TypeVar("StanzaType", bound=StanzaBase)
174HandlerType = Callable[[SelfType, StanzaType], Awaitable[None]]
177def exceptions_to_xmpp_errors[SelfType, StanzaType: StanzaBase](
178 cb: HandlerType[SelfType, StanzaType],
179) -> HandlerType[SelfType, StanzaType]:
180 @wraps(cb)
181 async def wrapped(self: SelfType, stanza: StanzaType) -> None:
182 try:
183 await cb(self, stanza)
184 except Ignore:
185 pass
186 except XMPPError:
187 raise
188 except NotImplementedError as e:
189 log.debug("NotImplementedError raised in %s", cb)
190 tb = e.__traceback__
191 assert tb is not None
192 while tb.tb_next is not None:
193 tb = tb.tb_next
194 frame = tb.tb_frame
195 method_name = frame.f_code.co_name
196 self_obj = frame.f_locals.get("self")
197 if self_obj is not None:
198 method_name = f"{self_obj.__class__.__name__}.{method_name}"
199 raise XMPPError(
200 "feature-not-implemented",
201 f"{method_name} is not implemented by the legacy module",
202 clear=False,
203 )
204 except ContactIsUser:
205 raise XMPPError(
206 "bad-request", "Actions with your bridged self are not allowed."
207 )
208 except Exception as e:
209 log.error(
210 "Failed to handle incoming stanza: %s - %s", self, stanza, exc_info=e
211 )
212 raise XMPPError("internal-server-error", str(e))
214 return wrapped
217log = logging.getLogger(__name__)