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