Coverage for slidge / core / session.py: 85%
261 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-03-13 22:59 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-03-13 22:59 +0000
1import abc
2import asyncio
3import logging
4from collections.abc import Iterable
5from typing import (
6 TYPE_CHECKING,
7 Any,
8 Generic,
9 NamedTuple,
10 Optional,
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, ResourceDict
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 SubclassableOnce
27from ..util.lock import NamedLockMixin
28from ..util.types import (
29 LegacyGroupIdType,
30 LegacyMessageType,
31 LegacyThreadType,
32 LinkPreview,
33 Mention,
34 PseudoPresenceShow,
35 RecipientType,
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: str | None
48 show: str | None
49 kwargs: dict[str, Any]
52class BaseSession(
53 Generic[LegacyMessageType, RecipientType],
54 NamedLockMixin,
55 SubclassableOnce,
56 abc.ABC,
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: str | None = 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: CachedPresence | None = 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 @abc.abstractmethod
148 async def login(self) -> str | None:
149 """
150 Logs in the gateway user to the legacy network.
152 Triggered when the gateway start and on user registration.
153 It is recommended that this function returns once the user is logged in,
154 so if you need to await forever (for instance to listen to incoming events),
155 it's a good idea to wrap your listener in an asyncio.Task.
157 :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'"
158 """
159 raise NotImplementedError
161 async def logout(self) -> None:
162 """
163 Logs out the gateway user from the legacy network.
165 Called on gateway shutdown.
166 """
167 raise NotImplementedError
169 async def on_text(
170 self,
171 chat: RecipientType,
172 text: str,
173 *,
174 reply_to_msg_id: LegacyMessageType | None = None,
175 reply_to_fallback_text: str | None = None,
176 reply_to: Optional["Sender"] = None,
177 thread: LegacyThreadType | None = None,
178 link_previews: Iterable[LinkPreview] = (),
179 mentions: list[Mention] | None = None,
180 ) -> LegacyMessageType | None:
181 """
182 Triggered when the user sends a text message from XMPP to a bridged entity, e.g.
183 to ``translated_user_name@slidge.example.com``, or ``translated_group_name@slidge.example.com``
185 Override this and implement sending a message to the legacy network in this method.
187 :param text: Content of the message
188 :param chat: Recipient of the message. :class:`.LegacyContact` instance for 1:1 chat,
189 :class:`.MUC` instance for groups.
190 :param reply_to_msg_id: A legacy message ID if the message references (quotes)
191 another message (:xep:`0461`)
192 :param reply_to_fallback_text: Content of the quoted text. Not necessarily set
193 by XMPP clients
194 :param reply_to: Author of the quoted message. :class:`LegacyContact` instance for
195 1:1 chat, :class:`LegacyParticipant` instance for groups.
196 If `None`, should be interpreted as a self-reply if reply_to_msg_id is not None.
197 :param link_previews: A list of sender-generated link previews.
198 At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
199 supports it.
200 :param mentions: (only for groups) A list of Contacts mentioned by their
201 nicknames.
202 :param thread:
204 :return: An ID of some sort that can be used later to ack and mark the message
205 as read by the user
206 """
207 raise NotImplementedError
209 send_text = deprecated("BaseSession.send_text", on_text)
211 async def on_file(
212 self,
213 chat: RecipientType,
214 url: str,
215 *,
216 http_response: aiohttp.ClientResponse,
217 reply_to_msg_id: LegacyMessageType | None = None,
218 reply_to_fallback_text: str | None = None,
219 reply_to: Union["LegacyContact", "LegacyParticipant"] | None = None,
220 thread: LegacyThreadType | None = None,
221 ) -> LegacyMessageType | None:
222 """
223 Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
225 :param url: URL of the file
226 :param chat: See :meth:`.BaseSession.on_text`
227 :param http_response: The HTTP GET response object on the URL
228 :param reply_to_msg_id: See :meth:`.BaseSession.on_text`
229 :param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
230 :param reply_to: See :meth:`.BaseSession.on_text`
231 :param thread:
233 :return: An ID of some sort that can be used later to ack and mark the message
234 as read by the user
235 """
236 raise NotImplementedError
238 send_file = deprecated("BaseSession.send_file", on_file)
240 async def on_sticker(
241 self,
242 chat: RecipientType,
243 sticker: Sticker,
244 *,
245 reply_to_msg_id: LegacyMessageType | None = None,
246 reply_to_fallback_text: str | None = None,
247 reply_to: Union["LegacyContact", "LegacyParticipant"] | None = None,
248 thread: LegacyThreadType | None = None,
249 ) -> LegacyMessageType | None:
250 """
251 Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
253 :param chat: See :meth:`.BaseSession.on_text`
254 :param sticker: The sticker sent by the user.
255 :param reply_to_msg_id: See :meth:`.BaseSession.on_text`
256 :param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
257 :param reply_to: See :meth:`.BaseSession.on_text`
258 :param thread:
260 :return: An ID of some sort that can be used later to ack and mark the message
261 as read by the user
262 """
263 raise NotImplementedError
265 async def on_active(
266 self, chat: RecipientType, thread: LegacyThreadType | None = None
267 ) -> None:
268 """
269 Triggered when the user sends an 'active' chat state (:xep:`0085`)
271 :param chat: See :meth:`.BaseSession.on_text`
272 :param thread:
273 """
274 raise NotImplementedError
276 active = deprecated("BaseSession.active", on_active)
278 async def on_inactive(
279 self, chat: RecipientType, thread: LegacyThreadType | None = None
280 ) -> None:
281 """
282 Triggered when the user sends an 'inactive' chat state (:xep:`0085`)
284 :param chat: See :meth:`.BaseSession.on_text`
285 :param thread:
286 """
287 raise NotImplementedError
289 inactive = deprecated("BaseSession.inactive", on_inactive)
291 async def on_composing(
292 self, chat: RecipientType, thread: LegacyThreadType | None = None
293 ) -> None:
294 """
295 Triggered when the user starts typing in a legacy chat (:xep:`0085`)
297 :param chat: See :meth:`.BaseSession.on_text`
298 :param thread:
299 """
300 raise NotImplementedError
302 composing = deprecated("BaseSession.composing", on_composing)
304 async def on_paused(
305 self, chat: RecipientType, thread: LegacyThreadType | None = None
306 ) -> None:
307 """
308 Triggered when the user pauses typing in a legacy chat (:xep:`0085`)
310 :param chat: See :meth:`.BaseSession.on_text`
311 :param thread:
312 """
313 raise NotImplementedError
315 paused = deprecated("BaseSession.paused", on_paused)
317 async def on_gone(
318 self, chat: RecipientType, thread: LegacyThreadType | None = None
319 ) -> None:
320 """
321 Triggered when the user is "gone" in a legacy chat (:xep:`0085`)
323 :param chat: See :meth:`.BaseSession.on_text`
324 :param thread:
325 """
326 raise NotImplementedError
328 async def on_displayed(
329 self,
330 chat: RecipientType,
331 legacy_msg_id: LegacyMessageType,
332 thread: LegacyThreadType | None = None,
333 ) -> None:
334 """
335 Triggered when the user reads a message in a legacy chat. (:xep:`0333`)
337 This is only possible if a valid ``legacy_msg_id`` was passed when
338 transmitting a message from a legacy chat to the user, eg in
339 :meth:`slidge.contact.LegacyContact.send_text`
340 or
341 :meth:`slidge.group.LegacyParticipant.send_text`.
343 :param chat: See :meth:`.BaseSession.on_text`
344 :param legacy_msg_id: Identifier of the message/
345 :param thread:
346 """
347 raise NotImplementedError
349 displayed = deprecated("BaseSession.displayed", on_displayed)
351 async def on_correct(
352 self,
353 chat: RecipientType,
354 text: str,
355 legacy_msg_id: LegacyMessageType,
356 *,
357 thread: LegacyThreadType | None = None,
358 link_previews: Iterable[LinkPreview] = (),
359 mentions: list[Mention] | None = None,
360 ) -> LegacyMessageType | None:
361 """
362 Triggered when the user corrects a message using :xep:`0308`
364 This is only possible if a valid ``legacy_msg_id`` was returned by
365 :meth:`.on_text`.
367 :param chat: See :meth:`.BaseSession.on_text`
368 :param text: The new text
369 :param legacy_msg_id: Identifier of the edited message
370 :param thread:
371 :param link_previews: A list of sender-generated link previews.
372 At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
373 supports it.
374 :param mentions: (only for groups) A list of Contacts mentioned by their
375 nicknames.
376 """
377 raise NotImplementedError
379 correct = deprecated("BaseSession.correct", on_correct)
381 async def on_react(
382 self,
383 chat: RecipientType,
384 legacy_msg_id: LegacyMessageType,
385 emojis: list[str],
386 thread: LegacyThreadType | None = None,
387 ) -> None:
388 """
389 Triggered when the user sends message reactions (:xep:`0444`).
391 :param chat: See :meth:`.BaseSession.on_text`
392 :param thread:
393 :param legacy_msg_id: ID of the message the user reacts to
394 :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``.
395 An empty string means "no reaction", ie, remove all reactions if any were present before
396 """
397 raise NotImplementedError
399 react = deprecated("BaseSession.react", on_react)
401 async def on_retract(
402 self,
403 chat: RecipientType,
404 legacy_msg_id: LegacyMessageType,
405 thread: LegacyThreadType | None = None,
406 ) -> None:
407 """
408 Triggered when the user retracts (:xep:`0424`) a message.
410 :param chat: See :meth:`.BaseSession.on_text`
411 :param thread:
412 :param legacy_msg_id: Legacy ID of the retracted message
413 """
414 raise NotImplementedError
416 retract = deprecated("BaseSession.retract", on_retract)
418 async def on_presence(
419 self,
420 resource: str,
421 show: PseudoPresenceShow,
422 status: str,
423 resources: dict[str, ResourceDict],
424 merged_resource: ResourceDict | None,
425 ) -> None:
426 """
427 Called when the gateway component receives a presence, ie, when
428 one of the user's clients goes online of offline, or changes its
429 status.
431 :param resource: The XMPP client identifier, arbitrary string.
432 :param show: The presence ``<show>``, if available. If the resource is
433 just 'available' without any ``<show>`` element, this is an empty
434 str.
435 :param status: A status message, like a deeply profound quote, eg,
436 "Roses are red, violets are blue, [INSERT JOKE]".
437 :param resources: A summary of all the resources for this user.
438 :param merged_resource: A global presence for the user account,
439 following rules described in :meth:`merge_resources`
440 """
441 raise NotImplementedError
443 presence = deprecated("BaseSession.presence", on_presence)
445 async def on_search(self, form_values: dict[str, str]) -> SearchResult | None:
446 """
447 Triggered when the user uses Jabber Search (:xep:`0055`) on the component
449 Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS`
451 :param form_values: search query, defined for a specific plugin by overriding
452 in :attr:`.BaseGateway.SEARCH_FIELDS`
453 :return:
454 """
455 raise NotImplementedError
457 search = deprecated("BaseSession.search", on_search)
459 async def on_avatar(
460 self,
461 bytes_: bytes | None,
462 hash_: str | None,
463 type_: str | None,
464 width: int | None,
465 height: int | None,
466 ) -> None:
467 """
468 Triggered when the user uses modifies their avatar via :xep:`0084`.
470 :param bytes_: The data of the avatar. According to the spec, this
471 should always be a PNG, but some implementations do not respect
472 that. If `None` it means the user has unpublished their avatar.
473 :param hash_: The SHA1 hash of the avatar data. This is an identifier of
474 the avatar.
475 :param type_: The MIME type of the avatar.
476 :param width: The width of the avatar image.
477 :param height: The height of the avatar image.
478 """
479 raise NotImplementedError
481 async def on_moderate(
482 self, muc: LegacyMUC, legacy_msg_id: LegacyMessageType, reason: str | None
483 ) -> None:
484 """
485 Triggered when the user attempts to retract a message that was sent in
486 a MUC using :xep:`0425`.
488 If retraction is not possible, this should raise the appropriate
489 XMPPError with a human-readable message.
491 NB: the legacy module is responsible for calling
492 :method:`LegacyParticipant.moderate` when this is successful, because
493 slidge will acknowledge the moderation IQ, but will not send the
494 moderation message from the MUC automatically.
496 :param muc: The MUC in which the message was sent
497 :param legacy_msg_id: The legacy ID of the message to be retracted
498 :param reason: Optionally, a reason for the moderation, given by the
499 user-moderator.
500 """
501 raise NotImplementedError
503 async def on_create_group(
504 self, name: str, contacts: list[LegacyContact]
505 ) -> LegacyGroupIdType:
506 """
507 Triggered when the user request the creation of a group via the
508 dedicated :term:`Command`.
510 :param name: Name of the group
511 :param contacts: list of contacts that should be members of the group
512 """
513 raise NotImplementedError
515 async def on_invitation(
516 self, contact: LegacyContact, muc: LegacyMUC, reason: str | None
517 ) -> None:
518 """
519 Triggered when the user invites a :term:`Contact` to a legacy MUC via
520 :xep:`0249`.
522 The default implementation calls :meth:`LegacyMUC.on_set_affiliation`
523 with the 'member' affiliation. Override if you want to customize this
524 behaviour.
526 :param contact: The invitee
527 :param muc: The group
528 :param reason: Optionally, a reason
529 """
530 await muc.on_set_affiliation(contact, "member", reason, None)
532 async def on_leave_group(self, muc_legacy_id: LegacyGroupIdType) -> None:
533 """
534 Triggered when the user leaves a group via the dedicated slidge command
535 or the :xep:`0077` ``<remove />`` mechanism.
537 This should be interpreted as definitely leaving the group.
539 :param muc_legacy_id: The legacy ID of the group to leave
540 """
541 raise NotImplementedError
543 async def on_preferences(
544 self, previous: dict[str, Any], new: dict[str, Any]
545 ) -> None:
546 """
547 This is called when the user updates their preferences.
549 Override this if you need set custom preferences field and need to trigger
550 something when a preference has changed.
551 """
552 raise NotImplementedError
554 def __reset_ready(self) -> None:
555 self.ready = self.xmpp.loop.create_future()
557 @property
558 def logged(self):
559 return self._logged
561 @logged.setter
562 def logged(self, v: bool) -> None:
563 self.is_logging_in = False
564 self._logged = v
565 if self.ready.done():
566 if v:
567 return
568 self.__reset_ready()
569 self.shutdown(logout=False)
570 with self.xmpp.store.session() as orm:
571 self.xmpp.store.mam.reset_source(orm)
572 self.xmpp.store.rooms.reset_updated(orm)
573 self.xmpp.store.contacts.reset_updated(orm)
574 orm.commit()
575 else:
576 if v:
577 self.ready.set_result(True)
579 def __repr__(self) -> str:
580 return f"<Session of {self.user_jid}>"
582 def shutdown(self, logout: bool = True) -> asyncio.Task:
583 for m in self.bookmarks:
584 m.shutdown()
585 with self.xmpp.store.session() as orm:
586 for jid in orm.execute(
587 sa.select(Contact.jid).filter_by(user=self.user, is_friend=True)
588 ).scalars():
589 pres = self.xmpp.make_presence(
590 pfrom=jid,
591 pto=self.user_jid,
592 ptype="unavailable",
593 pstatus="Gateway has shut down.",
594 )
595 pres.send()
596 if logout:
597 return self.xmpp.loop.create_task(self.__logout())
598 else:
599 return self.xmpp.loop.create_task(noop_coro())
601 async def __logout(self) -> None:
602 try:
603 await self.logout()
604 except NotImplementedError:
605 pass
606 except KeyboardInterrupt:
607 pass
609 @staticmethod
610 def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
611 """
612 Convert a legacy msg ID to a valid XMPP msg ID.
613 Needed for read marks, retractions and message corrections.
615 The default implementation just converts the legacy ID to a :class:`str`,
616 but this should be overridden in case some characters needs to be escaped,
617 or to add some additional,
618 :term:`legacy network <Legacy Network`>-specific logic.
620 :param legacy_msg_id:
621 :return: A string that is usable as an XMPP stanza ID
622 """
623 return str(legacy_msg_id)
625 legacy_msg_id_to_xmpp_msg_id = staticmethod(
626 deprecated("BaseSession.legacy_msg_id_to_xmpp_msg_id", legacy_to_xmpp_msg_id)
627 )
629 @staticmethod
630 def xmpp_to_legacy_msg_id(i: str) -> LegacyMessageType:
631 """
632 Convert a legacy XMPP ID to a valid XMPP msg ID.
633 Needed for read marks and message corrections.
635 The default implementation just converts the legacy ID to a :class:`str`,
636 but this should be overridden in case some characters needs to be escaped,
637 or to add some additional,
638 :term:`legacy network <Legacy Network`>-specific logic.
640 The default implementation is an identity function.
642 :param i: The XMPP stanza ID
643 :return: An ID that can be used to identify a message on the legacy network
644 """
645 return cast(LegacyMessageType, i)
647 xmpp_msg_id_to_legacy_msg_id = staticmethod(
648 deprecated("BaseSession.xmpp_msg_id_to_legacy_msg_id", xmpp_to_legacy_msg_id)
649 )
651 def raise_if_not_logged(self) -> None:
652 if not self.logged:
653 raise XMPPError(
654 "internal-server-error",
655 text="You are not logged to the legacy network",
656 )
658 @classmethod
659 def _from_user_or_none(cls, user):
660 if user is None:
661 log.debug("user not found")
662 raise XMPPError(text="User not found", condition="subscription-required")
664 session = _sessions.get(user.jid.bare)
665 if session is None:
666 _sessions[user.jid.bare] = session = cls(user)
667 return session
669 @classmethod
670 def from_user(cls, user):
671 return cls._from_user_or_none(user)
673 @classmethod
674 def from_stanza(cls, s) -> "BaseSession":
675 # """
676 # Get a user's :class:`.LegacySession` using the "from" field of a stanza
677 #
678 # Meant to be called from :class:`BaseGateway` only.
679 #
680 # :param s:
681 # :return:
682 # """
683 return cls.from_jid(s.get_from())
685 @classmethod
686 def from_jid(cls, jid: JID) -> "BaseSession":
687 # """
688 # Get a user's :class:`.LegacySession` using its jid
689 #
690 # Meant to be called from :class:`BaseGateway` only.
691 #
692 # :param jid:
693 # :return:
694 # """
695 session = _sessions.get(jid.bare)
696 if session is not None:
697 return session
698 with cls.xmpp.store.session() as orm:
699 user = orm.query(GatewayUser).filter_by(jid=jid.bare).one_or_none()
700 return cls._from_user_or_none(user)
702 @classmethod
703 async def kill_by_jid(cls, jid: JID) -> None:
704 # """
705 # Terminate a user session.
706 #
707 # Meant to be called from :class:`BaseGateway` only.
708 #
709 # :param jid:
710 # :return:
711 # """
712 log.debug("Killing session of %s", jid)
713 for user_jid, session in _sessions.items():
714 if user_jid == jid.bare:
715 break
716 else:
717 log.debug("Did not find a session for %s", jid)
718 return
719 for c in session.contacts:
720 c.unsubscribe()
721 for m in session.bookmarks:
722 m.shutdown()
724 try:
725 session = _sessions.pop(jid.bare)
726 except KeyError:
727 log.warning("User not found during unregistration")
728 return
730 session.cancel_all_tasks()
732 await cls.xmpp.unregister(session)
733 with cls.xmpp.store.session() as orm:
734 orm.delete(session.user)
735 orm.commit()
737 def __ack(self, msg: Message) -> None:
738 if not self.xmpp.PROPER_RECEIPTS:
739 self.xmpp.delivery_receipt.ack(msg)
741 def send_gateway_status(
742 self,
743 status: str | None = None,
744 show=PresenceShows | None,
745 **kwargs,
746 ) -> None:
747 """
748 Send a presence from the gateway to the user.
750 Can be used to indicate the user session status, ie "SMS code required", "connected", …
752 :param status: A status message
753 :param show: Presence stanza 'show' element. I suggest using "dnd" to show
754 that the gateway is not fully functional
755 """
756 self.__cached_presence = CachedPresence(status, show, kwargs)
757 self.xmpp.send_presence(
758 pto=self.user_jid.bare, pstatus=status, pshow=show, **kwargs
759 )
761 def send_cached_presence(self, to: JID) -> None:
762 if not self.__cached_presence:
763 self.xmpp.send_presence(pto=to, ptype="unavailable")
764 return
765 self.xmpp.send_presence(
766 pto=to,
767 pstatus=self.__cached_presence.status,
768 pshow=self.__cached_presence.show,
769 **self.__cached_presence.kwargs,
770 )
772 def send_gateway_message(self, text: str, **msg_kwargs) -> None:
773 """
774 Send a message from the gateway component to the user.
776 Can be used to indicate the user session status, ie "SMS code required", "connected", …
778 :param text: A text
779 """
780 self.xmpp.send_text(text, mto=self.user_jid, **msg_kwargs)
782 def send_gateway_invite(
783 self,
784 muc: LegacyMUC,
785 reason: str | None = None,
786 password: str | None = None,
787 ) -> None:
788 """
789 Send an invitation to join a MUC, emanating from the gateway component.
791 :param muc:
792 :param reason:
793 :param password:
794 """
795 self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user_jid)
797 async def input(self, text: str, **msg_kwargs):
798 """
799 Request user input via direct messages from the gateway component.
801 Wraps call to :meth:`.BaseSession.input`
803 :param text: The prompt to send to the user
804 :param msg_kwargs: Extra attributes
805 :return:
806 """
807 return await self.xmpp.input(self.user_jid, text, **msg_kwargs)
809 async def send_qr(self, text: str) -> None:
810 """
811 Sends a QR code generated from 'text' via HTTP Upload and send the URL to
812 ``self.user``
814 :param text: Text to encode as a QR code
815 """
816 await self.xmpp.send_qr(text, mto=self.user_jid)
818 async def get_contact_or_group_or_participant(self, jid: JID, create: bool = True):
819 if (contact := self.contacts.by_jid_only_if_exists(jid)) is not None:
820 return contact
821 if (muc := self.bookmarks.by_jid_only_if_exists(JID(jid.bare))) is not None:
822 return await self.__get_muc_or_participant(muc, jid)
823 else:
824 muc = None
826 if not create:
827 return None
829 try:
830 return await self.contacts.by_jid(jid)
831 except XMPPError:
832 if muc is None:
833 try:
834 muc = await self.bookmarks.by_jid(jid)
835 except XMPPError:
836 return
837 return await self.__get_muc_or_participant(muc, jid)
839 @staticmethod
840 async def __get_muc_or_participant(muc: LegacyMUC, jid: JID):
841 if nick := jid.resource:
842 return await muc.get_participant(nick, create=False, fill_first=True)
843 return muc
845 async def wait_for_ready(self, timeout: int | float | None = 10) -> None:
846 # """
847 # Wait until session, contacts and bookmarks are ready
848 #
849 # (slidge internal use)
850 #
851 # :param timeout:
852 # :return:
853 # """
854 try:
855 await asyncio.wait_for(asyncio.shield(self.ready), timeout)
856 await asyncio.wait_for(asyncio.shield(self.contacts.ready), timeout)
857 await asyncio.wait_for(asyncio.shield(self.bookmarks.ready), timeout)
858 except TimeoutError:
859 raise XMPPError(
860 "recipient-unavailable",
861 "Legacy session is not fully initialized, retry later",
862 )
864 def legacy_module_data_update(self, data: dict) -> None:
865 user = self.user
866 user.legacy_module_data.update(data)
867 self.xmpp.store.users.update(user)
869 def legacy_module_data_set(self, data: dict) -> None:
870 user = self.user
871 user.legacy_module_data = data
872 self.xmpp.store.users.update(user)
874 def legacy_module_data_clear(self) -> None:
875 user = self.user
876 user.legacy_module_data.clear()
877 self.xmpp.store.users.update(user)
880# keys = user.jid.bare
881_sessions: dict[str, BaseSession] = {}
882log = logging.getLogger(__name__)