Coverage for slidge / core / session.py: 84%
258 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-06 15:18 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-01-06 15:18 +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, 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 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 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],
54 NamedLockMixin,
55 metaclass=ABCSubclassableOnceAtMost,
56):
57 """
58 The session of a registered :term:`User`.
60 Represents a gateway user logged in to the legacy network and performing actions.
62 Will be instantiated automatically on slidge startup for each registered user,
63 or upon registration for new (validated) users.
65 Must be subclassed for a functional :term:`Legacy Module`.
66 """
68 """
69 Since we cannot set the XMPP ID of messages sent by XMPP clients, we need to keep a mapping
70 between XMPP IDs and legacy message IDs if we want to further refer to a message that was sent
71 by the user. This also applies to 'carboned' messages, ie, messages sent by the user from
72 the official client of a legacy network.
73 """
75 xmpp: "BaseGateway"
76 """
77 The gateway instance singleton. Use it for low-level XMPP calls or custom methods that are not
78 session-specific.
79 """
81 MESSAGE_IDS_ARE_THREAD_IDS = False
82 """
83 Set this to True if the legacy service uses message IDs as thread IDs,
84 eg Mattermost, where you can only 'create a thread' by replying to the message,
85 in which case the message ID is also a thread ID (and all messages are potential
86 threads).
87 """
88 SPECIAL_MSG_ID_PREFIX: Optional[str] = None
89 """
90 If you set this, XMPP message IDs starting with this won't be converted to legacy ID,
91 but passed as is to :meth:`.on_react`, and usual checks for emoji restriction won't be
92 applied.
93 This can be used to implement voting in polls in a hacky way.
94 """
96 _roster_cls: Type[LegacyRoster]
97 _bookmarks_cls: Type[LegacyBookmarks]
99 def __init__(self, user: GatewayUser) -> None:
100 super().__init__()
101 self.user = user
102 self.log = logging.getLogger(user.jid.bare)
104 self.ignore_messages = set[str]()
106 self.contacts = self._roster_cls(self)
107 self.is_logging_in = False
108 self._logged = False
109 self.__reset_ready()
111 self.bookmarks = self._bookmarks_cls(self)
113 self.thread_creation_lock = asyncio.Lock()
115 self.__cached_presence: Optional[CachedPresence] = None
117 self.__tasks = set[asyncio.Task]()
119 @property
120 def user_jid(self) -> JID:
121 return self.user.jid
123 @property
124 def user_pk(self) -> int:
125 return self.user.id
127 @property
128 def http(self) -> aiohttp.ClientSession:
129 return self.xmpp.http
131 def __remove_task(self, fut) -> None:
132 self.log.debug("Removing fut %s", fut)
133 self.__tasks.remove(fut)
135 def create_task(self, coro, name: str | None = None) -> asyncio.Task:
136 task = self.xmpp.loop.create_task(coro, name=name)
137 self.__tasks.add(task)
138 self.log.debug("Creating task %s", task)
139 task.add_done_callback(lambda _: self.__remove_task(task))
140 return task
142 def cancel_all_tasks(self) -> None:
143 for task in self.__tasks:
144 task.cancel()
146 async def login(self) -> Optional[str]:
147 """
148 Logs in the gateway user to the legacy network.
150 Triggered when the gateway start and on user registration.
151 It is recommended that this function returns once the user is logged in,
152 so if you need to await forever (for instance to listen to incoming events),
153 it's a good idea to wrap your listener in an asyncio.Task.
155 :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'"
156 """
157 raise NotImplementedError
159 async def logout(self) -> None:
160 """
161 Logs out the gateway user from the legacy network.
163 Called on gateway shutdown.
164 """
165 raise NotImplementedError
167 async def on_text(
168 self,
169 chat: RecipientType,
170 text: str,
171 *,
172 reply_to_msg_id: Optional[LegacyMessageType] = None,
173 reply_to_fallback_text: Optional[str] = None,
174 reply_to: Optional["Sender"] = None,
175 thread: Optional[LegacyThreadType] = None,
176 link_previews: Iterable[LinkPreview] = (),
177 mentions: Optional[list[Mention]] = None,
178 ) -> Optional[LegacyMessageType]:
179 """
180 Triggered when the user sends a text message from XMPP to a bridged entity, e.g.
181 to ``translated_user_name@slidge.example.com``, or ``translated_group_name@slidge.example.com``
183 Override this and implement sending a message to the legacy network in this method.
185 :param text: Content of the message
186 :param chat: Recipient of the message. :class:`.LegacyContact` instance for 1:1 chat,
187 :class:`.MUC` instance for groups.
188 :param reply_to_msg_id: A legacy message ID if the message references (quotes)
189 another message (:xep:`0461`)
190 :param reply_to_fallback_text: Content of the quoted text. Not necessarily set
191 by XMPP clients
192 :param reply_to: Author of the quoted message. :class:`LegacyContact` instance for
193 1:1 chat, :class:`LegacyParticipant` instance for groups.
194 If `None`, should be interpreted as a self-reply if reply_to_msg_id is not None.
195 :param link_previews: A list of sender-generated link previews.
196 At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
197 supports it.
198 :param mentions: (only for groups) A list of Contacts mentioned by their
199 nicknames.
200 :param thread:
202 :return: An ID of some sort that can be used later to ack and mark the message
203 as read by the user
204 """
205 raise NotImplementedError
207 send_text = deprecated("BaseSession.send_text", on_text)
209 async def on_file(
210 self,
211 chat: RecipientType,
212 url: str,
213 *,
214 http_response: aiohttp.ClientResponse,
215 reply_to_msg_id: Optional[LegacyMessageType] = None,
216 reply_to_fallback_text: Optional[str] = None,
217 reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None,
218 thread: Optional[LegacyThreadType] = None,
219 ) -> Optional[LegacyMessageType]:
220 """
221 Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
223 :param url: URL of the file
224 :param chat: See :meth:`.BaseSession.on_text`
225 :param http_response: The HTTP GET response object on the URL
226 :param reply_to_msg_id: See :meth:`.BaseSession.on_text`
227 :param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
228 :param reply_to: See :meth:`.BaseSession.on_text`
229 :param thread:
231 :return: An ID of some sort that can be used later to ack and mark the message
232 as read by the user
233 """
234 raise NotImplementedError
236 send_file = deprecated("BaseSession.send_file", on_file)
238 async def on_sticker(
239 self,
240 chat: RecipientType,
241 sticker: Sticker,
242 *,
243 reply_to_msg_id: Optional[LegacyMessageType] = None,
244 reply_to_fallback_text: Optional[str] = None,
245 reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None,
246 thread: Optional[LegacyThreadType] = None,
247 ) -> Optional[LegacyMessageType]:
248 """
249 Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
251 :param chat: See :meth:`.BaseSession.on_text`
252 :param sticker: The sticker sent by the user.
253 :param reply_to_msg_id: See :meth:`.BaseSession.on_text`
254 :param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
255 :param reply_to: See :meth:`.BaseSession.on_text`
256 :param thread:
258 :return: An ID of some sort that can be used later to ack and mark the message
259 as read by the user
260 """
261 raise NotImplementedError
263 async def on_active(
264 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
265 ):
266 """
267 Triggered when the user sends an 'active' chat state (:xep:`0085`)
269 :param chat: See :meth:`.BaseSession.on_text`
270 :param thread:
271 """
272 raise NotImplementedError
274 active = deprecated("BaseSession.active", on_active)
276 async def on_inactive(
277 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
278 ):
279 """
280 Triggered when the user sends an 'inactive' chat state (:xep:`0085`)
282 :param chat: See :meth:`.BaseSession.on_text`
283 :param thread:
284 """
285 raise NotImplementedError
287 inactive = deprecated("BaseSession.inactive", on_inactive)
289 async def on_composing(
290 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
291 ):
292 """
293 Triggered when the user starts typing in a legacy chat (:xep:`0085`)
295 :param chat: See :meth:`.BaseSession.on_text`
296 :param thread:
297 """
298 raise NotImplementedError
300 composing = deprecated("BaseSession.composing", on_composing)
302 async def on_paused(
303 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
304 ):
305 """
306 Triggered when the user pauses typing in a legacy chat (:xep:`0085`)
308 :param chat: See :meth:`.BaseSession.on_text`
309 :param thread:
310 """
311 raise NotImplementedError
313 paused = deprecated("BaseSession.paused", on_paused)
315 async def on_gone(
316 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
317 ):
318 """
319 Triggered when the user is "gone" in a legacy chat (:xep:`0085`)
321 :param chat: See :meth:`.BaseSession.on_text`
322 :param thread:
323 """
324 raise NotImplementedError
326 async def on_displayed(
327 self,
328 chat: RecipientType,
329 legacy_msg_id: LegacyMessageType,
330 thread: Optional[LegacyThreadType] = None,
331 ):
332 """
333 Triggered when the user reads a message in a legacy chat. (:xep:`0333`)
335 This is only possible if a valid ``legacy_msg_id`` was passed when
336 transmitting a message from a legacy chat to the user, eg in
337 :meth:`slidge.contact.LegacyContact.send_text`
338 or
339 :meth:`slidge.group.LegacyParticipant.send_text`.
341 :param chat: See :meth:`.BaseSession.on_text`
342 :param legacy_msg_id: Identifier of the message/
343 :param thread:
344 """
345 raise NotImplementedError
347 displayed = deprecated("BaseSession.displayed", on_displayed)
349 async def on_correct(
350 self,
351 chat: RecipientType,
352 text: str,
353 legacy_msg_id: LegacyMessageType,
354 *,
355 thread: Optional[LegacyThreadType] = None,
356 link_previews: Iterable[LinkPreview] = (),
357 mentions: Optional[list[Mention]] = None,
358 ) -> Optional[LegacyMessageType]:
359 """
360 Triggered when the user corrects a message using :xep:`0308`
362 This is only possible if a valid ``legacy_msg_id`` was returned by
363 :meth:`.on_text`.
365 :param chat: See :meth:`.BaseSession.on_text`
366 :param text: The new text
367 :param legacy_msg_id: Identifier of the edited message
368 :param thread:
369 :param link_previews: A list of sender-generated link previews.
370 At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
371 supports it.
372 :param mentions: (only for groups) A list of Contacts mentioned by their
373 nicknames.
374 """
375 raise NotImplementedError
377 correct = deprecated("BaseSession.correct", on_correct)
379 async def on_react(
380 self,
381 chat: RecipientType,
382 legacy_msg_id: LegacyMessageType,
383 emojis: list[str],
384 thread: Optional[LegacyThreadType] = None,
385 ):
386 """
387 Triggered when the user sends message reactions (:xep:`0444`).
389 :param chat: See :meth:`.BaseSession.on_text`
390 :param thread:
391 :param legacy_msg_id: ID of the message the user reacts to
392 :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``.
393 An empty string means "no reaction", ie, remove all reactions if any were present before
394 """
395 raise NotImplementedError
397 react = deprecated("BaseSession.react", on_react)
399 async def on_retract(
400 self,
401 chat: RecipientType,
402 legacy_msg_id: LegacyMessageType,
403 thread: Optional[LegacyThreadType] = None,
404 ):
405 """
406 Triggered when the user retracts (:xep:`0424`) a message.
408 :param chat: See :meth:`.BaseSession.on_text`
409 :param thread:
410 :param legacy_msg_id: Legacy ID of the retracted message
411 """
412 raise NotImplementedError
414 retract = deprecated("BaseSession.retract", on_retract)
416 async def on_presence(
417 self,
418 resource: str,
419 show: PseudoPresenceShow,
420 status: str,
421 resources: dict[str, ResourceDict],
422 merged_resource: Optional[ResourceDict],
423 ):
424 """
425 Called when the gateway component receives a presence, ie, when
426 one of the user's clients goes online of offline, or changes its
427 status.
429 :param resource: The XMPP client identifier, arbitrary string.
430 :param show: The presence ``<show>``, if available. If the resource is
431 just 'available' without any ``<show>`` element, this is an empty
432 str.
433 :param status: A status message, like a deeply profound quote, eg,
434 "Roses are red, violets are blue, [INSERT JOKE]".
435 :param resources: A summary of all the resources for this user.
436 :param merged_resource: A global presence for the user account,
437 following rules described in :meth:`merge_resources`
438 """
439 raise NotImplementedError
441 presence = deprecated("BaseSession.presence", on_presence)
443 async def on_search(self, form_values: dict[str, str]) -> Optional[SearchResult]:
444 """
445 Triggered when the user uses Jabber Search (:xep:`0055`) on the component
447 Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS`
449 :param form_values: search query, defined for a specific plugin by overriding
450 in :attr:`.BaseGateway.SEARCH_FIELDS`
451 :return:
452 """
453 raise NotImplementedError
455 search = deprecated("BaseSession.search", on_search)
457 async def on_avatar(
458 self,
459 bytes_: Optional[bytes],
460 hash_: Optional[str],
461 type_: Optional[str],
462 width: Optional[int],
463 height: Optional[int],
464 ) -> None:
465 """
466 Triggered when the user uses modifies their avatar via :xep:`0084`.
468 :param bytes_: The data of the avatar. According to the spec, this
469 should always be a PNG, but some implementations do not respect
470 that. If `None` it means the user has unpublished their avatar.
471 :param hash_: The SHA1 hash of the avatar data. This is an identifier of
472 the avatar.
473 :param type_: The MIME type of the avatar.
474 :param width: The width of the avatar image.
475 :param height: The height of the avatar image.
476 """
477 raise NotImplementedError
479 async def on_moderate(
480 self, muc: LegacyMUC, legacy_msg_id: LegacyMessageType, reason: Optional[str]
481 ):
482 """
483 Triggered when the user attempts to retract a message that was sent in
484 a MUC using :xep:`0425`.
486 If retraction is not possible, this should raise the appropriate
487 XMPPError with a human-readable message.
489 NB: the legacy module is responsible for calling
490 :method:`LegacyParticipant.moderate` when this is successful, because
491 slidge will acknowledge the moderation IQ, but will not send the
492 moderation message from the MUC automatically.
494 :param muc: The MUC in which the message was sent
495 :param legacy_msg_id: The legacy ID of the message to be retracted
496 :param reason: Optionally, a reason for the moderation, given by the
497 user-moderator.
498 """
499 raise NotImplementedError
501 async def on_create_group(
502 self, name: str, contacts: list[LegacyContact]
503 ) -> LegacyGroupIdType:
504 """
505 Triggered when the user request the creation of a group via the
506 dedicated :term:`Command`.
508 :param name: Name of the group
509 :param contacts: list of contacts that should be members of the group
510 """
511 raise NotImplementedError
513 async def on_invitation(
514 self, contact: LegacyContact, muc: LegacyMUC, reason: Optional[str]
515 ) -> None:
516 """
517 Triggered when the user invites a :term:`Contact` to a legacy MUC via
518 :xep:`0249`.
520 The default implementation calls :meth:`LegacyMUC.on_set_affiliation`
521 with the 'member' affiliation. Override if you want to customize this
522 behaviour.
524 :param contact: The invitee
525 :param muc: The group
526 :param reason: Optionally, a reason
527 """
528 await muc.on_set_affiliation(contact, "member", reason, None)
530 async def on_leave_group(self, muc_legacy_id: LegacyGroupIdType):
531 """
532 Triggered when the user leaves a group via the dedicated slidge command
533 or the :xep:`0077` ``<remove />`` mechanism.
535 This should be interpreted as definitely leaving the group.
537 :param muc_legacy_id: The legacy ID of the group to leave
538 """
539 raise NotImplementedError
541 async def on_preferences(
542 self, previous: dict[str, Any], new: dict[str, Any]
543 ) -> None:
544 """
545 This is called when the user updates their preferences.
547 Override this if you need set custom preferences field and need to trigger
548 something when a preference has changed.
549 """
550 raise NotImplementedError
552 def __reset_ready(self) -> None:
553 self.ready = self.xmpp.loop.create_future()
555 @property
556 def logged(self):
557 return self._logged
559 @logged.setter
560 def logged(self, v: bool) -> None:
561 self.is_logging_in = False
562 self._logged = v
563 if self.ready.done():
564 if v:
565 return
566 self.__reset_ready()
567 self.shutdown(logout=False)
568 with self.xmpp.store.session() as orm:
569 self.xmpp.store.mam.reset_source(orm)
570 self.xmpp.store.rooms.reset_updated(orm)
571 self.xmpp.store.contacts.reset_updated(orm)
572 orm.commit()
573 else:
574 if v:
575 self.ready.set_result(True)
577 def __repr__(self) -> str:
578 return f"<Session of {self.user_jid}>"
580 def shutdown(self, logout: bool = True) -> asyncio.Task:
581 for m in self.bookmarks:
582 m.shutdown()
583 with self.xmpp.store.session() as orm:
584 for jid in orm.execute(
585 sa.select(Contact.jid).filter_by(user=self.user, is_friend=True)
586 ).scalars():
587 pres = self.xmpp.make_presence(
588 pfrom=jid,
589 pto=self.user_jid,
590 ptype="unavailable",
591 pstatus="Gateway has shut down.",
592 )
593 pres.send()
594 if logout:
595 return self.xmpp.loop.create_task(self.__logout())
596 else:
597 return self.xmpp.loop.create_task(noop_coro())
599 async def __logout(self) -> None:
600 try:
601 await self.logout()
602 except NotImplementedError:
603 pass
604 except KeyboardInterrupt:
605 pass
607 @staticmethod
608 def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
609 """
610 Convert a legacy msg ID to a valid XMPP msg ID.
611 Needed for read marks, retractions and message corrections.
613 The default implementation just converts the legacy ID to a :class:`str`,
614 but this should be overridden in case some characters needs to be escaped,
615 or to add some additional,
616 :term:`legacy network <Legacy Network`>-specific logic.
618 :param legacy_msg_id:
619 :return: A string that is usable as an XMPP stanza ID
620 """
621 return str(legacy_msg_id)
623 legacy_msg_id_to_xmpp_msg_id = staticmethod(
624 deprecated("BaseSession.legacy_msg_id_to_xmpp_msg_id", legacy_to_xmpp_msg_id)
625 )
627 @staticmethod
628 def xmpp_to_legacy_msg_id(i: str) -> LegacyMessageType:
629 """
630 Convert a legacy XMPP ID to a valid XMPP msg ID.
631 Needed for read marks and message corrections.
633 The default implementation just converts the legacy ID to a :class:`str`,
634 but this should be overridden in case some characters needs to be escaped,
635 or to add some additional,
636 :term:`legacy network <Legacy Network`>-specific logic.
638 The default implementation is an identity function.
640 :param i: The XMPP stanza ID
641 :return: An ID that can be used to identify a message on the legacy network
642 """
643 return cast(LegacyMessageType, i)
645 xmpp_msg_id_to_legacy_msg_id = staticmethod(
646 deprecated("BaseSession.xmpp_msg_id_to_legacy_msg_id", xmpp_to_legacy_msg_id)
647 )
649 def raise_if_not_logged(self):
650 if not self.logged:
651 raise XMPPError(
652 "internal-server-error",
653 text="You are not logged to the legacy network",
654 )
656 @classmethod
657 def _from_user_or_none(cls, user):
658 if user is None:
659 log.debug("user not found")
660 raise XMPPError(text="User not found", condition="subscription-required")
662 session = _sessions.get(user.jid.bare)
663 if session is None:
664 _sessions[user.jid.bare] = session = cls(user)
665 return session
667 @classmethod
668 def from_user(cls, user):
669 return cls._from_user_or_none(user)
671 @classmethod
672 def from_stanza(cls, s) -> "BaseSession":
673 # """
674 # Get a user's :class:`.LegacySession` using the "from" field of a stanza
675 #
676 # Meant to be called from :class:`BaseGateway` only.
677 #
678 # :param s:
679 # :return:
680 # """
681 return cls.from_jid(s.get_from())
683 @classmethod
684 def from_jid(cls, jid: JID) -> "BaseSession":
685 # """
686 # Get a user's :class:`.LegacySession` using its jid
687 #
688 # Meant to be called from :class:`BaseGateway` only.
689 #
690 # :param jid:
691 # :return:
692 # """
693 session = _sessions.get(jid.bare)
694 if session is not None:
695 return session
696 with cls.xmpp.store.session() as orm:
697 user = orm.query(GatewayUser).filter_by(jid=jid.bare).one_or_none()
698 return cls._from_user_or_none(user)
700 @classmethod
701 async def kill_by_jid(cls, jid: JID) -> None:
702 # """
703 # Terminate a user session.
704 #
705 # Meant to be called from :class:`BaseGateway` only.
706 #
707 # :param jid:
708 # :return:
709 # """
710 log.debug("Killing session of %s", jid)
711 for user_jid, session in _sessions.items():
712 if user_jid == jid.bare:
713 break
714 else:
715 log.debug("Did not find a session for %s", jid)
716 return
717 for c in session.contacts:
718 c.unsubscribe()
719 for m in session.bookmarks:
720 m.shutdown()
722 try:
723 session = _sessions.pop(jid.bare)
724 except KeyError:
725 log.warning("User not found during unregistration")
726 return
728 session.cancel_all_tasks()
730 await cls.xmpp.unregister(session)
731 with cls.xmpp.store.session() as orm:
732 orm.delete(session.user)
733 orm.commit()
735 def __ack(self, msg: Message) -> None:
736 if not self.xmpp.PROPER_RECEIPTS:
737 self.xmpp.delivery_receipt.ack(msg)
739 def send_gateway_status(
740 self,
741 status: Optional[str] = None,
742 show=Optional[PresenceShows],
743 **kwargs,
744 ) -> None:
745 """
746 Send a presence from the gateway to the user.
748 Can be used to indicate the user session status, ie "SMS code required", "connected", …
750 :param status: A status message
751 :param show: Presence stanza 'show' element. I suggest using "dnd" to show
752 that the gateway is not fully functional
753 """
754 self.__cached_presence = CachedPresence(status, show, kwargs)
755 self.xmpp.send_presence(
756 pto=self.user_jid.bare, pstatus=status, pshow=show, **kwargs
757 )
759 def send_cached_presence(self, to: JID) -> None:
760 if not self.__cached_presence:
761 self.xmpp.send_presence(pto=to, ptype="unavailable")
762 return
763 self.xmpp.send_presence(
764 pto=to,
765 pstatus=self.__cached_presence.status,
766 pshow=self.__cached_presence.show,
767 **self.__cached_presence.kwargs,
768 )
770 def send_gateway_message(self, text: str, **msg_kwargs) -> None:
771 """
772 Send a message from the gateway component to the user.
774 Can be used to indicate the user session status, ie "SMS code required", "connected", …
776 :param text: A text
777 """
778 self.xmpp.send_text(text, mto=self.user_jid, **msg_kwargs)
780 def send_gateway_invite(
781 self,
782 muc: LegacyMUC,
783 reason: Optional[str] = None,
784 password: Optional[str] = None,
785 ) -> None:
786 """
787 Send an invitation to join a MUC, emanating from the gateway component.
789 :param muc:
790 :param reason:
791 :param password:
792 """
793 self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user_jid)
795 async def input(self, text: str, **msg_kwargs):
796 """
797 Request user input via direct messages from the gateway component.
799 Wraps call to :meth:`.BaseSession.input`
801 :param text: The prompt to send to the user
802 :param msg_kwargs: Extra attributes
803 :return:
804 """
805 return await self.xmpp.input(self.user_jid, text, **msg_kwargs)
807 async def send_qr(self, text: str) -> None:
808 """
809 Sends a QR code generated from 'text' via HTTP Upload and send the URL to
810 ``self.user``
812 :param text: Text to encode as a QR code
813 """
814 await self.xmpp.send_qr(text, mto=self.user_jid)
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 return await muc.get_participant(nick, create=False, fill_first=True)
841 return muc
843 async def wait_for_ready(self, timeout: Optional[Union[int, float]] = 10):
844 # """
845 # Wait until session, contacts and bookmarks are ready
846 #
847 # (slidge internal use)
848 #
849 # :param timeout:
850 # :return:
851 # """
852 try:
853 await asyncio.wait_for(asyncio.shield(self.ready), timeout)
854 await asyncio.wait_for(asyncio.shield(self.contacts.ready), timeout)
855 await asyncio.wait_for(asyncio.shield(self.bookmarks.ready), timeout)
856 except asyncio.TimeoutError:
857 raise XMPPError(
858 "recipient-unavailable",
859 "Legacy session is not fully initialized, retry later",
860 )
862 def legacy_module_data_update(self, data: dict) -> None:
863 user = self.user
864 user.legacy_module_data.update(data)
865 self.xmpp.store.users.update(user)
867 def legacy_module_data_set(self, data: dict) -> None:
868 user = self.user
869 user.legacy_module_data = data
870 self.xmpp.store.users.update(user)
872 def legacy_module_data_clear(self) -> None:
873 user = self.user
874 user.legacy_module_data.clear()
875 self.xmpp.store.users.update(user)
878# keys = user.jid.bare
879_sessions: dict[str, BaseSession] = {}
880log = logging.getLogger(__name__)