slidge 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +111 -44
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -795
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.2.dist-info/RECORD +0 -96
  100. slidge-0.1.2.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/WHEEL +0 -0
@@ -8,7 +8,7 @@ import re
8
8
  import tempfile
9
9
  from copy import copy
10
10
  from datetime import datetime
11
- from typing import TYPE_CHECKING, Callable, Collection, Optional, Sequence, Union
11
+ from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Sequence, Union
12
12
 
13
13
  import aiohttp
14
14
  import qrcode
@@ -18,40 +18,30 @@ from slixmpp.plugins.xep_0060.stanza import OwnerAffiliation
18
18
  from slixmpp.types import MessageTypes
19
19
  from slixmpp.xmlstream.xmlstream import NotConnectedError
20
20
 
21
- from ... import command # noqa: F401
22
- from ...command.adhoc import AdhocProvider
23
- from ...command.admin import Exec
24
- from ...command.base import Command, FormField
25
- from ...command.chat_command import ChatCommandProvider
26
- from ...command.register import RegistrationType
27
- from ...slixfix.roster import RosterBackend
28
- from ...slixfix.xep_0292.vcard4 import VCard4Provider
29
- from ...util import ABCSubclassableOnceAtMost
30
- from ...util.db import GatewayUser, user_store
31
- from ...util.sql import db
32
- from ...util.types import AvatarType, MessageOrPresenceTypeVar
33
- from .. import config
34
- from ..mixins import MessageMixin
35
- from ..pubsub import PubSubComponent
36
- from ..session import BaseSession
37
- from .caps import Caps
38
- from .delivery_receipt import DeliveryReceipt
39
- from .disco import Disco
40
- from .mam import Mam
41
- from .muc_admin import MucAdmin
42
- from .ping import Ping
43
- from .presence import PresenceHandlerMixin
44
- from .registration import Registration
45
- from .search import Search
46
- from .session_dispatcher import SessionDispatcher
47
- from .vcard_temp import VCardTemp
21
+ from slidge import command # noqa: F401
22
+ from slidge.command.adhoc import AdhocProvider
23
+ from slidge.command.admin import Exec
24
+ from slidge.command.base import Command, FormField
25
+ from slidge.command.chat_command import ChatCommandProvider
26
+ from slidge.command.register import RegistrationType
27
+ from slidge.core import config
28
+ from slidge.core.dispatcher.session_dispatcher import SessionDispatcher
29
+ from slidge.core.mixins import MessageMixin
30
+ from slidge.core.pubsub import PubSubComponent
31
+ from slidge.core.session import BaseSession
32
+ from slidge.db import GatewayUser, SlidgeStore
33
+ from slidge.db.avatar import avatar_cache
34
+ from slidge.slixfix.delivery_receipt import DeliveryReceipt
35
+ from slidge.slixfix.roster import RosterBackend
36
+ from slidge.util import ABCSubclassableOnceAtMost
37
+ from slidge.util.types import AvatarType, MessageOrPresenceTypeVar
38
+ from slidge.util.util import timeit
48
39
 
49
40
  if TYPE_CHECKING:
50
- from ...group.room import LegacyMUC
41
+ pass
51
42
 
52
43
 
53
44
  class BaseGateway(
54
- PresenceHandlerMixin,
55
45
  ComponentXMPP,
56
46
  MessageMixin,
57
47
  metaclass=ABCSubclassableOnceAtMost,
@@ -109,7 +99,7 @@ class BaseGateway(
109
99
  Path, bytes or URL used by the component as an avatar.
110
100
  """
111
101
 
112
- REGISTRATION_FIELDS: Collection[FormField] = [
102
+ REGISTRATION_FIELDS: Sequence[FormField] = [
113
103
  FormField(var="username", label="User name", required=True),
114
104
  FormField(var="password", label="Password", required=True, private=True),
115
105
  ]
@@ -141,6 +131,23 @@ class BaseGateway(
141
131
  )
142
132
  REGISTRATION_QR_INSTRUCTIONS = "Flash this code or follow this link"
143
133
 
134
+ PREFERENCES = [
135
+ FormField(
136
+ var="sync_presence",
137
+ label="Propagate your XMPP presence to the legacy network.",
138
+ value="true",
139
+ required=True,
140
+ type="boolean",
141
+ ),
142
+ FormField(
143
+ var="sync_avatar",
144
+ label="Propagate your XMPP avatar to the legacy network.",
145
+ value="true",
146
+ required=True,
147
+ type="boolean",
148
+ ),
149
+ ]
150
+
144
151
  ROSTER_GROUP: str = "slidge"
145
152
  """
146
153
  Name of the group assigned to a :class:`.LegacyContact` automagically
@@ -202,8 +209,52 @@ class BaseGateway(
202
209
  mtype: MessageTypes = "chat"
203
210
  is_group = False
204
211
  _can_send_carbon = False
212
+ store: SlidgeStore
213
+ avatar_pk: int
214
+
215
+ AVATAR_ID_TYPE: Callable[[str], Any] = str
216
+ """
217
+ Modify this if the legacy network uses unique avatar IDs that are not strings.
218
+
219
+ This is required because we store those IDs as TEXT in the persistent SQL DB.
220
+ The callable specified here will receive is responsible for converting the
221
+ serialised-as-text version of the avatar unique ID back to the proper type.
222
+ Common example: ``int``.
223
+ """
224
+ # FIXME: do we really need this since we have session.xmpp_to_legacy_msg_id?
225
+ # (maybe we do)
226
+ LEGACY_MSG_ID_TYPE: Callable[[str], Any] = str
227
+ """
228
+ Modify this if the legacy network uses unique message IDs that are not strings.
229
+
230
+ This is required because we store those IDs as TEXT in the persistent SQL DB.
231
+ The callable specified here will receive is responsible for converting the
232
+ serialised-as-text version of the message unique ID back to the proper type.
233
+ Common example: ``int``.
234
+ """
235
+ LEGACY_CONTACT_ID_TYPE: Callable[[str], Any] = str
236
+ """
237
+ Modify this if the legacy network uses unique contact IDs that are not strings.
238
+
239
+ This is required because we store those IDs as TEXT in the persistent SQL DB.
240
+ The callable specified here is responsible for converting the
241
+ serialised-as-text version of the contact unique ID back to the proper type.
242
+ Common example: ``int``.
243
+ """
244
+ LEGACY_ROOM_ID_TYPE: Callable[[str], Any] = str
245
+ """
246
+ Modify this if the legacy network uses unique room IDs that are not strings.
247
+
248
+ This is required because we store those IDs as TEXT in the persistent SQL DB.
249
+ The callable specified here is responsible for converting the
250
+ serialised-as-text version of the room unique ID back to the proper type.
251
+ Common example: ``int``.
252
+ """
253
+
254
+ http: aiohttp.ClientSession
205
255
 
206
256
  def __init__(self):
257
+ self.log = log
207
258
  self.datetime_started = datetime.now()
208
259
  self.xmpp = self # ugly hack to work with the BaseSender mixin :/
209
260
  self.default_ns = "jabber:component:accept"
@@ -222,7 +273,6 @@ class BaseGateway(
222
273
  },
223
274
  "xep_0100": {
224
275
  "component_name": self.COMPONENT_NAME,
225
- "user_store": user_store,
226
276
  "type": self.COMPONENT_TYPE,
227
277
  },
228
278
  "xep_0184": {
@@ -236,16 +286,20 @@ class BaseGateway(
236
286
  fix_error_ns=True,
237
287
  )
238
288
  self.loop.set_exception_handler(self.__exception_handler)
239
- self.http: aiohttp.ClientSession = aiohttp.ClientSession()
289
+ self.loop.create_task(self.__set_http())
240
290
  self.has_crashed: bool = False
241
291
  self.use_origin_id = False
242
292
 
243
293
  self.jid_validator: re.Pattern = re.compile(config.USER_JID_VALIDATOR)
244
- self.qr_pending_registrations = dict[str, asyncio.Future[bool]]()
294
+ self.qr_pending_registrations = dict[str, asyncio.Future[Optional[dict]]]()
245
295
 
246
296
  self.session_cls: BaseSession = BaseSession.get_unique_subclass()
247
297
  self.session_cls.xmpp = self
248
298
 
299
+ from ..group.room import LegacyMUC
300
+
301
+ LegacyMUC.get_self_or_unique_subclass().xmpp = self
302
+
249
303
  self.get_session_from_stanza: Callable[
250
304
  [Union[Message, Presence, Iq]], BaseSession
251
305
  ] = self.session_cls.from_stanza # type: ignore
@@ -255,16 +309,18 @@ class BaseGateway(
255
309
 
256
310
  self.register_plugins()
257
311
  self.__register_slixmpp_events()
258
- self.roster.set_backend(RosterBackend)
312
+ self.__register_slixmpp_api()
313
+ self.roster.set_backend(RosterBackend(self))
259
314
 
260
315
  self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME})
261
316
  self.pubsub: PubSubComponent = self["pubsub"]
262
- self.vcard: VCard4Provider = self["xep_0292_provider"]
263
317
  self.delivery_receipt: DeliveryReceipt = DeliveryReceipt(self)
264
318
 
265
319
  # with this we receive user avatar updates
266
320
  self.plugin["xep_0030"].add_feature("urn:xmpp:avatar:metadata+notify")
267
321
 
322
+ self.plugin["xep_0030"].add_feature("urn:xmpp:chat-markers:0")
323
+
268
324
  if self.GROUPS:
269
325
  self.plugin["xep_0030"].add_feature("http://jabber.org/protocol/muc")
270
326
  self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2")
@@ -281,20 +337,18 @@ class BaseGateway(
281
337
  # why does mypy need these type annotations? no idea
282
338
  self.__adhoc_handler: AdhocProvider = AdhocProvider(self)
283
339
  self.__chat_commands_handler: ChatCommandProvider = ChatCommandProvider(self)
284
- self.__disco_handler = Disco(self)
285
-
286
- self.__ping_handler = Ping(self)
287
- self.__mam_handler = Mam(self)
288
- self.__search_handler = Search(self)
289
- self.__caps_handler = Caps(self)
290
- self.__vcard_temp_handler = VCardTemp(self)
291
- self.__muc_admin_handler = MucAdmin(self)
292
- self.__registration = Registration(self)
340
+
293
341
  self.__dispatcher = SessionDispatcher(self)
294
342
 
295
343
  self.__register_commands()
296
344
 
297
- db.mam_launch_cleanup_task(self.loop)
345
+ MessageMixin.__init__(self) # ComponentXMPP does not call super().__init__()
346
+
347
+ async def __set_http(self):
348
+ self.http = aiohttp.ClientSession()
349
+ if getattr(self, "_test_mode", False):
350
+ return
351
+ avatar_cache.http = self.http
298
352
 
299
353
  def __register_commands(self):
300
354
  for cls in Command.subclasses:
@@ -303,7 +357,7 @@ class BaseGateway(
303
357
  continue
304
358
  if cls is Exec:
305
359
  if config.DEV_MODE:
306
- log.warning("/!\ DEV MODE ENABLED /!\\")
360
+ log.warning(r"/!\ DEV MODE ENABLED /!\\")
307
361
  else:
308
362
  continue
309
363
  c = cls(self)
@@ -322,7 +376,7 @@ class BaseGateway(
322
376
  log.debug("Context in the exception handler: %s", context)
323
377
  exc = context.get("exception")
324
378
  if exc is None:
325
- log.warning("No exception in this context: %s", context)
379
+ log.debug("No exception in this context: %s", context)
326
380
  elif isinstance(exc, SystemExit):
327
381
  log.debug("SystemExit called in an asyncio task")
328
382
  else:
@@ -332,40 +386,27 @@ class BaseGateway(
332
386
  loop.stop()
333
387
 
334
388
  def __register_slixmpp_events(self):
389
+ self.del_event_handler("presence_subscribe", self._handle_subscribe)
390
+ self.del_event_handler("presence_unsubscribe", self._handle_unsubscribe)
391
+ self.del_event_handler("presence_subscribed", self._handle_subscribed)
392
+ self.del_event_handler("presence_unsubscribed", self._handle_unsubscribed)
393
+ self.del_event_handler(
394
+ "roster_subscription_request", self._handle_new_subscription
395
+ )
396
+ self.del_event_handler("presence_probe", self._handle_probe)
335
397
  self.add_event_handler("session_start", self.__on_session_start)
336
398
  self.add_event_handler("disconnected", self.connect)
337
- self.add_event_handler("user_register", self._on_user_register)
338
- self.add_event_handler("user_unregister", self._on_user_unregister)
339
- self.add_event_handler("groupchat_message_error", self.__on_group_chat_error)
399
+
400
+ def __register_slixmpp_api(self) -> None:
401
+ self.plugin["xep_0231"].api.register(self.store.bob.get_bob, "get_bob")
402
+ self.plugin["xep_0231"].api.register(self.store.bob.set_bob, "set_bob")
403
+ self.plugin["xep_0231"].api.register(self.store.bob.del_bob, "del_bob")
340
404
 
341
405
  @property # type: ignore
342
406
  def jid(self):
343
407
  # Override to avoid slixmpp deprecation warnings.
344
408
  return self.boundjid
345
409
 
346
- async def __on_group_chat_error(self, msg: Message):
347
- condition = msg["error"].get_condition()
348
- if condition not in KICKABLE_ERRORS:
349
- return
350
-
351
- try:
352
- muc = await self.get_muc_from_stanza(msg)
353
- except XMPPError as e:
354
- log.debug("Not removing resource", exc_info=e)
355
- return
356
- mfrom = msg.get_from()
357
- resource = mfrom.resource
358
- try:
359
- muc.user_resources.remove(resource)
360
- except KeyError:
361
- # this actually happens quite frequently on for both beagle and monal
362
- # (not sure why?), but is of no consequence
363
- log.debug("%s was not in the resources of %s", resource, muc)
364
- else:
365
- log.info(
366
- "Removed %s from the resources of %s because of error", resource, muc
367
- )
368
-
369
410
  async def __on_session_start(self, event):
370
411
  log.debug("Gateway session start: %s", event)
371
412
 
@@ -374,11 +415,13 @@ class BaseGateway(
374
415
  await disco.del_feature(feature="urn:xmpp:http:upload:0", jid=self.boundjid)
375
416
  await self.plugin["xep_0115"].update_caps(jid=self.boundjid)
376
417
 
377
- await self.pubsub.set_avatar(
378
- jid=self.boundjid.bare, avatar=self.COMPONENT_AVATAR
379
- )
418
+ if self.COMPONENT_AVATAR is not None:
419
+ cached_avatar = await avatar_cache.convert_or_get(self.COMPONENT_AVATAR)
420
+ self.avatar_pk = cached_avatar.pk
421
+ else:
422
+ cached_avatar = None
380
423
 
381
- for user in user_store.get_all():
424
+ for user in self.store.users.get_all():
382
425
  # TODO: before this, we should check if the user has removed us from their roster
383
426
  # while we were offline and trigger unregister from there. Presence probe does not seem
384
427
  # to work in this case, there must be another way. privileged entity could be used
@@ -396,10 +439,14 @@ class BaseGateway(
396
439
  )
397
440
  continue
398
441
  self.send_presence(
399
- pto=user.bare_jid, ptype="probe"
442
+ pto=user.jid.bare, ptype="probe"
400
443
  ) # ensure we get all resources for user
401
444
  session = self.session_cls.from_user(user)
402
- session.create_task(self.__login_wrap(session))
445
+ session.create_task(self.login_wrap(session))
446
+ if cached_avatar is not None:
447
+ await self.pubsub.broadcast_avatar(
448
+ self.boundjid.bare, session.user_jid, cached_avatar
449
+ )
403
450
 
404
451
  log.info("Slidge has successfully started")
405
452
 
@@ -455,12 +502,13 @@ class BaseGateway(
455
502
  exc_info=e,
456
503
  )
457
504
 
458
- async def __login_wrap(self, session: "BaseSession"):
505
+ @timeit
506
+ async def login_wrap(self, session: "BaseSession"):
459
507
  session.send_gateway_status("Logging in…", show="dnd")
460
508
  try:
461
509
  status = await session.login()
462
510
  except Exception as e:
463
- log.warning("Login problem for %s", session.user, exc_info=e)
511
+ log.warning("Login problem for %s", session.user_jid, exc_info=e)
464
512
  log.exception(e)
465
513
  session.send_gateway_status(f"Could not login: {e}", show="busy")
466
514
  session.send_gateway_message(
@@ -469,10 +517,10 @@ class BaseGateway(
469
517
  )
470
518
  return
471
519
 
472
- log.info("Login success for %s", session.user)
520
+ log.info("Login success for %s", session.user_jid)
473
521
  session.logged = True
474
522
  session.send_gateway_status("Syncing contacts…", show="dnd")
475
- await session.contacts.fill()
523
+ await session.contacts._fill()
476
524
  if not (r := session.contacts.ready).done():
477
525
  r.set_result(True)
478
526
  if self.GROUPS:
@@ -483,24 +531,25 @@ class BaseGateway(
483
531
  for c in session.contacts:
484
532
  # we need to receive presences directed at the contacts, in
485
533
  # order to send pubsub events for their +notify features
486
- self.send_presence(pfrom=c.jid, pto=session.user.bare_jid, ptype="probe")
534
+ self.send_presence(pfrom=c.jid, pto=session.user_jid.bare, ptype="probe")
487
535
  if status is None:
488
536
  session.send_gateway_status("Logged in", show="chat")
489
537
  else:
490
538
  session.send_gateway_status(status, show="chat")
491
- # If we stored users avatars (or their hash) persistently across slidge
492
- # restarts, we would not need to fetch it on startup
493
- session.create_task(self.__fetch_user_avatar(session))
539
+ if session.user.preferences.get("sync_avatar", False):
540
+ session.create_task(self.fetch_user_avatar(session))
541
+ else:
542
+ self.xmpp.store.users.set_avatar_hash(session.user_pk, None)
494
543
 
495
- async def __fetch_user_avatar(self, session: BaseSession):
544
+ async def fetch_user_avatar(self, session: BaseSession):
496
545
  try:
497
546
  iq = await self.xmpp.plugin["xep_0060"].get_items(
498
- session.user.bare_jid,
547
+ session.user_jid.bare,
499
548
  self.xmpp.plugin["xep_0084"].stanza.MetaData.namespace,
500
549
  ifrom=self.boundjid.bare,
501
550
  )
502
- except IqError as e:
503
- session.log.debug("Failed to retrieve avatar: %r", e)
551
+ except IqError:
552
+ self.xmpp.store.users.set_avatar_hash(session.user_pk, None)
504
553
  return
505
554
  await self.__dispatcher.on_avatar_metadata_info(
506
555
  session, iq["pubsub"]["items"]["item"]["avatar_metadata"]["info"]
@@ -515,21 +564,6 @@ class BaseGateway(
515
564
  stanza.send()
516
565
  return stanza
517
566
 
518
- async def _on_user_register(self, iq: Iq):
519
- session = self.get_session_from_stanza(iq)
520
- for jid in config.ADMINS:
521
- self.send_message(
522
- mto=jid,
523
- mbody=f"{iq.get_from()} has registered",
524
- mtype="chat",
525
- mfrom=self.boundjid.bare,
526
- )
527
- session.send_gateway_message(self.WELCOME_MESSAGE)
528
- await self.__login_wrap(session)
529
-
530
- async def _on_user_unregister(self, iq: Iq):
531
- await self.session_cls.kill_by_jid(iq.get_from())
532
-
533
567
  def raise_if_not_allowed_jid(self, jid: JID):
534
568
  if not self.jid_validator.match(jid.bare):
535
569
  raise XMPPError(
@@ -568,26 +602,6 @@ class BaseGateway(
568
602
  except XMPPError:
569
603
  pass
570
604
 
571
- async def get_muc_from_stanza(self, iq: Union[Iq, Message]) -> "LegacyMUC":
572
- ito = iq.get_to()
573
-
574
- if ito == self.boundjid.bare:
575
- raise XMPPError(
576
- text="No MAM on the component itself, use a JID with a resource"
577
- )
578
-
579
- ifrom = iq.get_from()
580
- user = user_store.get_by_jid(ifrom)
581
- if user is None:
582
- raise XMPPError("registration-required")
583
-
584
- session = self.get_session_from_user(user)
585
- session.raise_if_not_logged()
586
-
587
- muc = await session.bookmarks.by_jid(ito)
588
-
589
- return muc
590
-
591
605
  def exception(self, exception: Exception):
592
606
  # """
593
607
  # Called when a task created by slixmpp's internal (eg, on slix events) raises an Exception.
@@ -622,14 +636,14 @@ class BaseGateway(
622
636
  async def w():
623
637
  session.cancel_all_tasks()
624
638
  await session.logout()
625
- await self.__login_wrap(session)
639
+ await self.login_wrap(session)
626
640
 
627
641
  session.create_task(w())
628
642
 
629
643
  async def make_registration_form(self, _jid, _node, _ifrom, iq: Iq):
630
644
  self.raise_if_not_allowed_jid(iq.get_from())
631
645
  reg = iq["register"]
632
- user = user_store.get_by_stanza(iq)
646
+ user = self.store.users.get_by_stanza(iq)
633
647
  log.debug("User found: %s", user)
634
648
 
635
649
  form = reg["form"]
@@ -675,18 +689,20 @@ class BaseGateway(
675
689
  reply.set_payload(reg)
676
690
  return reply
677
691
 
678
- async def user_prevalidate(self, ifrom: JID, form_dict: dict[str, Optional[str]]):
692
+ async def user_prevalidate(
693
+ self, ifrom: JID, form_dict: dict[str, Optional[str]]
694
+ ) -> Optional[Mapping]:
679
695
  # Pre validate a registration form using the content of self.REGISTRATION_FIELDS
680
696
  # before passing it to the plugin custom validation logic.
681
697
  for field in self.REGISTRATION_FIELDS:
682
698
  if field.required and not form_dict.get(field.var):
683
699
  raise ValueError(f"Missing field: '{field.label}'")
684
700
 
685
- await self.validate(ifrom, form_dict)
701
+ return await self.validate(ifrom, form_dict)
686
702
 
687
703
  async def validate(
688
704
  self, user_jid: JID, registration_form: dict[str, Optional[str]]
689
- ):
705
+ ) -> Optional[Mapping]:
690
706
  """
691
707
  Validate a user's initial registration form.
692
708
 
@@ -706,11 +722,19 @@ class BaseGateway(
706
722
 
707
723
  :param user_jid: JID of the user that has just registered
708
724
  :param registration_form: A dict where keys are the :attr:`.FormField.var` attributes
709
- of the :attr:`.BaseGateway.REGISTRATION_FIELDS` iterable
725
+ of the :attr:`.BaseGateway.REGISTRATION_FIELDS` iterable.
726
+ This dict can be modified and will be accessible as the ``legacy_module_data``
727
+ of the
728
+
729
+ :return : A dict that will be stored as the persistent "legacy_module_data"
730
+ for this user. If you don't return anything here, the whole registration_form
731
+ content will be stored.
710
732
  """
711
733
  raise NotImplementedError
712
734
 
713
- async def validate_two_factor_code(self, user: GatewayUser, code: str):
735
+ async def validate_two_factor_code(
736
+ self, user: GatewayUser, code: str
737
+ ) -> Optional[dict]:
714
738
  """
715
739
  Called when the user enters their 2FA code.
716
740
 
@@ -725,6 +749,9 @@ class BaseGateway(
725
749
  :attr:`.registration_form` attributes to get what you need.
726
750
  :param code: The code they entered, either via "chatbot" message or
727
751
  adhoc command
752
+
753
+ :return : A dict which keys and values will be added to the persistent "legacy_module_data"
754
+ for this user.
728
755
  """
729
756
  raise NotImplementedError
730
757
 
@@ -744,7 +771,10 @@ class BaseGateway(
744
771
  raise NotImplementedError
745
772
 
746
773
  async def confirm_qr(
747
- self, user_bare_jid: str, exception: Optional[Exception] = None
774
+ self,
775
+ user_bare_jid: str,
776
+ exception: Optional[Exception] = None,
777
+ legacy_data: Optional[dict] = None,
748
778
  ):
749
779
  """
750
780
  This method is meant to be called to finalize QR code-based registration
@@ -757,10 +787,12 @@ class BaseGateway(
757
787
  :class:`GatewayUser` instance
758
788
  :param exception: Optionally, an XMPPError to be raised to **not** confirm
759
789
  QR code flashing.
790
+ :param legacy_data: dict which keys and values will be added to the persistent
791
+ "legacy_module_data" for this user.
760
792
  """
761
793
  fut = self.qr_pending_registrations[user_bare_jid]
762
794
  if exception is None:
763
- fut.set_result(True)
795
+ fut.set_result(legacy_data)
764
796
  else:
765
797
  fut.set_exception(exception)
766
798
 
@@ -771,14 +803,17 @@ class BaseGateway(
771
803
  async def unregister(self, user: GatewayUser):
772
804
  """
773
805
  Optionally override this if you need to clean additional
774
- stuff after a user has been removed from the permanent user_store.
806
+ stuff after a user has been removed from the persistent user store.
775
807
 
776
808
  By default, this just calls :meth:`BaseSession.logout`.
777
809
 
778
810
  :param user:
779
811
  """
780
812
  session = self.get_session_from_user(user)
781
- await session.logout()
813
+ try:
814
+ await session.logout()
815
+ except NotImplementedError:
816
+ pass
782
817
 
783
818
  async def input(
784
819
  self, jid: JID, text=None, mtype: MessageTypes = "chat", **msg_kwargs
@@ -825,26 +860,12 @@ class BaseGateway(
825
860
  # """
826
861
  log.debug("Shutting down")
827
862
  tasks = []
828
- for user in user_store.get_all():
863
+ for user in self.store.users.get_all():
829
864
  tasks.append(self.session_cls.from_jid(user.jid).shutdown())
830
865
  self.send_presence(ptype="unavailable", pto=user.jid)
831
866
  return tasks
832
867
 
833
868
 
834
- KICKABLE_ERRORS = {
835
- "gone",
836
- "internal-server-error",
837
- "item-not-found",
838
- "jid-malformed",
839
- "recipient-unavailable",
840
- "redirect",
841
- "remote-server-not-found",
842
- "remote-server-timeout",
843
- "service-unavailable",
844
- "malformed error",
845
- }
846
-
847
-
848
869
  SLIXMPP_PLUGINS = [
849
870
  "link_preview", # https://wiki.soprani.ca/CheogramApp/LinkPreviews
850
871
  "xep_0030", # Service discovery
@@ -854,6 +875,7 @@ SLIXMPP_PLUGINS = [
854
875
  "xep_0055", # Jabber search
855
876
  "xep_0059", # Result Set Management
856
877
  "xep_0066", # Out of Band Data
878
+ "xep_0071", # XHTML-IM (for stickers and custom emojis maybe later)
857
879
  "xep_0077", # In-band registration
858
880
  "xep_0084", # User Avatar
859
881
  "xep_0085", # Chat state notifications
@@ -866,6 +888,7 @@ SLIXMPP_PLUGINS = [
866
888
  "xep_0184", # Message Delivery Receipts
867
889
  "xep_0199", # XMPP Ping
868
890
  "xep_0221", # Data Forms Media Element
891
+ "xep_0231", # Bits of Binary (for stickers and custom emojis maybe later)
869
892
  "xep_0249", # Direct MUC Invitations
870
893
  "xep_0264", # Jingle Content Thumbnails
871
894
  "xep_0280", # Carbons
@@ -2,6 +2,8 @@
2
2
  Mixins
3
3
  """
4
4
 
5
+ from typing import Optional
6
+
5
7
  from .avatar import AvatarMixin
6
8
  from .disco import ChatterDiscoMixin
7
9
  from .message import MessageCarbonMixin, MessageMixin
@@ -16,4 +18,12 @@ class FullCarbonMixin(ChatterDiscoMixin, MessageCarbonMixin, PresenceMixin):
16
18
  pass
17
19
 
18
20
 
19
- __all__ = ("AvatarMixin",)
21
+ class StoredAttributeMixin:
22
+ def serialize_extra_attributes(self) -> Optional[dict]:
23
+ return None
24
+
25
+ def deserialize_extra_attributes(self, data: dict) -> None:
26
+ pass
27
+
28
+
29
+ __all__ = ("AvatarMixin", "FullCarbonMixin", "StoredAttributeMixin")