Coverage for slidge/core/session.py: 84%
256 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-26 19:34 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-26 19:34 +0000
1import asyncio
2import logging
3from typing import (
4 TYPE_CHECKING,
5 Any,
6 Generic,
7 Iterable,
8 NamedTuple,
9 Optional,
10 Type,
11 Union,
12 cast,
13)
15import aiohttp
16import sqlalchemy as sa
17from slixmpp import JID, Message
18from slixmpp.exceptions import XMPPError
19from slixmpp.types import PresenceShows
21from ..command import SearchResult
22from ..contact import LegacyContact, LegacyRoster
23from ..db.models import Contact, GatewayUser
24from ..group.bookmarks import LegacyBookmarks
25from ..group.room import LegacyMUC
26from ..util import ABCSubclassableOnceAtMost
27from ..util.lock import NamedLockMixin
28from ..util.types import (
29 LegacyGroupIdType,
30 LegacyMessageType,
31 LegacyThreadType,
32 LinkPreview,
33 Mention,
34 PseudoPresenceShow,
35 RecipientType,
36 ResourceDict,
37 Sticker,
38)
39from ..util.util import deprecated, noop_coro
41if TYPE_CHECKING:
42 from ..group.participant import LegacyParticipant
43 from ..util.types import Sender
44 from .gateway import BaseGateway
47class CachedPresence(NamedTuple):
48 status: Optional[str]
49 show: Optional[str]
50 kwargs: dict[str, Any]
53class BaseSession(
54 Generic[LegacyMessageType, RecipientType],
55 NamedLockMixin,
56 metaclass=ABCSubclassableOnceAtMost,
57):
58 """
59 The session of a registered :term:`User`.
61 Represents a gateway user logged in to the legacy network and performing actions.
63 Will be instantiated automatically on slidge startup for each registered user,
64 or upon registration for new (validated) users.
66 Must be subclassed for a functional :term:`Legacy Module`.
67 """
69 """
70 Since we cannot set the XMPP ID of messages sent by XMPP clients, we need to keep a mapping
71 between XMPP IDs and legacy message IDs if we want to further refer to a message that was sent
72 by the user. This also applies to 'carboned' messages, ie, messages sent by the user from
73 the official client of a legacy network.
74 """
76 xmpp: "BaseGateway"
77 """
78 The gateway instance singleton. Use it for low-level XMPP calls or custom methods that are not
79 session-specific.
80 """
82 MESSAGE_IDS_ARE_THREAD_IDS = False
83 """
84 Set this to True if the legacy service uses message IDs as thread IDs,
85 eg Mattermost, where you can only 'create a thread' by replying to the message,
86 in which case the message ID is also a thread ID (and all messages are potential
87 threads).
88 """
89 SPECIAL_MSG_ID_PREFIX: Optional[str] = None
90 """
91 If you set this, XMPP message IDs starting with this won't be converted to legacy ID,
92 but passed as is to :meth:`.on_react`, and usual checks for emoji restriction won't be
93 applied.
94 This can be used to implement voting in polls in a hacky way.
95 """
97 _roster_cls: Type[LegacyRoster]
98 _bookmarks_cls: Type[LegacyBookmarks]
100 def __init__(self, user: GatewayUser) -> None:
101 super().__init__()
102 self.user = user
103 self.log = logging.getLogger(user.jid.bare)
105 self.ignore_messages = set[str]()
107 self.contacts = self._roster_cls(self)
108 self.is_logging_in = False
109 self._logged = False
110 self.__reset_ready()
112 self.bookmarks = self._bookmarks_cls(self)
114 self.thread_creation_lock = asyncio.Lock()
116 self.__cached_presence: Optional[CachedPresence] = None
118 self.__tasks = set[asyncio.Task]()
120 @property
121 def user_jid(self) -> JID:
122 return self.user.jid
124 @property
125 def user_pk(self) -> int:
126 return self.user.id
128 @property
129 def http(self) -> aiohttp.ClientSession:
130 return self.xmpp.http
132 def __remove_task(self, fut) -> None:
133 self.log.debug("Removing fut %s", fut)
134 self.__tasks.remove(fut)
136 def create_task(self, coro, name: str | None = None) -> asyncio.Task:
137 task = self.xmpp.loop.create_task(coro, name=name)
138 self.__tasks.add(task)
139 self.log.debug("Creating task %s", task)
140 task.add_done_callback(lambda _: self.__remove_task(task))
141 return task
143 def cancel_all_tasks(self) -> None:
144 for task in self.__tasks:
145 task.cancel()
147 async def login(self) -> Optional[str]:
148 """
149 Logs in the gateway user to the legacy network.
151 Triggered when the gateway start and on user registration.
152 It is recommended that this function returns once the user is logged in,
153 so if you need to await forever (for instance to listen to incoming events),
154 it's a good idea to wrap your listener in an asyncio.Task.
156 :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'"
157 """
158 raise NotImplementedError
160 async def logout(self) -> None:
161 """
162 Logs out the gateway user from the legacy network.
164 Called on gateway shutdown.
165 """
166 raise NotImplementedError
168 async def on_text(
169 self,
170 chat: RecipientType,
171 text: str,
172 *,
173 reply_to_msg_id: Optional[LegacyMessageType] = None,
174 reply_to_fallback_text: Optional[str] = None,
175 reply_to: Optional["Sender"] = None,
176 thread: Optional[LegacyThreadType] = None,
177 link_previews: Iterable[LinkPreview] = (),
178 mentions: Optional[list[Mention]] = None,
179 ) -> Optional[LegacyMessageType]:
180 """
181 Triggered when the user sends a text message from XMPP to a bridged entity, e.g.
182 to ``translated_user_name@slidge.example.com``, or ``translated_group_name@slidge.example.com``
184 Override this and implement sending a message to the legacy network in this method.
186 :param text: Content of the message
187 :param chat: Recipient of the message. :class:`.LegacyContact` instance for 1:1 chat,
188 :class:`.MUC` instance for groups.
189 :param reply_to_msg_id: A legacy message ID if the message references (quotes)
190 another message (:xep:`0461`)
191 :param reply_to_fallback_text: Content of the quoted text. Not necessarily set
192 by XMPP clients
193 :param reply_to: Author of the quoted message. :class:`LegacyContact` instance for
194 1:1 chat, :class:`LegacyParticipant` instance for groups.
195 If `None`, should be interpreted as a self-reply if reply_to_msg_id is not None.
196 :param link_previews: A list of sender-generated link previews.
197 At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
198 supports it.
199 :param mentions: (only for groups) A list of Contacts mentioned by their
200 nicknames.
201 :param thread:
203 :return: An ID of some sort that can be used later to ack and mark the message
204 as read by the user
205 """
206 raise NotImplementedError
208 send_text = deprecated("BaseSession.send_text", on_text)
210 async def on_file(
211 self,
212 chat: RecipientType,
213 url: str,
214 *,
215 http_response: aiohttp.ClientResponse,
216 reply_to_msg_id: Optional[LegacyMessageType] = None,
217 reply_to_fallback_text: Optional[str] = None,
218 reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None,
219 thread: Optional[LegacyThreadType] = None,
220 ) -> Optional[LegacyMessageType]:
221 """
222 Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
224 :param url: URL of the file
225 :param chat: See :meth:`.BaseSession.on_text`
226 :param http_response: The HTTP GET response object on the URL
227 :param reply_to_msg_id: See :meth:`.BaseSession.on_text`
228 :param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
229 :param reply_to: See :meth:`.BaseSession.on_text`
230 :param thread:
232 :return: An ID of some sort that can be used later to ack and mark the message
233 as read by the user
234 """
235 raise NotImplementedError
237 send_file = deprecated("BaseSession.send_file", on_file)
239 async def on_sticker(
240 self,
241 chat: RecipientType,
242 sticker: Sticker,
243 *,
244 reply_to_msg_id: Optional[LegacyMessageType] = None,
245 reply_to_fallback_text: Optional[str] = None,
246 reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None,
247 thread: Optional[LegacyThreadType] = None,
248 ) -> Optional[LegacyMessageType]:
249 """
250 Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
252 :param chat: See :meth:`.BaseSession.on_text`
253 :param sticker: The sticker sent by the user.
254 :param reply_to_msg_id: See :meth:`.BaseSession.on_text`
255 :param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
256 :param reply_to: See :meth:`.BaseSession.on_text`
257 :param thread:
259 :return: An ID of some sort that can be used later to ack and mark the message
260 as read by the user
261 """
262 raise NotImplementedError
264 async def on_active(
265 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
266 ):
267 """
268 Triggered when the user sends an 'active' chat state (:xep:`0085`)
270 :param chat: See :meth:`.BaseSession.on_text`
271 :param thread:
272 """
273 raise NotImplementedError
275 active = deprecated("BaseSession.active", on_active)
277 async def on_inactive(
278 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
279 ):
280 """
281 Triggered when the user sends an 'inactive' chat state (:xep:`0085`)
283 :param chat: See :meth:`.BaseSession.on_text`
284 :param thread:
285 """
286 raise NotImplementedError
288 inactive = deprecated("BaseSession.inactive", on_inactive)
290 async def on_composing(
291 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
292 ):
293 """
294 Triggered when the user starts typing in a legacy chat (:xep:`0085`)
296 :param chat: See :meth:`.BaseSession.on_text`
297 :param thread:
298 """
299 raise NotImplementedError
301 composing = deprecated("BaseSession.composing", on_composing)
303 async def on_paused(
304 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
305 ):
306 """
307 Triggered when the user pauses typing in a legacy chat (:xep:`0085`)
309 :param chat: See :meth:`.BaseSession.on_text`
310 :param thread:
311 """
312 raise NotImplementedError
314 paused = deprecated("BaseSession.paused", on_paused)
316 async def on_gone(
317 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
318 ):
319 """
320 Triggered when the user is "gone" in a legacy chat (:xep:`0085`)
322 :param chat: See :meth:`.BaseSession.on_text`
323 :param thread:
324 """
325 raise NotImplementedError
327 async def on_displayed(
328 self,
329 chat: RecipientType,
330 legacy_msg_id: LegacyMessageType,
331 thread: Optional[LegacyThreadType] = None,
332 ):
333 """
334 Triggered when the user reads a message in a legacy chat. (:xep:`0333`)
336 This is only possible if a valid ``legacy_msg_id`` was passed when
337 transmitting a message from a legacy chat to the user, eg in
338 :meth:`slidge.contact.LegacyContact.send_text`
339 or
340 :meth:`slidge.group.LegacyParticipant.send_text`.
342 :param chat: See :meth:`.BaseSession.on_text`
343 :param legacy_msg_id: Identifier of the message/
344 :param thread:
345 """
346 raise NotImplementedError
348 displayed = deprecated("BaseSession.displayed", on_displayed)
350 async def on_correct(
351 self,
352 chat: RecipientType,
353 text: str,
354 legacy_msg_id: LegacyMessageType,
355 *,
356 thread: Optional[LegacyThreadType] = None,
357 link_previews: Iterable[LinkPreview] = (),
358 mentions: Optional[list[Mention]] = None,
359 ) -> Optional[LegacyMessageType]:
360 """
361 Triggered when the user corrects a message using :xep:`0308`
363 This is only possible if a valid ``legacy_msg_id`` was returned by
364 :meth:`.on_text`.
366 :param chat: See :meth:`.BaseSession.on_text`
367 :param text: The new text
368 :param legacy_msg_id: Identifier of the edited message
369 :param thread:
370 :param link_previews: A list of sender-generated link previews.
371 At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
372 supports it.
373 :param mentions: (only for groups) A list of Contacts mentioned by their
374 nicknames.
375 """
376 raise NotImplementedError
378 correct = deprecated("BaseSession.correct", on_correct)
380 async def on_react(
381 self,
382 chat: RecipientType,
383 legacy_msg_id: LegacyMessageType,
384 emojis: list[str],
385 thread: Optional[LegacyThreadType] = None,
386 ):
387 """
388 Triggered when the user sends message reactions (:xep:`0444`).
390 :param chat: See :meth:`.BaseSession.on_text`
391 :param thread:
392 :param legacy_msg_id: ID of the message the user reacts to
393 :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``.
394 An empty string means "no reaction", ie, remove all reactions if any were present before
395 """
396 raise NotImplementedError
398 react = deprecated("BaseSession.react", on_react)
400 async def on_retract(
401 self,
402 chat: RecipientType,
403 legacy_msg_id: LegacyMessageType,
404 thread: Optional[LegacyThreadType] = None,
405 ):
406 """
407 Triggered when the user retracts (:xep:`0424`) a message.
409 :param chat: See :meth:`.BaseSession.on_text`
410 :param thread:
411 :param legacy_msg_id: Legacy ID of the retracted message
412 """
413 raise NotImplementedError
415 retract = deprecated("BaseSession.retract", on_retract)
417 async def on_presence(
418 self,
419 resource: str,
420 show: PseudoPresenceShow,
421 status: str,
422 resources: dict[str, ResourceDict],
423 merged_resource: Optional[ResourceDict],
424 ):
425 """
426 Called when the gateway component receives a presence, ie, when
427 one of the user's clients goes online of offline, or changes its
428 status.
430 :param resource: The XMPP client identifier, arbitrary string.
431 :param show: The presence ``<show>``, if available. If the resource is
432 just 'available' without any ``<show>`` element, this is an empty
433 str.
434 :param status: A status message, like a deeply profound quote, eg,
435 "Roses are red, violets are blue, [INSERT JOKE]".
436 :param resources: A summary of all the resources for this user.
437 :param merged_resource: A global presence for the user account,
438 following rules described in :meth:`merge_resources`
439 """
440 raise NotImplementedError
442 presence = deprecated("BaseSession.presence", on_presence)
444 async def on_search(self, form_values: dict[str, str]) -> Optional[SearchResult]:
445 """
446 Triggered when the user uses Jabber Search (:xep:`0055`) on the component
448 Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS`
450 :param form_values: search query, defined for a specific plugin by overriding
451 in :attr:`.BaseGateway.SEARCH_FIELDS`
452 :return:
453 """
454 raise NotImplementedError
456 search = deprecated("BaseSession.search", on_search)
458 async def on_avatar(
459 self,
460 bytes_: Optional[bytes],
461 hash_: Optional[str],
462 type_: Optional[str],
463 width: Optional[int],
464 height: Optional[int],
465 ) -> None:
466 """
467 Triggered when the user uses modifies their avatar via :xep:`0084`.
469 :param bytes_: The data of the avatar. According to the spec, this
470 should always be a PNG, but some implementations do not respect
471 that. If `None` it means the user has unpublished their avatar.
472 :param hash_: The SHA1 hash of the avatar data. This is an identifier of
473 the avatar.
474 :param type_: The MIME type of the avatar.
475 :param width: The width of the avatar image.
476 :param height: The height of the avatar image.
477 """
478 raise NotImplementedError
480 async def on_moderate(
481 self, muc: LegacyMUC, legacy_msg_id: LegacyMessageType, reason: Optional[str]
482 ):
483 """
484 Triggered when the user attempts to retract a message that was sent in
485 a MUC using :xep:`0425`.
487 If retraction is not possible, this should raise the appropriate
488 XMPPError with a human-readable message.
490 NB: the legacy module is responsible for calling
491 :method:`LegacyParticipant.moderate` when this is successful, because
492 slidge will acknowledge the moderation IQ, but will not send the
493 moderation message from the MUC automatically.
495 :param muc: The MUC in which the message was sent
496 :param legacy_msg_id: The legacy ID of the message to be retracted
497 :param reason: Optionally, a reason for the moderation, given by the
498 user-moderator.
499 """
500 raise NotImplementedError
502 async def on_create_group(
503 self, name: str, contacts: list[LegacyContact]
504 ) -> LegacyGroupIdType:
505 """
506 Triggered when the user request the creation of a group via the
507 dedicated :term:`Command`.
509 :param name: Name of the group
510 :param contacts: list of contacts that should be members of the group
511 """
512 raise NotImplementedError
514 async def on_invitation(
515 self, contact: LegacyContact, muc: LegacyMUC, reason: Optional[str]
516 ) -> None:
517 """
518 Triggered when the user invites a :term:`Contact` to a legacy MUC via
519 :xep:`0249`.
521 The default implementation calls :meth:`LegacyMUC.on_set_affiliation`
522 with the 'member' affiliation. Override if you want to customize this
523 behaviour.
525 :param contact: The invitee
526 :param muc: The group
527 :param reason: Optionally, a reason
528 """
529 await muc.on_set_affiliation(contact, "member", reason, None)
531 async def on_leave_group(self, muc_legacy_id: LegacyGroupIdType):
532 """
533 Triggered when the user leaves a group via the dedicated slidge command
534 or the :xep:`0077` ``<remove />`` mechanism.
536 This should be interpreted as definitely leaving the group.
538 :param muc_legacy_id: The legacy ID of the group to leave
539 """
540 raise NotImplementedError
542 async def on_preferences(
543 self, previous: dict[str, Any], new: dict[str, Any]
544 ) -> None:
545 """
546 This is called when the user updates their preferences.
548 Override this if you need set custom preferences field and need to trigger
549 something when a preference has changed.
550 """
551 raise NotImplementedError
553 def __reset_ready(self) -> None:
554 self.ready = self.xmpp.loop.create_future()
556 @property
557 def logged(self):
558 return self._logged
560 @logged.setter
561 def logged(self, v: bool) -> None:
562 self.is_logging_in = False
563 self._logged = v
564 if self.ready.done():
565 if v:
566 return
567 self.__reset_ready()
568 self.shutdown(logout=False)
569 else:
570 if v:
571 self.ready.set_result(True)
573 def __repr__(self) -> str:
574 return f"<Session of {self.user_jid}>"
576 def shutdown(self, logout: bool = True) -> asyncio.Task:
577 for m in self.bookmarks:
578 m.shutdown()
579 with self.xmpp.store.session() as orm:
580 for jid in orm.execute(
581 sa.select(Contact.jid).filter_by(user=self.user, is_friend=True)
582 ).scalars():
583 pres = self.xmpp.make_presence(
584 pfrom=jid,
585 pto=self.user_jid,
586 ptype="unavailable",
587 pstatus="Gateway has shut down.",
588 )
589 pres.send()
590 if logout:
591 return self.xmpp.loop.create_task(self.__logout())
592 else:
593 return self.xmpp.loop.create_task(noop_coro())
595 async def __logout(self) -> None:
596 try:
597 await self.logout()
598 except NotImplementedError:
599 pass
601 @staticmethod
602 def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
603 """
604 Convert a legacy msg ID to a valid XMPP msg ID.
605 Needed for read marks, retractions and message corrections.
607 The default implementation just converts the legacy ID to a :class:`str`,
608 but this should be overridden in case some characters needs to be escaped,
609 or to add some additional,
610 :term:`legacy network <Legacy Network`>-specific logic.
612 :param legacy_msg_id:
613 :return: A string that is usable as an XMPP stanza ID
614 """
615 return str(legacy_msg_id)
617 legacy_msg_id_to_xmpp_msg_id = staticmethod(
618 deprecated("BaseSession.legacy_msg_id_to_xmpp_msg_id", legacy_to_xmpp_msg_id)
619 )
621 @staticmethod
622 def xmpp_to_legacy_msg_id(i: str) -> LegacyMessageType:
623 """
624 Convert a legacy XMPP ID to a valid XMPP msg ID.
625 Needed for read marks and message corrections.
627 The default implementation just converts the legacy ID to a :class:`str`,
628 but this should be overridden in case some characters needs to be escaped,
629 or to add some additional,
630 :term:`legacy network <Legacy Network`>-specific logic.
632 The default implementation is an identity function.
634 :param i: The XMPP stanza ID
635 :return: An ID that can be used to identify a message on the legacy network
636 """
637 return cast(LegacyMessageType, i)
639 xmpp_msg_id_to_legacy_msg_id = staticmethod(
640 deprecated("BaseSession.xmpp_msg_id_to_legacy_msg_id", xmpp_to_legacy_msg_id)
641 )
643 def raise_if_not_logged(self):
644 if not self.logged:
645 raise XMPPError(
646 "internal-server-error",
647 text="You are not logged to the legacy network",
648 )
650 @classmethod
651 def _from_user_or_none(cls, user):
652 if user is None:
653 log.debug("user not found")
654 raise XMPPError(text="User not found", condition="subscription-required")
656 session = _sessions.get(user.jid.bare)
657 if session is None:
658 _sessions[user.jid.bare] = session = cls(user)
659 return session
661 @classmethod
662 def from_user(cls, user):
663 return cls._from_user_or_none(user)
665 @classmethod
666 def from_stanza(cls, s) -> "BaseSession":
667 # """
668 # Get a user's :class:`.LegacySession` using the "from" field of a stanza
669 #
670 # Meant to be called from :class:`BaseGateway` only.
671 #
672 # :param s:
673 # :return:
674 # """
675 return cls.from_jid(s.get_from())
677 @classmethod
678 def from_jid(cls, jid: JID) -> "BaseSession":
679 # """
680 # Get a user's :class:`.LegacySession` using its jid
681 #
682 # Meant to be called from :class:`BaseGateway` only.
683 #
684 # :param jid:
685 # :return:
686 # """
687 session = _sessions.get(jid.bare)
688 if session is not None:
689 return session
690 with cls.xmpp.store.session() as orm:
691 user = orm.query(GatewayUser).filter_by(jid=jid.bare).one_or_none()
692 return cls._from_user_or_none(user)
694 @classmethod
695 async def kill_by_jid(cls, jid: JID) -> None:
696 # """
697 # Terminate a user session.
698 #
699 # Meant to be called from :class:`BaseGateway` only.
700 #
701 # :param jid:
702 # :return:
703 # """
704 log.debug("Killing session of %s", jid)
705 for user_jid, session in _sessions.items():
706 if user_jid == jid.bare:
707 break
708 else:
709 log.debug("Did not find a session for %s", jid)
710 return
711 for c in session.contacts:
712 c.unsubscribe()
713 for m in session.bookmarks:
714 m.shutdown()
716 try:
717 session = _sessions.pop(jid.bare)
718 except KeyError:
719 log.warning("User not found during unregistration")
720 return
722 session.cancel_all_tasks()
724 await cls.xmpp.unregister(session)
725 with cls.xmpp.store.session() as orm:
726 orm.delete(session.user)
727 orm.commit()
729 def __ack(self, msg: Message) -> None:
730 if not self.xmpp.PROPER_RECEIPTS:
731 self.xmpp.delivery_receipt.ack(msg)
733 def send_gateway_status(
734 self,
735 status: Optional[str] = None,
736 show=Optional[PresenceShows],
737 **kwargs,
738 ) -> None:
739 """
740 Send a presence from the gateway to the user.
742 Can be used to indicate the user session status, ie "SMS code required", "connected", …
744 :param status: A status message
745 :param show: Presence stanza 'show' element. I suggest using "dnd" to show
746 that the gateway is not fully functional
747 """
748 self.__cached_presence = CachedPresence(status, show, kwargs)
749 self.xmpp.send_presence(
750 pto=self.user_jid.bare, pstatus=status, pshow=show, **kwargs
751 )
753 def send_cached_presence(self, to: JID) -> None:
754 if not self.__cached_presence:
755 self.xmpp.send_presence(pto=to, ptype="unavailable")
756 return
757 self.xmpp.send_presence(
758 pto=to,
759 pstatus=self.__cached_presence.status,
760 pshow=self.__cached_presence.show,
761 **self.__cached_presence.kwargs,
762 )
764 def send_gateway_message(self, text: str, **msg_kwargs) -> None:
765 """
766 Send a message from the gateway component to the user.
768 Can be used to indicate the user session status, ie "SMS code required", "connected", …
770 :param text: A text
771 """
772 self.xmpp.send_text(text, mto=self.user_jid, **msg_kwargs)
774 def send_gateway_invite(
775 self,
776 muc: LegacyMUC,
777 reason: Optional[str] = None,
778 password: Optional[str] = None,
779 ) -> None:
780 """
781 Send an invitation to join a MUC, emanating from the gateway component.
783 :param muc:
784 :param reason:
785 :param password:
786 """
787 self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user_jid)
789 async def input(self, text: str, **msg_kwargs):
790 """
791 Request user input via direct messages from the gateway component.
793 Wraps call to :meth:`.BaseSession.input`
795 :param text: The prompt to send to the user
796 :param msg_kwargs: Extra attributes
797 :return:
798 """
799 return await self.xmpp.input(self.user_jid, text, **msg_kwargs)
801 async def send_qr(self, text: str) -> None:
802 """
803 Sends a QR code generated from 'text' via HTTP Upload and send the URL to
804 ``self.user``
806 :param text: Text to encode as a QR code
807 """
808 await self.xmpp.send_qr(text, mto=self.user_jid)
810 def re_login(self) -> None:
811 # Logout then re-login
812 #
813 # No reason to override this
814 self.xmpp.re_login(self)
816 async def get_contact_or_group_or_participant(self, jid: JID, create: bool = True):
817 if (contact := self.contacts.by_jid_only_if_exists(jid)) is not None:
818 return contact
819 if (muc := self.bookmarks.by_jid_only_if_exists(JID(jid.bare))) is not None:
820 return await self.__get_muc_or_participant(muc, jid)
821 else:
822 muc = None
824 if not create:
825 return None
827 try:
828 return await self.contacts.by_jid(jid)
829 except XMPPError:
830 if muc is None:
831 try:
832 muc = await self.bookmarks.by_jid(jid)
833 except XMPPError:
834 return
835 return await self.__get_muc_or_participant(muc, jid)
837 @staticmethod
838 async def __get_muc_or_participant(muc: LegacyMUC, jid: JID):
839 if nick := jid.resource:
840 try:
841 return await muc.get_participant(
842 nick, raise_if_not_found=True, fill_first=True
843 )
844 except XMPPError:
845 return None
846 return muc
848 async def wait_for_ready(self, timeout: Optional[Union[int, float]] = 10):
849 # """
850 # Wait until session, contacts and bookmarks are ready
851 #
852 # (slidge internal use)
853 #
854 # :param timeout:
855 # :return:
856 # """
857 try:
858 await asyncio.wait_for(asyncio.shield(self.ready), timeout)
859 await asyncio.wait_for(asyncio.shield(self.contacts.ready), timeout)
860 await asyncio.wait_for(asyncio.shield(self.bookmarks.ready), timeout)
861 except asyncio.TimeoutError:
862 raise XMPPError(
863 "recipient-unavailable",
864 "Legacy session is not fully initialized, retry later",
865 )
867 def legacy_module_data_update(self, data: dict) -> None:
868 user = self.user
869 user.legacy_module_data.update(data)
870 self.xmpp.store.users.update(user)
872 def legacy_module_data_set(self, data: dict) -> None:
873 user = self.user
874 user.legacy_module_data = data
875 self.xmpp.store.users.update(user)
877 def legacy_module_data_clear(self) -> None:
878 user = self.user
879 user.legacy_module_data.clear()
880 self.xmpp.store.users.update(user)
883# keys = user.jid.bare
884_sessions: dict[str, BaseSession] = {}
885log = logging.getLogger(__name__)