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.
Files changed (96) hide show
  1. slidge/__init__.py +61 -0
  2. slidge/__main__.py +192 -0
  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 +3 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +209 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +892 -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 +757 -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 +525 -0
  41. slidge/core/session.py +752 -0
  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 +440 -0
  46. slidge/group/room.py +1095 -0
  47. slidge/migration.py +18 -0
  48. slidge/py.typed +0 -0
  49. slidge/slixfix/__init__.py +68 -0
  50. slidge/slixfix/link_preview/__init__.py +10 -0
  51. slidge/slixfix/link_preview/link_preview.py +17 -0
  52. slidge/slixfix/link_preview/stanza.py +99 -0
  53. slidge/slixfix/roster.py +60 -0
  54. slidge/slixfix/xep_0077/__init__.py +10 -0
  55. slidge/slixfix/xep_0077/register.py +289 -0
  56. slidge/slixfix/xep_0077/stanza.py +104 -0
  57. slidge/slixfix/xep_0100/__init__.py +5 -0
  58. slidge/slixfix/xep_0100/gateway.py +121 -0
  59. slidge/slixfix/xep_0100/stanza.py +9 -0
  60. slidge/slixfix/xep_0153/__init__.py +10 -0
  61. slidge/slixfix/xep_0153/stanza.py +25 -0
  62. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  63. slidge/slixfix/xep_0264/__init__.py +5 -0
  64. slidge/slixfix/xep_0264/stanza.py +36 -0
  65. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  66. slidge/slixfix/xep_0292/__init__.py +5 -0
  67. slidge/slixfix/xep_0292/vcard4.py +100 -0
  68. slidge/slixfix/xep_0313/__init__.py +12 -0
  69. slidge/slixfix/xep_0313/mam.py +262 -0
  70. slidge/slixfix/xep_0313/stanza.py +359 -0
  71. slidge/slixfix/xep_0317/__init__.py +5 -0
  72. slidge/slixfix/xep_0317/hats.py +17 -0
  73. slidge/slixfix/xep_0317/stanza.py +28 -0
  74. slidge/slixfix/xep_0356_old/__init__.py +7 -0
  75. slidge/slixfix/xep_0356_old/privilege.py +167 -0
  76. slidge/slixfix/xep_0356_old/stanza.py +44 -0
  77. slidge/slixfix/xep_0424/__init__.py +9 -0
  78. slidge/slixfix/xep_0424/retraction.py +77 -0
  79. slidge/slixfix/xep_0424/stanza.py +28 -0
  80. slidge/slixfix/xep_0490/__init__.py +8 -0
  81. slidge/slixfix/xep_0490/mds.py +47 -0
  82. slidge/slixfix/xep_0490/stanza.py +17 -0
  83. slidge/util/__init__.py +15 -0
  84. slidge/util/archive_msg.py +61 -0
  85. slidge/util/conf.py +206 -0
  86. slidge/util/db.py +229 -0
  87. slidge/util/schema.sql +126 -0
  88. slidge/util/sql.py +508 -0
  89. slidge/util/test.py +295 -0
  90. slidge/util/types.py +180 -0
  91. slidge/util/util.py +295 -0
  92. slidge-0.1.0.dist-info/LICENSE +661 -0
  93. slidge-0.1.0.dist-info/METADATA +109 -0
  94. slidge-0.1.0.dist-info/RECORD +96 -0
  95. slidge-0.1.0.dist-info/WHEEL +4 -0
  96. 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__)