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