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