slidge 0.1.0b2__py3-none-any.whl → 0.1.1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. slidge/__init__.py +55 -31
  2. slidge/__main__.py +118 -116
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +2 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +216 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +789 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +282 -116
  41. slidge/core/session.py +595 -372
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0084 → slixfix/link_preview}/__init__.py +3 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +14 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +25 -15
  56. slidge/slixfix/xep_0100/stanza.py +9 -0
  57. slidge/slixfix/xep_0153/__init__.py +10 -0
  58. slidge/slixfix/xep_0153/stanza.py +25 -0
  59. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  60. slidge/slixfix/xep_0264/__init__.py +5 -0
  61. slidge/slixfix/xep_0264/stanza.py +36 -0
  62. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  63. slidge/slixfix/xep_0292/__init__.py +5 -0
  64. slidge/slixfix/xep_0292/vcard4.py +100 -0
  65. slidge/slixfix/xep_0313/__init__.py +12 -0
  66. slidge/slixfix/xep_0313/mam.py +262 -0
  67. slidge/slixfix/xep_0313/stanza.py +359 -0
  68. slidge/slixfix/xep_0317/__init__.py +5 -0
  69. slidge/slixfix/xep_0317/hats.py +17 -0
  70. slidge/slixfix/xep_0317/stanza.py +28 -0
  71. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  72. slidge/slixfix/xep_0424/__init__.py +9 -0
  73. slidge/slixfix/xep_0424/retraction.py +77 -0
  74. slidge/slixfix/xep_0424/stanza.py +28 -0
  75. slidge/slixfix/xep_0490/__init__.py +8 -0
  76. slidge/slixfix/xep_0490/mds.py +47 -0
  77. slidge/slixfix/xep_0490/stanza.py +17 -0
  78. slidge/util/__init__.py +4 -6
  79. slidge/util/archive_msg.py +61 -0
  80. slidge/util/conf.py +206 -0
  81. slidge/util/db.py +57 -76
  82. slidge/util/schema.sql +126 -0
  83. slidge/util/sql.py +508 -0
  84. slidge/util/test.py +215 -25
  85. slidge/util/types.py +177 -4
  86. slidge/util/util.py +225 -59
  87. slidge-0.1.1.dist-info/METADATA +110 -0
  88. slidge-0.1.1.dist-info/RECORD +96 -0
  89. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/WHEEL +1 -1
  90. slidge/core/contact.py +0 -891
  91. slidge/core/gateway.py +0 -916
  92. slidge/plugins/discord/__init__.py +0 -90
  93. slidge/plugins/discord/client.py +0 -108
  94. slidge/plugins/discord/session.py +0 -162
  95. slidge/plugins/dummy.py +0 -203
  96. slidge/plugins/facebook.py +0 -493
  97. slidge/plugins/hackernews.py +0 -213
  98. slidge/plugins/mattermost/__init__.py +0 -1
  99. slidge/plugins/mattermost/api.py +0 -280
  100. slidge/plugins/mattermost/gateway.py +0 -365
  101. slidge/plugins/mattermost/websocket.py +0 -252
  102. slidge/plugins/signal/__init__.py +0 -3
  103. slidge/plugins/signal/contact.py +0 -106
  104. slidge/plugins/signal/gateway.py +0 -282
  105. slidge/plugins/signal/session.py +0 -448
  106. slidge/plugins/signal/txt.py +0 -53
  107. slidge/plugins/skype.py +0 -325
  108. slidge/plugins/steam.py +0 -310
  109. slidge/plugins/telegram/__init__.py +0 -5
  110. slidge/plugins/telegram/client.py +0 -228
  111. slidge/plugins/telegram/config.py +0 -12
  112. slidge/plugins/telegram/contact.py +0 -176
  113. slidge/plugins/telegram/gateway.py +0 -150
  114. slidge/plugins/telegram/session.py +0 -256
  115. slidge/util/xep_0030/__init__.py +0 -13
  116. slidge/util/xep_0030/disco.py +0 -811
  117. slidge/util/xep_0030/stanza/__init__.py +0 -7
  118. slidge/util/xep_0030/stanza/info.py +0 -270
  119. slidge/util/xep_0030/stanza/items.py +0 -147
  120. slidge/util/xep_0030/static.py +0 -467
  121. slidge/util/xep_0055/__init__.py +0 -5
  122. slidge/util/xep_0055/search.py +0 -75
  123. slidge/util/xep_0055/stanza.py +0 -10
  124. slidge/util/xep_0077/stanza.py +0 -71
  125. slidge/util/xep_0084/avatar.py +0 -137
  126. slidge/util/xep_0084/stanza.py +0 -104
  127. slidge/util/xep_0115/__init__.py +0 -12
  128. slidge/util/xep_0115/caps.py +0 -379
  129. slidge/util/xep_0115/stanza.py +0 -16
  130. slidge/util/xep_0115/static.py +0 -137
  131. slidge/util/xep_0292/__init__.py +0 -1
  132. slidge/util/xep_0292/stanza.py +0 -167
  133. slidge/util/xep_0292/vcard4.py +0 -75
  134. slidge/util/xep_0333/__init__.py +0 -10
  135. slidge/util/xep_0333/markers.py +0 -96
  136. slidge/util/xep_0333/stanza.py +0 -34
  137. slidge/util/xep_0356/__init__.py +0 -7
  138. slidge/util/xep_0356/permissions.py +0 -35
  139. slidge/util/xep_0356/privilege.py +0 -160
  140. slidge/util/xep_0356/stanza.py +0 -44
  141. slidge/util/xep_0363/__init__.py +0 -16
  142. slidge/util/xep_0363/http_upload.py +0 -215
  143. slidge/util/xep_0363/stanza.py +0 -46
  144. slidge/util/xep_0461/__init__.py +0 -6
  145. slidge/util/xep_0461/reply.py +0 -48
  146. slidge/util/xep_0461/stanza.py +0 -47
  147. slidge-0.1.0b2.dist-info/METADATA +0 -171
  148. slidge-0.1.0b2.dist-info/RECORD +0 -81
  149. /slidge/{plugins/__init__.py → py.typed} +0 -0
  150. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  151. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  152. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  153. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  154. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/LICENSE +0 -0
  155. {slidge-0.1.0b2.dist-info → slidge-0.1.1.dist-info}/entry_points.txt +0 -0
slidge/core/session.py CHANGED
@@ -1,55 +1,66 @@
1
- import functools
1
+ import asyncio
2
2
  import logging
3
- from typing import TYPE_CHECKING, Any, Generic, Literal, Optional, Type, TypeVar
4
-
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ Generic,
7
+ Iterable,
8
+ NamedTuple,
9
+ Optional,
10
+ Union,
11
+ cast,
12
+ )
13
+
14
+ import aiohttp
5
15
  from slixmpp import JID, Message
6
16
  from slixmpp.exceptions import XMPPError
17
+ from slixmpp.types import PresenceShows
7
18
 
8
- from ..core.contact import LegacyContactType, LegacyRoster, LegacyRosterType
9
- from ..util import ABCSubclassableOnceAtMost, BiDict
19
+ from ..command import SearchResult
20
+ from ..contact import LegacyContact, LegacyRoster
21
+ from ..group.bookmarks import LegacyBookmarks
22
+ from ..group.room import LegacyMUC
23
+ from ..util import ABCSubclassableOnceAtMost
10
24
  from ..util.db import GatewayUser, user_store
11
- from ..util.types import LegacyMessageType
25
+ from ..util.sql import SQLBiDict
26
+ from ..util.types import (
27
+ LegacyGroupIdType,
28
+ LegacyMessageType,
29
+ LegacyThreadType,
30
+ LinkPreview,
31
+ Mention,
32
+ PseudoPresenceShow,
33
+ RecipientType,
34
+ ResourceDict,
35
+ )
36
+ from ..util.util import deprecated
12
37
 
13
38
  if TYPE_CHECKING:
14
- from slidge import SearchResult
15
- from slidge.core.gateway import GatewayType
16
- else:
17
- GatewayType = TypeVar("GatewayType")
18
-
19
-
20
- def ignore_message_to_component_and_sent_carbons(func):
21
- @functools.wraps(func)
22
- async def wrapped(self: T, msg: Message):
23
- if msg.get_to() != self.xmpp.boundjid.bare:
24
- if (i := msg.get_id()) in self.ignore_messages:
25
- self.log.debug("Ignored message: %s", i)
26
- self.ignore_messages.remove(i)
27
- else:
28
- log.debug("FUNC: %s", func)
29
- return await func(self, msg)
30
- else:
31
- log.debug("Ignoring message to component: %s %s", self, msg)
32
-
33
- return wrapped
39
+ from ..group.participant import LegacyParticipant
40
+ from ..util.types import Sender
41
+ from .gateway import BaseGateway
34
42
 
35
43
 
36
- T = TypeVar("T", bound="BaseSession")
44
+ class CachedPresence(NamedTuple):
45
+ status: Optional[str]
46
+ show: Optional[str]
47
+ kwargs: dict[str, Any]
37
48
 
38
49
 
39
50
  class BaseSession(
40
- Generic[LegacyContactType, LegacyRosterType, GatewayType],
41
- metaclass=ABCSubclassableOnceAtMost,
51
+ Generic[LegacyMessageType, RecipientType], metaclass=ABCSubclassableOnceAtMost
42
52
  ):
43
53
  """
44
- Represents a gateway user logged in to the network and performing actions.
54
+ The session of a registered :term:`User`.
45
55
 
46
- Will be instantiated automatically when a user sends an online presence to the gateway
47
- component, as per :xep:`0100`.
56
+ Represents a gateway user logged in to the legacy network and performing actions.
48
57
 
49
- Must be subclassed for a functional slidge plugin.
58
+ Will be instantiated automatically on slidge startup for each registered user,
59
+ or upon registration for new (validated) users.
60
+
61
+ Must be subclassed for a functional :term:`Legacy Module`.
50
62
  """
51
63
 
52
- sent: BiDict[LegacyMessageType, str]
53
64
  """
54
65
  Since we cannot set the XMPP ID of messages sent by XMPP clients, we need to keep a mapping
55
66
  between XMPP IDs and legacy message IDs if we want to further refer to a message that was sent
@@ -57,489 +68,701 @@ class BaseSession(
57
68
  the official client of a legacy network.
58
69
  """
59
70
 
60
- xmpp: "GatewayType"
71
+ xmpp: "BaseGateway"
61
72
  """
62
73
  The gateway instance singleton. Use it for low-level XMPP calls or custom methods that are not
63
74
  session-specific.
64
75
  """
65
76
 
66
- def __init__(self, user: GatewayUser):
67
- self._roster_cls: Type[
68
- LegacyRosterType
69
- ] = LegacyRoster.get_self_or_unique_subclass()
77
+ http: aiohttp.ClientSession
70
78
 
79
+ MESSAGE_IDS_ARE_THREAD_IDS = False
80
+ """
81
+ Set this to True if the legacy service uses message IDs as thread IDs,
82
+ eg Mattermost, where you can only 'create a thread' by replying to the message,
83
+ in which case the message ID is also a thread ID (and all messages are potential
84
+ threads).
85
+ """
86
+ SPECIAL_MSG_ID_PREFIX: Optional[str] = None
87
+ """
88
+ If you set this, XMPP message IDs starting with this won't be converted to legacy ID,
89
+ but passed as is to :meth:`.on_react`, and usual checks for emoji restriction won't be
90
+ applied.
91
+ This can be used to implement voting in polls in a hacky way.
92
+ """
93
+
94
+ def __init__(self, user: GatewayUser):
71
95
  self.log = logging.getLogger(user.bare_jid)
72
96
 
73
97
  self.user = user
74
- self.sent: BiDict[
75
- LegacyMessageType, str
76
- ] = BiDict() # TODO: set a max size for this
98
+ self.sent = SQLBiDict[LegacyMessageType, str](
99
+ "session_message_sent", "legacy_id", "xmpp_id", self.user
100
+ )
101
+ # message ids (*not* stanza-ids), needed for last msg correction
102
+ self.muc_sent_msg_ids = SQLBiDict[LegacyMessageType, str](
103
+ "session_message_sent_muc", "legacy_id", "xmpp_id", self.user
104
+ )
77
105
 
78
106
  self.ignore_messages = set[str]()
79
107
 
80
- self.contacts: LegacyRosterType = self._roster_cls(self)
81
- self.post_init()
108
+ self.contacts: LegacyRoster = LegacyRoster.get_self_or_unique_subclass()(self)
109
+ self._logged = False
110
+ self.__reset_ready()
82
111
 
83
- @staticmethod
84
- def legacy_msg_id_to_xmpp_msg_id(legacy_msg_id: Any) -> str:
85
- """
86
- Convert a legacy msg ID to a valid XMPP msg ID.
87
- Needed for read marks and message corrections.
88
-
89
- The default implementation just converts the legacy ID to a :class:`str`,
90
- but this should be overridden in case some characters needs to be escaped,
91
- or to add some additional, legacy network-specific logic.
112
+ self.bookmarks: LegacyBookmarks = LegacyBookmarks.get_self_or_unique_subclass()(
113
+ self
114
+ )
92
115
 
93
- :param legacy_msg_id:
94
- :return: Should return a string that is usable as an XMPP stanza ID
95
- """
96
- return str(legacy_msg_id)
116
+ self.http = self.xmpp.http
97
117
 
98
- @staticmethod
99
- def xmpp_msg_id_to_legacy_msg_id(i: str) -> LegacyMessageType:
100
- """
101
- Convert a legacy XMPP ID to a valid XMPP msg ID.
102
- Needed for read marks and message corrections.
118
+ self.threads = SQLBiDict[str, LegacyThreadType]( # type:ignore
119
+ "session_thread_sent_muc", "legacy_id", "xmpp_id", self.user
120
+ )
121
+ self.thread_creation_lock = asyncio.Lock()
103
122
 
104
- The default implementation just converts the legacy ID to a :class:`str`,
105
- but this should be overridden in case some characters needs to be escaped,
106
- or to add some additional, legacy network-specific logic.
123
+ self.__cached_presence: Optional[CachedPresence] = None
107
124
 
108
- The default implementation is an identity function
125
+ self.avatar_hash: Optional[str] = None
109
126
 
110
- :param i: The XMPP stanza ID
111
- :return: An ID that can be used to identify a message on the legacy network
112
- """
113
- return i
127
+ self.__tasks = set[asyncio.Task]()
114
128
 
115
- @classmethod
116
- def _from_user_or_none(cls, user):
117
- if user is None:
118
- raise XMPPError(
119
- text="User not found", condition="subscription-required", etype="auth"
120
- )
129
+ def __remove_task(self, fut):
130
+ self.log.debug("Removing fut %s", fut)
131
+ self.__tasks.remove(fut)
121
132
 
122
- session = _sessions.get(user)
123
- if session is None:
124
- _sessions[user] = session = cls(user)
125
- return session
133
+ def create_task(self, coro) -> None:
134
+ task = self.xmpp.loop.create_task(coro)
135
+ self.__tasks.add(task)
136
+ self.log.debug("Creating task %s", task)
137
+ task.add_done_callback(lambda _: self.__remove_task(task))
126
138
 
127
- @classmethod
128
- def from_user(cls, user):
129
- return cls._from_user_or_none(user)
139
+ def cancel_all_tasks(self):
140
+ for task in self.__tasks:
141
+ task.cancel()
130
142
 
131
- @classmethod
132
- def from_stanza(cls: Type[T], s) -> T:
143
+ async def login(self) -> Optional[str]:
133
144
  """
134
- Get a user's :class:`.LegacySession` using the "from" field of a stanza
145
+ Logs in the gateway user to the legacy network.
135
146
 
136
- Meant to be called from :class:`BaseGateway` only.
147
+ Triggered when the gateway start and on user registration.
148
+ It is recommended that this function returns once the user is logged in,
149
+ so if you need to await forever (for instance to listen to incoming events),
150
+ it's a good idea to wrap your listener in an asyncio.Task.
137
151
 
138
- :param s:
139
- :return:
152
+ :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'"
140
153
  """
141
- return cls._from_user_or_none(user_store.get_by_stanza(s))
154
+ raise NotImplementedError
142
155
 
143
- @classmethod
144
- def from_jid(cls: Type[T], jid: JID) -> T:
156
+ async def logout(self):
145
157
  """
146
- Get a user's :class:`.LegacySession` using its jid
158
+ Logs out the gateway user from the legacy network.
147
159
 
148
- Meant to be called from :class:`BaseGateway` only.
149
-
150
- :param jid:
151
- :return:
160
+ Called on gateway shutdown.
152
161
  """
153
- return cls._from_user_or_none(user_store.get_by_jid(jid))
162
+ raise NotImplementedError
154
163
 
155
- @classmethod
156
- async def kill_by_jid(cls, jid: JID):
164
+ async def on_text(
165
+ self,
166
+ chat: RecipientType,
167
+ text: str,
168
+ *,
169
+ reply_to_msg_id: Optional[LegacyMessageType] = None,
170
+ reply_to_fallback_text: Optional[str] = None,
171
+ reply_to: Optional["Sender"] = None,
172
+ thread: Optional[LegacyThreadType] = None,
173
+ link_previews: Iterable[LinkPreview] = (),
174
+ mentions: Optional[list[Mention]] = None,
175
+ ) -> Optional[LegacyMessageType]:
157
176
  """
158
- Terminate a user session.
177
+ Triggered when the user sends a text message from XMPP to a bridged entity, e.g.
178
+ to ``translated_user_name@slidge.example.com``, or ``translated_group_name@slidge.example.com``
159
179
 
160
- Meant to be called from :class:`BaseGateway` only.
180
+ Override this and implement sending a message to the legacy network in this method.
161
181
 
162
- :param jid:
163
- :return:
164
- """
165
- log.debug("Killing session of %s", jid)
166
- for user, session in _sessions.items():
167
- if user.jid == jid.bare:
168
- break
169
- else:
170
- log.debug("Did not find a session for %s", jid)
171
- return
172
- for c in session.contacts:
173
- c.unsubscribe()
174
- await session.logout()
175
- await cls.xmpp.unregister(user)
176
- del _sessions[user]
177
- del user
178
- del session
182
+ :param text: Content of the message
183
+ :param chat: Recipient of the message. :class:`.LegacyContact` instance for 1:1 chat,
184
+ :class:`.MUC` instance for groups.
185
+ :param reply_to_msg_id: A legacy message ID if the message references (quotes)
186
+ another message (:xep:`0461`)
187
+ :param reply_to_fallback_text: Content of the quoted text. Not necessarily set
188
+ by XMPP clients
189
+ :param reply_to: Author of the quoted message. :class:`LegacyContact` instance for
190
+ 1:1 chat, :class:`LegacyParticipant` instance for groups.
191
+ If `None`, should be interpreted as a self-reply if reply_to_msg_id is not None.
192
+ :param link_previews: A list of sender-generated link previews.
193
+ At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
194
+ supports it.
195
+ :param mentions: (only for groups) A list of Contacts mentioned by their
196
+ nicknames.
197
+ :param thread:
179
198
 
180
- @ignore_message_to_component_and_sent_carbons
181
- async def send_from_msg(self, m: Message):
199
+ :return: An ID of some sort that can be used later to ack and mark the message
200
+ as read by the user
182
201
  """
183
- Meant to be called from :class:`BaseGateway` only.
202
+ raise NotImplementedError
184
203
 
185
- :param m:
186
- :return:
204
+ send_text = deprecated("BaseSession.send_text", on_text)
205
+
206
+ async def on_file(
207
+ self,
208
+ chat: RecipientType,
209
+ url: str,
210
+ *,
211
+ http_response: aiohttp.ClientResponse,
212
+ reply_to_msg_id: Optional[LegacyMessageType] = None,
213
+ reply_to_fallback_text: Optional[str] = None,
214
+ reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None,
215
+ thread: Optional[LegacyThreadType] = None,
216
+ ) -> Optional[LegacyMessageType]:
187
217
  """
188
- if m["replace"]["id"] or m["apply_to"]["id"]:
189
- # ignore last message correction and retraction (handled by a specific method)
190
- return
218
+ Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
191
219
 
192
- contact: LegacyContactType = self.contacts.by_stanza(m)
220
+ :param url: URL of the file
221
+ :param chat: See :meth:`.BaseSession.on_text`
222
+ :param http_response: The HTTP GET response object on the URL
223
+ :param reply_to_msg_id: See :meth:`.BaseSession.on_text`
224
+ :param reply_to_fallback_text: See :meth:`.BaseSession.on_text`
225
+ :param reply_to: See :meth:`.BaseSession.on_text`
226
+ :param thread:
193
227
 
194
- url = m["oob"]["url"]
195
- text = m["body"]
196
- if (
197
- m["feature_fallback"]["for"] == self.xmpp["xep_0461"].namespace
198
- and contact.REPLIES
199
- ):
200
- text = m["feature_fallback"].get_stripped_body()
228
+ :return: An ID of some sort that can be used later to ack and mark the message
229
+ as read by the user
230
+ """
231
+ raise NotImplementedError
201
232
 
202
- if url:
203
- legacy_msg_id = await self.send_file(
204
- url, contact, reply_to_msg_id=m["reply"]["id"] or None
205
- )
206
- elif text:
207
- legacy_msg_id = await self.send_text(
208
- text, contact, reply_to_msg_id=m["reply"]["id"] or None
209
- )
210
- else:
211
- log.debug("Ignoring %s", m)
212
- return
213
- if legacy_msg_id is not None:
214
- self.sent[legacy_msg_id] = m.get_id()
233
+ send_file = deprecated("BaseSession.send_file", on_file)
215
234
 
216
- @ignore_message_to_component_and_sent_carbons
217
- async def active_from_msg(self, m: Message):
235
+ async def on_active(
236
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
237
+ ):
218
238
  """
219
- Meant to be called from :class:`BaseGateway` only.
239
+ Triggered when the user sends an 'active' chat state (:xep:`0085`)
220
240
 
221
- :param m:
222
- :return:
241
+ :param chat: See :meth:`.BaseSession.on_text`
242
+ :param thread:
223
243
  """
224
- if m.get_to() != self.xmpp.boundjid.bare:
225
- await self.active(self.contacts.by_stanza(m))
244
+ raise NotImplementedError
226
245
 
227
- @ignore_message_to_component_and_sent_carbons
228
- async def inactive_from_msg(self, m: Message):
229
- """
230
- Meant to be called from :class:`BaseGateway` only.
246
+ active = deprecated("BaseSession.active", on_active)
231
247
 
232
- :param m:
233
- :return:
248
+ async def on_inactive(
249
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
250
+ ):
234
251
  """
235
- if m.get_to() != self.xmpp.boundjid.bare:
236
- await self.inactive(self.contacts.by_stanza(m))
252
+ Triggered when the user sends an 'inactive' chat state (:xep:`0085`)
237
253
 
238
- @ignore_message_to_component_and_sent_carbons
239
- async def composing_from_msg(self, m: Message):
254
+ :param chat: See :meth:`.BaseSession.on_text`
255
+ :param thread:
240
256
  """
241
- Meant to be called from :class:`BaseGateway` only.
257
+ raise NotImplementedError
242
258
 
243
- :param m:
244
- :return:
245
- """
246
- if m.get_to() != self.xmpp.boundjid.bare:
247
- log.debug("COMPOSING: %s", self.contacts.by_stanza(m))
248
- await self.composing(self.contacts.by_stanza(m))
259
+ inactive = deprecated("BaseSession.inactive", on_inactive)
249
260
 
250
- @ignore_message_to_component_and_sent_carbons
251
- async def paused_from_msg(self, m: Message):
261
+ async def on_composing(
262
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
263
+ ):
252
264
  """
253
- Meant to be called from :class:`BaseGateway` only.
265
+ Triggered when the user starts typing in a legacy chat (:xep:`0085`)
254
266
 
255
- :param m:
256
- :return:
267
+ :param chat: See :meth:`.BaseSession.on_text`
268
+ :param thread:
257
269
  """
258
- if m.get_to() != self.xmpp.boundjid.bare:
259
- await self.paused(self.contacts.by_stanza(m))
270
+ raise NotImplementedError
260
271
 
261
- @ignore_message_to_component_and_sent_carbons
262
- async def displayed_from_msg(self, m: Message):
263
- """
264
- Meant to be called from :class:`BaseGateway` only.
272
+ composing = deprecated("BaseSession.composing", on_composing)
265
273
 
266
- :param m:
267
- :return:
274
+ async def on_paused(
275
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
276
+ ):
268
277
  """
269
- displayed_msg_id = m["displayed"]["id"]
270
- try:
271
- legacy_msg_id = self.xmpp_msg_id_to_legacy_msg_id(displayed_msg_id)
272
- except NotImplementedError:
273
- log.debug("Couldn't convert xmpp msg ID to legacy ID, ignoring read mark")
274
- return
278
+ Triggered when the user pauses typing in a legacy chat (:xep:`0085`)
275
279
 
276
- await self.displayed(legacy_msg_id, self.contacts.by_stanza(m))
277
-
278
- @ignore_message_to_component_and_sent_carbons
279
- async def correct_from_msg(self, m: Message):
280
- xmpp_id = m["replace"]["id"]
281
- legacy_id = self.sent.inverse.get(xmpp_id)
282
- if legacy_id is None:
283
- log.debug("Did not find legacy ID to correct")
284
- new_legacy_msg_id = await self.send_text(
285
- m["body"], self.contacts.by_stanza(m)
286
- )
287
- else:
288
- new_legacy_msg_id = await self.correct(
289
- m["body"], legacy_id, self.contacts.by_stanza(m)
290
- )
291
- if new_legacy_msg_id is not None:
292
- self.sent[new_legacy_msg_id] = m.get_id()
293
-
294
- @ignore_message_to_component_and_sent_carbons
295
- async def react_from_msg(self, m: Message):
296
- react_to = m["reactions"]["id"]
297
- if (legacy_id := self.sent.inverse.get(react_to)) is None:
298
- log.debug("Cannot find the XMPP ID of this msg: %s", react_to)
299
- try:
300
- legacy_id = self.xmpp_msg_id_to_legacy_msg_id(react_to)
301
- except ValueError:
302
- log.warning(
303
- "Could not convert legacy ID, xmpp reaction was not sent: %s", m
304
- )
305
- return
306
- await self.react(
307
- legacy_id, [r["value"] for r in m["reactions"]], self.contacts.by_stanza(m)
308
- )
280
+ :param chat: See :meth:`.BaseSession.on_text`
281
+ :param thread:
282
+ """
283
+ raise NotImplementedError
309
284
 
310
- @ignore_message_to_component_and_sent_carbons
311
- async def retract_from_msg(self, m: Message):
312
- xmpp_id = m["apply_to"]["id"]
313
- if (legacy_id := self.sent.inverse.get(xmpp_id)) is None:
314
- log.debug(
315
- "Cannot find the XMPP ID of this msg: %s, cannot retract", xmpp_id
316
- )
317
- return
318
- await self.retract(legacy_id, self.contacts.by_stanza(m))
285
+ paused = deprecated("BaseSession.paused", on_paused)
319
286
 
320
- def send_gateway_status(
287
+ async def on_displayed(
321
288
  self,
322
- status: Optional[str] = None,
323
- show=Optional[Literal["away", "chat", "dnd", "xa"]],
324
- **kwargs,
289
+ chat: RecipientType,
290
+ legacy_msg_id: LegacyMessageType,
291
+ thread: Optional[LegacyThreadType] = None,
325
292
  ):
326
293
  """
327
- Send a presence from the gateway to the user.
294
+ Triggered when the user reads a message in a legacy chat. (:xep:`0333`)
328
295
 
329
- Can be used to indicate the user session status, ie "SMS code required", "connected", …
330
-
331
- :param status: A status message
332
- :param show: Presence stanza 'show' element. I suggest using "dnd" to show
333
- that the gateway is not fully functional
334
- """
335
- self.xmpp.send_presence(
336
- pto=self.user.bare_jid, pstatus=status, pshow=show, **kwargs
337
- )
296
+ This is only possible if a valid ``legacy_msg_id`` was passed when
297
+ transmitting a message from a legacy chat to the user, eg in
298
+ :meth:`slidge.contact.LegacyContact.send_text`
299
+ or
300
+ :meth:`slidge.group.LegacyParticipant.send_text`.
338
301
 
339
- def send_gateway_message(self, text, **msg_kwargs):
302
+ :param chat: See :meth:`.BaseSession.on_text`
303
+ :param legacy_msg_id: Identifier of the message/
304
+ :param thread:
340
305
  """
341
- Send a message from the gateway component to the user.
306
+ raise NotImplementedError
342
307
 
343
- Can be used to indicate the user session status, ie "SMS code required", "connected", …
308
+ displayed = deprecated("BaseSession.displayed", on_displayed)
344
309
 
345
- :param text: A text
310
+ async def on_correct(
311
+ self,
312
+ chat: RecipientType,
313
+ text: str,
314
+ legacy_msg_id: LegacyMessageType,
315
+ *,
316
+ thread: Optional[LegacyThreadType] = None,
317
+ link_previews: Iterable[LinkPreview] = (),
318
+ mentions: Optional[list[Mention]] = None,
319
+ ) -> Optional[LegacyMessageType]:
346
320
  """
347
- self.xmpp.send_message(
348
- mto=self.user.jid, mbody=text, mfrom=self.xmpp.boundjid, **msg_kwargs
349
- )
321
+ Triggered when the user corrects a message using :xep:`0308`
350
322
 
351
- async def input(self, text: str, **msg_kwargs):
323
+ This is only possible if a valid ``legacy_msg_id`` was returned by
324
+ :meth:`.on_text`.
325
+
326
+ :param chat: See :meth:`.BaseSession.on_text`
327
+ :param text: The new text
328
+ :param legacy_msg_id: Identifier of the edited message
329
+ :param thread:
330
+ :param link_previews: A list of sender-generated link previews.
331
+ At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
332
+ supports it.
333
+ :param mentions: (only for groups) A list of Contacts mentioned by their
334
+ nicknames.
352
335
  """
353
- Request user input via direct messages.
336
+ raise NotImplementedError
354
337
 
355
- Wraps call to :meth:`.BaseSession.input`
338
+ correct = deprecated("BaseSession.correct", on_correct)
356
339
 
357
- :param text: The prompt to send to the user
358
- :param msg_kwargs: Extra attributes
359
- :return:
340
+ async def on_react(
341
+ self,
342
+ chat: RecipientType,
343
+ legacy_msg_id: LegacyMessageType,
344
+ emojis: list[str],
345
+ thread: Optional[LegacyThreadType] = None,
346
+ ):
360
347
  """
361
- return await self.xmpp.input(self.user.jid, text, **msg_kwargs)
348
+ Triggered when the user sends message reactions (:xep:`0444`).
362
349
 
363
- async def send_qr(self, text: str):
350
+ :param chat: See :meth:`.BaseSession.on_text`
351
+ :param thread:
352
+ :param legacy_msg_id: ID of the message the user reacts to
353
+ :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``.
354
+ An empty string means "no reaction", ie, remove all reactions if any were present before
364
355
  """
365
- Sends a QR code generated from 'text' via HTTP Upload and send the URL to
366
- ``self.user``
356
+ raise NotImplementedError
367
357
 
368
- :param text: Text to encode as a QR code
369
- """
370
- await self.xmpp.send_qr(text, mto=self.user.jid)
358
+ react = deprecated("BaseSession.react", on_react)
371
359
 
372
- def post_init(self):
360
+ async def on_retract(
361
+ self,
362
+ chat: RecipientType,
363
+ legacy_msg_id: LegacyMessageType,
364
+ thread: Optional[LegacyThreadType] = None,
365
+ ):
373
366
  """
374
- Add useful attributes for your session here, if you wish.
367
+ Triggered when the user retracts (:xep:`0424`) a message.
375
368
 
376
- In most cases, this is the right place to add a legacy network-specific
377
- ``LegacyClient``-like instance attached to this gateway user.
369
+ :param chat: See :meth:`.BaseSession.on_text`
370
+ :param thread:
371
+ :param legacy_msg_id: Legacy ID of the retracted message
378
372
  """
379
- pass
373
+ raise NotImplementedError
380
374
 
381
- async def login(self) -> Optional[str]:
382
- """
383
- Login the gateway user to the legacy network.
375
+ retract = deprecated("BaseSession.retract", on_retract)
384
376
 
385
- Triggered when the gateway start and on user registration.
386
- It is recommended that this function returns once the user is logged in,
387
- so if you need to await forever (for instance to listen to incoming events),
388
- it's a good idea to wrap your listener in an asyncio.Task.
377
+ async def on_presence(
378
+ self,
379
+ resource: str,
380
+ show: PseudoPresenceShow,
381
+ status: str,
382
+ resources: dict[str, ResourceDict],
383
+ merged_resource: Optional[ResourceDict],
384
+ ):
385
+ """
386
+ Called when the gateway component receives a presence, ie, when
387
+ one of the user's clients goes online of offline, or changes its
388
+ status.
389
389
 
390
- :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'"
390
+ :param resource: The XMPP client identifier, arbitrary string.
391
+ :param show: The presence ``<show>``, if available. If the resource is
392
+ just 'available' without any ``<show>`` element, this is an empty
393
+ str.
394
+ :param status: A status message, like a deeply profound quote, eg,
395
+ "Roses are red, violets are blue, [INSERT JOKE]".
396
+ :param resources: A summary of all the resources for this user.
397
+ :param merged_resource: A global presence for the user account,
398
+ following rules described in :meth:`merge_resources`
391
399
  """
392
400
  raise NotImplementedError
393
401
 
394
- async def logout(self):
402
+ presence = deprecated("BaseSession.presence", on_presence)
403
+
404
+ async def on_search(self, form_values: dict[str, str]) -> Optional[SearchResult]:
395
405
  """
396
- Logout the gateway user from the legacy network.
406
+ Triggered when the user uses Jabber Search (:xep:`0055`) on the component
397
407
 
398
- Called on user unregistration and gateway shutdown.
408
+ Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS`
409
+
410
+ :param form_values: search query, defined for a specific plugin by overriding
411
+ in :attr:`.BaseGateway.SEARCH_FIELDS`
412
+ :return:
399
413
  """
400
414
  raise NotImplementedError
401
415
 
402
- def re_login(self):
403
- """
404
- Logout then re-login
416
+ search = deprecated("BaseSession.search", on_search)
405
417
 
406
- No reason to override this
418
+ async def on_avatar(
419
+ self,
420
+ bytes_: Optional[bytes],
421
+ hash_: Optional[str],
422
+ type_: Optional[str],
423
+ width: Optional[int],
424
+ height: Optional[int],
425
+ ) -> None:
426
+ """
427
+ Triggered when the user uses modifies their avatar via :xep:`0084`.
428
+
429
+ :param bytes_: The data of the avatar. According to the spec, this
430
+ should always be a PNG, but some implementations do not respect
431
+ that. If `None` it means the user has unpublished their avatar.
432
+ :param hash_: The SHA1 hash of the avatar data. This is an identifier of
433
+ the avatar.
434
+ :param type_: The MIME type of the avatar.
435
+ :param width: The width of the avatar image.
436
+ :param height: The height of the avatar image.
407
437
  """
408
- self.xmpp.re_login(self)
438
+ raise NotImplementedError
409
439
 
410
- async def send_text(
411
- self,
412
- t: str,
413
- c: LegacyContactType,
414
- *,
415
- reply_to_msg_id: Optional[LegacyMessageType] = None,
416
- ) -> Optional[LegacyMessageType]:
440
+ async def on_moderate(
441
+ self, muc: LegacyMUC, legacy_msg_id: LegacyMessageType, reason: Optional[str]
442
+ ):
417
443
  """
418
- Triggered when the user sends a text message from xmpp to a bridged contact, e.g.
419
- to ``translated_user_name@slidge.example.com``.
444
+ Triggered when the user attempts to retract a message that was sent in
445
+ a MUC using :xep:`0425`.
420
446
 
421
- Override this and implement sending a message to the legacy network in this method.
447
+ If retraction is not possible, this should raise the appropriate
448
+ XMPPError with a human-readable message.
422
449
 
423
- :param t: Content of the message
424
- :param c: Recipient of the message
425
- :param reply_to_msg_id:
426
- :return: An ID of some sort that can be used later to ack and mark the message
427
- as read by the user
450
+ NB: the legacy module is responsible for calling
451
+ :method:`LegacyParticipant.moderate` when this is successful, because
452
+ slidge will acknowledge the moderation IQ, but will not send the
453
+ moderation message from the MUC automatically.
454
+
455
+ :param muc: The MUC in which the message was sent
456
+ :param legacy_msg_id: The legacy ID of the message to be retracted
457
+ :param reason: Optionally, a reason for the moderation, given by the
458
+ user-moderator.
428
459
  """
429
460
  raise NotImplementedError
430
461
 
431
- async def send_file(
432
- self,
433
- u: str,
434
- c: LegacyContactType,
435
- *,
436
- reply_to_msg_id: Optional[LegacyMessageType] = None,
437
- ) -> Optional[LegacyMessageType]:
462
+ async def on_create_group(
463
+ self, name: str, contacts: list[LegacyContact]
464
+ ) -> LegacyGroupIdType:
438
465
  """
439
- Triggered when the user has sends a file using HTTP Upload (:xep:`0363`)
466
+ Triggered when the user request the creation of a group via the
467
+ dedicated :term:`Command`.
440
468
 
441
- :param u: URL of the file
442
- :param c: Recipient of the file
443
- :param reply_to_msg_id:
444
- :return: An ID of some sort that can be used later to ack and mark the message
445
- as read by the user
469
+ :param name: Name of the group
470
+ :param contacts: list of contacts that should be members of the group
446
471
  """
447
472
  raise NotImplementedError
448
473
 
449
- async def active(self, c: LegacyContactType):
474
+ async def on_invitation(
475
+ self, contact: LegacyContact, muc: LegacyMUC, reason: Optional[str]
476
+ ):
450
477
  """
451
- Triggered when the user sends an 'active' chat state to the legacy network (:xep:`0085`)
478
+ Triggered when the user invites a :term:`Contact` to a legacy MUC via
479
+ :xep:`0249`.
452
480
 
453
- :param c: Recipient of the active chat state
454
- """
455
- raise NotImplementedError
481
+ The default implementation calls :meth:`LegacyMUC.on_set_affiliation`
482
+ with the 'member' affiliation. Override if you want to customize this
483
+ behaviour.
456
484
 
457
- async def inactive(self, c: LegacyContactType):
485
+ :param contact: The invitee
486
+ :param muc: The group
487
+ :param reason: Optionally, a reason
458
488
  """
459
- Triggered when the user sends an 'inactive' chat state to the legacy network (:xep:`0085`)
489
+ await muc.on_set_affiliation(contact, "member", reason, None)
460
490
 
461
- :param c:
491
+ def __reset_ready(self):
492
+ self.ready = self.xmpp.loop.create_future()
493
+
494
+ @property
495
+ def logged(self):
496
+ return self._logged
497
+
498
+ @logged.setter
499
+ def logged(self, v: bool):
500
+ self._logged = v
501
+ if self.ready.done():
502
+ if v:
503
+ return
504
+ self.__reset_ready()
505
+ else:
506
+ if v:
507
+ self.ready.set_result(True)
508
+
509
+ def __repr__(self):
510
+ return f"<Session of {self.user}>"
511
+
512
+ def shutdown(self) -> asyncio.Task:
513
+ for c in self.contacts:
514
+ c.offline()
515
+ for m in self.bookmarks:
516
+ m.shutdown()
517
+ return self.xmpp.loop.create_task(self.logout())
518
+
519
+ @staticmethod
520
+ def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
462
521
  """
463
- raise NotImplementedError
522
+ Convert a legacy msg ID to a valid XMPP msg ID.
523
+ Needed for read marks, retractions and message corrections.
524
+
525
+ The default implementation just converts the legacy ID to a :class:`str`,
526
+ but this should be overridden in case some characters needs to be escaped,
527
+ or to add some additional,
528
+ :term:`legacy network <Legacy Network`>-specific logic.
464
529
 
465
- async def composing(self, c: LegacyContactType):
530
+ :param legacy_msg_id:
531
+ :return: A string that is usable as an XMPP stanza ID
466
532
  """
467
- Triggered when the user starts typing in the window of a legacy contact (:xep:`0085`)
533
+ return str(legacy_msg_id)
534
+
535
+ legacy_msg_id_to_xmpp_msg_id = staticmethod(
536
+ deprecated("BaseSession.legacy_msg_id_to_xmpp_msg_id", legacy_to_xmpp_msg_id)
537
+ )
468
538
 
469
- :param c:
539
+ @staticmethod
540
+ def xmpp_to_legacy_msg_id(i: str) -> LegacyMessageType:
470
541
  """
471
- raise NotImplementedError
542
+ Convert a legacy XMPP ID to a valid XMPP msg ID.
543
+ Needed for read marks and message corrections.
544
+
545
+ The default implementation just converts the legacy ID to a :class:`str`,
546
+ but this should be overridden in case some characters needs to be escaped,
547
+ or to add some additional,
548
+ :term:`legacy network <Legacy Network`>-specific logic.
549
+
550
+ The default implementation is an identity function.
472
551
 
473
- async def paused(self, c: LegacyContactType):
552
+ :param i: The XMPP stanza ID
553
+ :return: An ID that can be used to identify a message on the legacy network
474
554
  """
475
- Triggered when the user pauses typing in the window of a legacy contact (:xep:`0085`)
555
+ return cast(LegacyMessageType, i)
556
+
557
+ xmpp_msg_id_to_legacy_msg_id = staticmethod(
558
+ deprecated("BaseSession.xmpp_msg_id_to_legacy_msg_id", xmpp_to_legacy_msg_id)
559
+ )
560
+
561
+ def raise_if_not_logged(self):
562
+ if not self.logged:
563
+ raise XMPPError(
564
+ "internal-server-error",
565
+ text="You are not logged to the legacy network",
566
+ )
567
+
568
+ @classmethod
569
+ def _from_user_or_none(cls, user):
570
+ if user is None:
571
+ log.debug("user not found", stack_info=True)
572
+ raise XMPPError(text="User not found", condition="subscription-required")
573
+
574
+ session = _sessions.get(user)
575
+ if session is None:
576
+ _sessions[user] = session = cls(user)
577
+ return session
476
578
 
477
- :param c:
579
+ @classmethod
580
+ def from_user(cls, user):
581
+ return cls._from_user_or_none(user)
582
+
583
+ @classmethod
584
+ def from_stanza(cls, s) -> "BaseSession":
585
+ # """
586
+ # Get a user's :class:`.LegacySession` using the "from" field of a stanza
587
+ #
588
+ # Meant to be called from :class:`BaseGateway` only.
589
+ #
590
+ # :param s:
591
+ # :return:
592
+ # """
593
+ return cls._from_user_or_none(user_store.get_by_stanza(s))
594
+
595
+ @classmethod
596
+ def from_jid(cls, jid: JID) -> "BaseSession":
597
+ # """
598
+ # Get a user's :class:`.LegacySession` using its jid
599
+ #
600
+ # Meant to be called from :class:`BaseGateway` only.
601
+ #
602
+ # :param jid:
603
+ # :return:
604
+ # """
605
+ return cls._from_user_or_none(user_store.get_by_jid(jid))
606
+
607
+ @classmethod
608
+ async def kill_by_jid(cls, jid: JID):
609
+ # """
610
+ # Terminate a user session.
611
+ #
612
+ # Meant to be called from :class:`BaseGateway` only.
613
+ #
614
+ # :param jid:
615
+ # :return:
616
+ # """
617
+ log.debug("Killing session of %s", jid)
618
+ for user, session in _sessions.items():
619
+ if user.jid == jid.bare:
620
+ break
621
+ else:
622
+ log.debug("Did not find a session for %s", jid)
623
+ return
624
+ for c in session.contacts:
625
+ c.unsubscribe()
626
+ await cls.xmpp.unregister(user)
627
+ del _sessions[user]
628
+ del user
629
+ del session
630
+
631
+ def __ack(self, msg: Message):
632
+ if not self.xmpp.PROPER_RECEIPTS:
633
+ self.xmpp.delivery_receipt.ack(msg)
634
+
635
+ def send_gateway_status(
636
+ self,
637
+ status: Optional[str] = None,
638
+ show=Optional[PresenceShows],
639
+ **kwargs,
640
+ ):
478
641
  """
479
- raise NotImplementedError
642
+ Send a presence from the gateway to the user.
643
+
644
+ Can be used to indicate the user session status, ie "SMS code required", "connected", …
480
645
 
481
- async def displayed(self, legacy_msg_id: Any, c: LegacyContactType):
646
+ :param status: A status message
647
+ :param show: Presence stanza 'show' element. I suggest using "dnd" to show
648
+ that the gateway is not fully functional
482
649
  """
483
- Triggered when the user reads a message sent by a legacy contact. (:xep:`0333`)
650
+ self.__cached_presence = CachedPresence(status, show, kwargs)
651
+ self.xmpp.send_presence(
652
+ pto=self.user.bare_jid, pstatus=status, pshow=show, **kwargs
653
+ )
484
654
 
485
- This is only possible if a valid ``legacy_msg_id`` was passed when transmitting a message
486
- from a contact to the user in :meth:`.LegacyContact.sent_text` or :meth:`slidge.LegacyContact.send_file`.
655
+ def send_cached_presence(self, to: JID):
656
+ if not self.__cached_presence:
657
+ self.xmpp.send_presence(pto=to, ptype="unavailable")
658
+ return
659
+ self.xmpp.send_presence(
660
+ pto=to,
661
+ pstatus=self.__cached_presence.status,
662
+ pshow=self.__cached_presence.show,
663
+ **self.__cached_presence.kwargs,
664
+ )
487
665
 
488
- :param legacy_msg_id: Identifier of the message, passed to :meth:`slidge.LegacyContact.send_text`
489
- or :meth:`slidge.LegacyContact.send_file`
490
- :param c:
666
+ def send_gateway_message(self, text: str, **msg_kwargs):
491
667
  """
492
- raise NotImplementedError
668
+ Send a message from the gateway component to the user.
493
669
 
494
- async def correct(
495
- self, text: str, legacy_msg_id: Any, c: LegacyContactType
496
- ) -> Optional[LegacyMessageType]:
670
+ Can be used to indicate the user session status, ie "SMS code required", "connected", …
671
+
672
+ :param text: A text
497
673
  """
498
- Triggered when the user corrected a message using :xep:`0308`
674
+ self.xmpp.send_text(text, mto=self.user.jid, **msg_kwargs)
499
675
 
500
- This is only possible if a valid ``legacy_msg_id`` was passed when transmitting a message
501
- from a contact to the user in :meth:`.LegacyContact.send_text` or :meth:`slidge.LegacyContact.send_file`.
676
+ def send_gateway_invite(
677
+ self,
678
+ muc: LegacyMUC,
679
+ reason: Optional[str] = None,
680
+ password: Optional[str] = None,
681
+ ):
682
+ """
683
+ Send an invitation to join a MUC, emanating from the gateway component.
502
684
 
503
- :param text:
504
- :param legacy_msg_id:
505
- :param c:
685
+ :param muc:
686
+ :param reason:
687
+ :param password:
506
688
  """
507
- raise NotImplementedError
689
+ self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user.jid)
508
690
 
509
- async def search(self, form_values: dict[str, str]) -> "SearchResult":
691
+ async def input(self, text: str, **msg_kwargs):
510
692
  """
511
- Triggered when the user uses Jabber Search (:xep:`0055`) on the component
693
+ Request user input via direct messages from the gateway component.
512
694
 
513
- Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS`
695
+ Wraps call to :meth:`.BaseSession.input`
514
696
 
515
- :param form_values: search query, defined for a specific plugin by overriding
516
- in :attr:`.BaseGateway.SEARCH_FIELDS`
697
+ :param text: The prompt to send to the user
698
+ :param msg_kwargs: Extra attributes
517
699
  :return:
518
700
  """
519
- raise NotImplementedError
701
+ return await self.xmpp.input(self.user.jid, text, **msg_kwargs)
520
702
 
521
- async def react(self, legacy_msg_id: Any, emojis: list[str], c: LegacyContactType):
703
+ async def send_qr(self, text: str):
522
704
  """
523
- Triggered when the user sends message reactions (:xep:`0444`).
705
+ Sends a QR code generated from 'text' via HTTP Upload and send the URL to
706
+ ``self.user``
524
707
 
525
- :param legacy_msg_id: ID of the message the user reacts to
526
- :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``.
527
- An empty string means "no reaction", ie, remove all reactions if any were present before
528
- :param c: Contact the reaction refers to
708
+ :param text: Text to encode as a QR code
529
709
  """
530
- raise NotImplementedError
710
+ await self.xmpp.send_qr(text, mto=self.user.jid)
531
711
 
532
- async def retract(self, legacy_msg_id: Any, c: LegacyContactType):
533
- """
534
- Triggered when the user retracts (:xep:`0424`) a message.
712
+ def re_login(self):
713
+ # Logout then re-login
714
+ #
715
+ # No reason to override this
716
+ self.xmpp.re_login(self)
535
717
 
536
- :param legacy_msg_id: Legacy ID of the retracted message
537
- :param c: The contact this retraction refers to
538
- """
539
- raise NotImplementedError
718
+ async def get_contact_or_group_or_participant(self, jid: JID):
719
+ if jid.bare in (contacts := self.contacts.known_contacts(only_friends=False)):
720
+ return contacts[jid.bare]
721
+ if jid.bare in (mucs := self.bookmarks._mucs_by_bare_jid):
722
+ return await self.__get_muc_or_participant(mucs[jid.bare], jid)
723
+ else:
724
+ muc = None
540
725
 
726
+ try:
727
+ return await self.contacts.by_jid(jid)
728
+ except XMPPError:
729
+ if muc is None:
730
+ try:
731
+ muc = await self.bookmarks.by_jid(jid)
732
+ except XMPPError:
733
+ return
734
+ return await self.__get_muc_or_participant(muc, jid)
735
+
736
+ @staticmethod
737
+ async def __get_muc_or_participant(muc: LegacyMUC, jid: JID):
738
+ if nick := jid.resource:
739
+ try:
740
+ return await muc.get_participant(
741
+ nick, raise_if_not_found=True, fill_first=True
742
+ )
743
+ except XMPPError:
744
+ return None
745
+ return muc
746
+
747
+ async def wait_for_ready(self, timeout: Optional[Union[int, float]] = 10):
748
+ # """
749
+ # Wait until session, contacts and bookmarks are ready
750
+ #
751
+ # (slidge internal use)
752
+ #
753
+ # :param timeout:
754
+ # :return:
755
+ # """
756
+ try:
757
+ await asyncio.wait_for(asyncio.shield(self.ready), timeout)
758
+ await asyncio.wait_for(asyncio.shield(self.contacts.ready), timeout)
759
+ await asyncio.wait_for(asyncio.shield(self.bookmarks.ready), timeout)
760
+ except asyncio.TimeoutError:
761
+ raise XMPPError(
762
+ "recipient-unavailable",
763
+ "Legacy session is not fully initialized, retry later",
764
+ )
541
765
 
542
- SessionType = TypeVar("SessionType", bound=BaseSession)
543
766
 
544
767
  _sessions: dict[GatewayUser, BaseSession] = {}
545
768
  log = logging.getLogger(__name__)