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