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