slidge 0.1.3__py3-none-any.whl → 0.2.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -196
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +8 -1
  5. slidge/command/admin.py +6 -7
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -6
  9. slidge/contact/contact.py +165 -49
  10. slidge/contact/roster.py +122 -47
  11. slidge/core/config.py +14 -11
  12. slidge/core/gateway/base.py +148 -36
  13. slidge/core/gateway/caps.py +7 -5
  14. slidge/core/gateway/disco.py +2 -4
  15. slidge/core/gateway/mam.py +1 -4
  16. slidge/core/gateway/muc_admin.py +1 -1
  17. slidge/core/gateway/ping.py +2 -3
  18. slidge/core/gateway/presence.py +1 -1
  19. slidge/core/gateway/registration.py +32 -21
  20. slidge/core/gateway/search.py +3 -5
  21. slidge/core/gateway/session_dispatcher.py +120 -57
  22. slidge/core/gateway/vcard_temp.py +7 -5
  23. slidge/core/mixins/__init__.py +11 -1
  24. slidge/core/mixins/attachment.py +32 -14
  25. slidge/core/mixins/avatar.py +90 -25
  26. slidge/core/mixins/base.py +8 -2
  27. slidge/core/mixins/db.py +18 -0
  28. slidge/core/mixins/disco.py +0 -10
  29. slidge/core/mixins/message.py +18 -8
  30. slidge/core/mixins/message_maker.py +17 -9
  31. slidge/core/mixins/presence.py +17 -4
  32. slidge/core/pubsub.py +54 -220
  33. slidge/core/session.py +69 -34
  34. slidge/db/__init__.py +4 -0
  35. slidge/db/alembic/env.py +64 -0
  36. slidge/db/alembic/script.py.mako +26 -0
  37. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  38. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  39. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  40. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  41. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  42. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  43. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +85 -0
  44. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  45. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
  46. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  47. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  48. slidge/db/avatar.py +235 -0
  49. slidge/db/meta.py +65 -0
  50. slidge/db/models.py +375 -0
  51. slidge/db/store.py +1078 -0
  52. slidge/group/archive.py +58 -14
  53. slidge/group/bookmarks.py +72 -57
  54. slidge/group/participant.py +87 -28
  55. slidge/group/room.py +369 -211
  56. slidge/main.py +201 -0
  57. slidge/migration.py +30 -0
  58. slidge/slixfix/__init__.py +35 -2
  59. slidge/slixfix/roster.py +11 -4
  60. slidge/slixfix/xep_0292/vcard4.py +3 -0
  61. slidge/util/archive_msg.py +2 -1
  62. slidge/util/db.py +1 -47
  63. slidge/util/test.py +71 -4
  64. slidge/util/types.py +29 -4
  65. slidge/util/util.py +22 -0
  66. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/METADATA +4 -4
  67. slidge-0.2.0a1.dist-info/RECORD +114 -0
  68. slidge/core/cache.py +0 -183
  69. slidge/util/schema.sql +0 -126
  70. slidge/util/sql.py +0 -508
  71. slidge-0.1.3.dist-info/RECORD +0 -96
  72. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/LICENSE +0 -0
  73. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/WHEEL +0 -0
  74. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/entry_points.txt +0 -0
@@ -8,7 +8,16 @@ 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 (
12
+ TYPE_CHECKING,
13
+ Any,
14
+ Callable,
15
+ Collection,
16
+ Mapping,
17
+ Optional,
18
+ Sequence,
19
+ Union,
20
+ )
12
21
 
13
22
  import aiohttp
14
23
  import qrcode
@@ -24,12 +33,13 @@ from ...command.admin import Exec
24
33
  from ...command.base import Command, FormField
25
34
  from ...command.chat_command import ChatCommandProvider
26
35
  from ...command.register import RegistrationType
36
+ from ...db import GatewayUser, SlidgeStore
37
+ from ...db.avatar import avatar_cache
27
38
  from ...slixfix.roster import RosterBackend
28
39
  from ...slixfix.xep_0292.vcard4 import VCard4Provider
29
40
  from ...util import ABCSubclassableOnceAtMost
30
- from ...util.db import GatewayUser, user_store
31
- from ...util.sql import db
32
41
  from ...util.types import AvatarType, MessageOrPresenceTypeVar
42
+ from ...util.util import timeit
33
43
  from .. import config
34
44
  from ..mixins import MessageMixin
35
45
  from ..pubsub import PubSubComponent
@@ -141,6 +151,23 @@ class BaseGateway(
141
151
  )
142
152
  REGISTRATION_QR_INSTRUCTIONS = "Flash this code or follow this link"
143
153
 
154
+ PREFERENCES = [
155
+ FormField(
156
+ var="sync_presence",
157
+ label="Propagate your XMPP presence to the legacy network.",
158
+ value="true",
159
+ required=True,
160
+ type="boolean",
161
+ ),
162
+ FormField(
163
+ var="sync_avatar",
164
+ label="Propagate your XMPP avatar to the legacy network.",
165
+ value="true",
166
+ required=True,
167
+ type="boolean",
168
+ ),
169
+ ]
170
+
144
171
  ROSTER_GROUP: str = "slidge"
145
172
  """
146
173
  Name of the group assigned to a :class:`.LegacyContact` automagically
@@ -202,8 +229,50 @@ class BaseGateway(
202
229
  mtype: MessageTypes = "chat"
203
230
  is_group = False
204
231
  _can_send_carbon = False
232
+ store: SlidgeStore
233
+ avatar_pk: int
234
+
235
+ AVATAR_ID_TYPE: Callable[[str], Any] = str
236
+ """
237
+ Modify this if the legacy network uses unique avatar 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 will receive is responsible for converting the
241
+ serialised-as-text version of the avatar unique ID back to the proper type.
242
+ Common example: ``int``.
243
+ """
244
+ # FIXME: do we really need this since we have session.xmpp_to_legacy_msg_id?
245
+ # (maybe we do)
246
+ LEGACY_MSG_ID_TYPE: Callable[[str], Any] = str
247
+ """
248
+ Modify this if the legacy network uses unique message IDs that are not strings.
249
+
250
+ This is required because we store those IDs as TEXT in the persistent SQL DB.
251
+ The callable specified here will receive is responsible for converting the
252
+ serialised-as-text version of the message unique ID back to the proper type.
253
+ Common example: ``int``.
254
+ """
255
+ LEGACY_CONTACT_ID_TYPE: Callable[[str], Any] = str
256
+ """
257
+ Modify this if the legacy network uses unique contact IDs that are not strings.
258
+
259
+ This is required because we store those IDs as TEXT in the persistent SQL DB.
260
+ The callable specified here is responsible for converting the
261
+ serialised-as-text version of the contact unique ID back to the proper type.
262
+ Common example: ``int``.
263
+ """
264
+ LEGACY_ROOM_ID_TYPE: Callable[[str], Any] = str
265
+ """
266
+ Modify this if the legacy network uses unique room IDs that are not strings.
267
+
268
+ This is required because we store those IDs as TEXT in the persistent SQL DB.
269
+ The callable specified here is responsible for converting the
270
+ serialised-as-text version of the room unique ID back to the proper type.
271
+ Common example: ``int``.
272
+ """
205
273
 
206
274
  def __init__(self):
275
+ self.log = log
207
276
  self.datetime_started = datetime.now()
208
277
  self.xmpp = self # ugly hack to work with the BaseSender mixin :/
209
278
  self.default_ns = "jabber:component:accept"
@@ -222,7 +291,6 @@ class BaseGateway(
222
291
  },
223
292
  "xep_0100": {
224
293
  "component_name": self.COMPONENT_NAME,
225
- "user_store": user_store,
226
294
  "type": self.COMPONENT_TYPE,
227
295
  },
228
296
  "xep_0184": {
@@ -241,11 +309,15 @@ class BaseGateway(
241
309
  self.use_origin_id = False
242
310
 
243
311
  self.jid_validator: re.Pattern = re.compile(config.USER_JID_VALIDATOR)
244
- self.qr_pending_registrations = dict[str, asyncio.Future[bool]]()
312
+ self.qr_pending_registrations = dict[str, asyncio.Future[Optional[dict]]]()
245
313
 
246
314
  self.session_cls: BaseSession = BaseSession.get_unique_subclass()
247
315
  self.session_cls.xmpp = self
248
316
 
317
+ from ...group.room import LegacyMUC
318
+
319
+ LegacyMUC.get_unique_subclass().xmpp = self
320
+
249
321
  self.get_session_from_stanza: Callable[
250
322
  [Union[Message, Presence, Iq]], BaseSession
251
323
  ] = self.session_cls.from_stanza # type: ignore
@@ -255,7 +327,7 @@ class BaseGateway(
255
327
 
256
328
  self.register_plugins()
257
329
  self.__register_slixmpp_events()
258
- self.roster.set_backend(RosterBackend)
330
+ self.roster.set_backend(RosterBackend(self))
259
331
 
260
332
  self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME})
261
333
  self.pubsub: PubSubComponent = self["pubsub"]
@@ -265,6 +337,8 @@ class BaseGateway(
265
337
  # with this we receive user avatar updates
266
338
  self.plugin["xep_0030"].add_feature("urn:xmpp:avatar:metadata+notify")
267
339
 
340
+ self.plugin["xep_0030"].add_feature("urn:xmpp:chat-markers:0")
341
+
268
342
  if self.GROUPS:
269
343
  self.plugin["xep_0030"].add_feature("http://jabber.org/protocol/muc")
270
344
  self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2")
@@ -294,7 +368,16 @@ class BaseGateway(
294
368
 
295
369
  self.__register_commands()
296
370
 
297
- db.mam_launch_cleanup_task(self.loop)
371
+ self.__mam_cleanup_task = self.loop.create_task(self.__mam_cleanup())
372
+
373
+ MessageMixin.__init__(self) # ComponentXMPP does not call super().__init__()
374
+
375
+ async def __mam_cleanup(self):
376
+ if not config.MAM_MAX_DAYS:
377
+ return
378
+ while True:
379
+ await asyncio.sleep(3600 * 6)
380
+ self.store.mam.nuke_older_than(config.MAM_MAX_DAYS)
298
381
 
299
382
  def __register_commands(self):
300
383
  for cls in Command.subclasses:
@@ -303,7 +386,7 @@ class BaseGateway(
303
386
  continue
304
387
  if cls is Exec:
305
388
  if config.DEV_MODE:
306
- log.warning("/!\ DEV MODE ENABLED /!\\")
389
+ log.warning(r"/!\ DEV MODE ENABLED /!\\")
307
390
  else:
308
391
  continue
309
392
  c = cls(self)
@@ -322,7 +405,7 @@ class BaseGateway(
322
405
  log.debug("Context in the exception handler: %s", context)
323
406
  exc = context.get("exception")
324
407
  if exc is None:
325
- log.warning("No exception in this context: %s", context)
408
+ log.debug("No exception in this context: %s", context)
326
409
  elif isinstance(exc, SystemExit):
327
410
  log.debug("SystemExit called in an asyncio task")
328
411
  else:
@@ -356,7 +439,7 @@ class BaseGateway(
356
439
  mfrom = msg.get_from()
357
440
  resource = mfrom.resource
358
441
  try:
359
- muc.user_resources.remove(resource)
442
+ muc.remove_user_resource(resource)
360
443
  except KeyError:
361
444
  # this actually happens quite frequently on for both beagle and monal
362
445
  # (not sure why?), but is of no consequence
@@ -374,11 +457,15 @@ class BaseGateway(
374
457
  await disco.del_feature(feature="urn:xmpp:http:upload:0", jid=self.boundjid)
375
458
  await self.plugin["xep_0115"].update_caps(jid=self.boundjid)
376
459
 
377
- await self.pubsub.set_avatar(
378
- jid=self.boundjid.bare, avatar=self.COMPONENT_AVATAR
379
- )
460
+ if self.COMPONENT_AVATAR:
461
+ cached_avatar = await avatar_cache.convert_or_get(
462
+ self.COMPONENT_AVATAR, None
463
+ )
464
+ self.avatar_pk = cached_avatar.pk
465
+ else:
466
+ cached_avatar = None
380
467
 
381
- for user in user_store.get_all():
468
+ for user in self.store.users.get_all():
382
469
  # TODO: before this, we should check if the user has removed us from their roster
383
470
  # while we were offline and trigger unregister from there. Presence probe does not seem
384
471
  # to work in this case, there must be another way. privileged entity could be used
@@ -396,10 +483,14 @@ class BaseGateway(
396
483
  )
397
484
  continue
398
485
  self.send_presence(
399
- pto=user.bare_jid, ptype="probe"
486
+ pto=user.jid.bare, ptype="probe"
400
487
  ) # ensure we get all resources for user
401
488
  session = self.session_cls.from_user(user)
402
489
  session.create_task(self.__login_wrap(session))
490
+ if cached_avatar is not None:
491
+ await self.pubsub.broadcast_avatar(
492
+ self.boundjid.bare, session.user_jid, cached_avatar
493
+ )
403
494
 
404
495
  log.info("Slidge has successfully started")
405
496
 
@@ -455,12 +546,13 @@ class BaseGateway(
455
546
  exc_info=e,
456
547
  )
457
548
 
549
+ @timeit
458
550
  async def __login_wrap(self, session: "BaseSession"):
459
551
  session.send_gateway_status("Logging in…", show="dnd")
460
552
  try:
461
553
  status = await session.login()
462
554
  except Exception as e:
463
- log.warning("Login problem for %s", session.user, exc_info=e)
555
+ log.warning("Login problem for %s", session.user_jid, exc_info=e)
464
556
  log.exception(e)
465
557
  session.send_gateway_status(f"Could not login: {e}", show="busy")
466
558
  session.send_gateway_message(
@@ -469,10 +561,10 @@ class BaseGateway(
469
561
  )
470
562
  return
471
563
 
472
- log.info("Login success for %s", session.user)
564
+ log.info("Login success for %s", session.user_jid)
473
565
  session.logged = True
474
566
  session.send_gateway_status("Syncing contacts…", show="dnd")
475
- await session.contacts.fill()
567
+ await session.contacts._fill()
476
568
  if not (r := session.contacts.ready).done():
477
569
  r.set_result(True)
478
570
  if self.GROUPS:
@@ -483,19 +575,18 @@ class BaseGateway(
483
575
  for c in session.contacts:
484
576
  # we need to receive presences directed at the contacts, in
485
577
  # order to send pubsub events for their +notify features
486
- self.send_presence(pfrom=c.jid, pto=session.user.bare_jid, ptype="probe")
578
+ self.send_presence(pfrom=c.jid, pto=session.user_jid.bare, ptype="probe")
487
579
  if status is None:
488
580
  session.send_gateway_status("Logged in", show="chat")
489
581
  else:
490
582
  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))
583
+ if session.user.preferences.get("sync_avatar", False):
584
+ session.create_task(self.fetch_user_avatar(session))
494
585
 
495
- async def __fetch_user_avatar(self, session: BaseSession):
586
+ async def fetch_user_avatar(self, session: BaseSession):
496
587
  try:
497
588
  iq = await self.xmpp.plugin["xep_0060"].get_items(
498
- session.user.bare_jid,
589
+ session.user_jid.bare,
499
590
  self.xmpp.plugin["xep_0084"].stanza.MetaData.namespace,
500
591
  ifrom=self.boundjid.bare,
501
592
  )
@@ -577,7 +668,7 @@ class BaseGateway(
577
668
  )
578
669
 
579
670
  ifrom = iq.get_from()
580
- user = user_store.get_by_jid(ifrom)
671
+ user = self.store.users.get(ifrom)
581
672
  if user is None:
582
673
  raise XMPPError("registration-required")
583
674
 
@@ -629,7 +720,7 @@ class BaseGateway(
629
720
  async def make_registration_form(self, _jid, _node, _ifrom, iq: Iq):
630
721
  self.raise_if_not_allowed_jid(iq.get_from())
631
722
  reg = iq["register"]
632
- user = user_store.get_by_stanza(iq)
723
+ user = self.store.users.get_by_stanza(iq)
633
724
  log.debug("User found: %s", user)
634
725
 
635
726
  form = reg["form"]
@@ -675,18 +766,20 @@ class BaseGateway(
675
766
  reply.set_payload(reg)
676
767
  return reply
677
768
 
678
- async def user_prevalidate(self, ifrom: JID, form_dict: dict[str, Optional[str]]):
769
+ async def user_prevalidate(
770
+ self, ifrom: JID, form_dict: dict[str, Optional[str]]
771
+ ) -> Optional[Mapping]:
679
772
  # Pre validate a registration form using the content of self.REGISTRATION_FIELDS
680
773
  # before passing it to the plugin custom validation logic.
681
774
  for field in self.REGISTRATION_FIELDS:
682
775
  if field.required and not form_dict.get(field.var):
683
776
  raise ValueError(f"Missing field: '{field.label}'")
684
777
 
685
- await self.validate(ifrom, form_dict)
778
+ return await self.validate(ifrom, form_dict)
686
779
 
687
780
  async def validate(
688
781
  self, user_jid: JID, registration_form: dict[str, Optional[str]]
689
- ):
782
+ ) -> Optional[Mapping]:
690
783
  """
691
784
  Validate a user's initial registration form.
692
785
 
@@ -706,11 +799,19 @@ class BaseGateway(
706
799
 
707
800
  :param user_jid: JID of the user that has just registered
708
801
  :param registration_form: A dict where keys are the :attr:`.FormField.var` attributes
709
- of the :attr:`.BaseGateway.REGISTRATION_FIELDS` iterable
802
+ of the :attr:`.BaseGateway.REGISTRATION_FIELDS` iterable.
803
+ This dict can be modified and will be accessible as the ``legacy_module_data``
804
+ of the
805
+
806
+ :return : A dict that will be stored as the persistent "legacy_module_data"
807
+ for this user. If you don't return anything here, the whole registration_form
808
+ content will be stored.
710
809
  """
711
810
  raise NotImplementedError
712
811
 
713
- async def validate_two_factor_code(self, user: GatewayUser, code: str):
812
+ async def validate_two_factor_code(
813
+ self, user: GatewayUser, code: str
814
+ ) -> Optional[dict]:
714
815
  """
715
816
  Called when the user enters their 2FA code.
716
817
 
@@ -725,6 +826,9 @@ class BaseGateway(
725
826
  :attr:`.registration_form` attributes to get what you need.
726
827
  :param code: The code they entered, either via "chatbot" message or
727
828
  adhoc command
829
+
830
+ :return : A dict which keys and values will be added to the persistent "legacy_module_data"
831
+ for this user.
728
832
  """
729
833
  raise NotImplementedError
730
834
 
@@ -744,7 +848,10 @@ class BaseGateway(
744
848
  raise NotImplementedError
745
849
 
746
850
  async def confirm_qr(
747
- self, user_bare_jid: str, exception: Optional[Exception] = None
851
+ self,
852
+ user_bare_jid: str,
853
+ exception: Optional[Exception] = None,
854
+ legacy_data: Optional[dict] = None,
748
855
  ):
749
856
  """
750
857
  This method is meant to be called to finalize QR code-based registration
@@ -757,10 +864,12 @@ class BaseGateway(
757
864
  :class:`GatewayUser` instance
758
865
  :param exception: Optionally, an XMPPError to be raised to **not** confirm
759
866
  QR code flashing.
867
+ :param legacy_data: dict which keys and values will be added to the persistent
868
+ "legacy_module_data" for this user.
760
869
  """
761
870
  fut = self.qr_pending_registrations[user_bare_jid]
762
871
  if exception is None:
763
- fut.set_result(True)
872
+ fut.set_result(legacy_data)
764
873
  else:
765
874
  fut.set_exception(exception)
766
875
 
@@ -771,14 +880,17 @@ class BaseGateway(
771
880
  async def unregister(self, user: GatewayUser):
772
881
  """
773
882
  Optionally override this if you need to clean additional
774
- stuff after a user has been removed from the permanent user_store.
883
+ stuff after a user has been removed from the persistent user store.
775
884
 
776
885
  By default, this just calls :meth:`BaseSession.logout`.
777
886
 
778
887
  :param user:
779
888
  """
780
889
  session = self.get_session_from_user(user)
781
- await session.logout()
890
+ try:
891
+ await session.logout()
892
+ except NotImplementedError:
893
+ pass
782
894
 
783
895
  async def input(
784
896
  self, jid: JID, text=None, mtype: MessageTypes = "chat", **msg_kwargs
@@ -825,7 +937,7 @@ class BaseGateway(
825
937
  # """
826
938
  log.debug("Shutting down")
827
939
  tasks = []
828
- for user in user_store.get_all():
940
+ for user in self.store.users.get_all():
829
941
  tasks.append(self.session_cls.from_jid(user.jid).shutdown())
830
942
  self.send_presence(ptype="unavailable", pto=user.jid)
831
943
  return tasks
@@ -5,8 +5,6 @@ from slixmpp import Presence
5
5
  from slixmpp.exceptions import XMPPError
6
6
  from slixmpp.xmlstream import StanzaBase
7
7
 
8
- from ...contact import LegacyContact
9
-
10
8
  if TYPE_CHECKING:
11
9
  from .base import BaseGateway
12
10
 
@@ -25,6 +23,9 @@ class Caps:
25
23
  if not isinstance(stanza, Presence):
26
24
  return stanza
27
25
 
26
+ if stanza.get_plugin("caps", check=True):
27
+ return stanza
28
+
28
29
  if stanza["type"] not in ("available", "chat", "away", "dnd", "xa"):
29
30
  return stanza
30
31
 
@@ -44,10 +45,11 @@ class Caps:
44
45
 
45
46
  await session.ready
46
47
 
47
- entity = await session.get_contact_or_group_or_participant(pfrom)
48
- if not isinstance(entity, LegacyContact):
48
+ try:
49
+ contact = await session.contacts.by_jid(pfrom)
50
+ except XMPPError:
49
51
  return stanza
50
- ver = await entity.get_caps_ver(pfrom)
52
+ ver = await contact.get_caps_ver(pfrom)
51
53
  else:
52
54
  ver = await caps.get_verstring(pfrom)
53
55
 
@@ -5,8 +5,6 @@ from slixmpp.exceptions import XMPPError
5
5
  from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
6
6
  from slixmpp.types import OptJid
7
7
 
8
- from ...util.db import user_store
9
-
10
8
  if TYPE_CHECKING:
11
9
  from .base import BaseGateway
12
10
 
@@ -38,7 +36,7 @@ class Disco:
38
36
  if ifrom is None:
39
37
  raise XMPPError("subscription-required")
40
38
 
41
- user = user_store.get_by_jid(ifrom)
39
+ user = self.xmpp.store.users.get(ifrom)
42
40
  if user is None:
43
41
  raise XMPPError("registration-required")
44
42
  session = self.xmpp.get_session_from_user(user)
@@ -63,7 +61,7 @@ class Disco:
63
61
  if jid != self.xmpp.boundjid.bare:
64
62
  return DiscoItems()
65
63
 
66
- user = user_store.get_by_jid(ifrom)
64
+ user = self.xmpp.store.users.get(ifrom)
67
65
  if user is None:
68
66
  raise XMPPError("registration-required")
69
67
 
@@ -3,8 +3,6 @@ from typing import TYPE_CHECKING
3
3
  from slixmpp import CoroutineCallback, Iq, StanzaPath
4
4
  from slixmpp.exceptions import XMPPError
5
5
 
6
- from ...util.db import user_store
7
-
8
6
  if TYPE_CHECKING:
9
7
  from .base import BaseGateway
10
8
 
@@ -46,8 +44,7 @@ class Mam:
46
44
  text="No MAM on the component itself, use a JID with a resource"
47
45
  )
48
46
 
49
- ifrom = iq.get_from()
50
- user = user_store.get_by_jid(ifrom)
47
+ user = self.xmpp.store.users.get(iq.get_from())
51
48
  if user is None:
52
49
  raise XMPPError("registration-required")
53
50
 
@@ -28,7 +28,7 @@ class MucAdmin:
28
28
 
29
29
  reply = iq.reply()
30
30
  reply.enable("mucadmin_query")
31
- for participant in await muc.get_participants():
31
+ async for participant in muc.get_participants():
32
32
  if not participant.affiliation == affiliation:
33
33
  continue
34
34
  reply["mucadmin_query"].append(participant.mucadmin_item())
@@ -4,7 +4,6 @@ from slixmpp import CoroutineCallback, Iq, StanzaPath
4
4
  from slixmpp.exceptions import XMPPError
5
5
 
6
6
  from ...group import LegacyMUC
7
- from ...util.db import user_store
8
7
 
9
8
  if TYPE_CHECKING:
10
9
  from .base import BaseGateway
@@ -31,7 +30,7 @@ class Ping:
31
30
  iq.reply().send()
32
31
 
33
32
  ifrom = iq.get_from()
34
- user = user_store.get_by_jid(ifrom)
33
+ user = self.xmpp.store.users.get(ifrom)
35
34
  if user is None:
36
35
  raise XMPPError("registration-required")
37
36
 
@@ -60,7 +59,7 @@ class Ping:
60
59
 
61
60
  @staticmethod
62
61
  def __handle_muc_ping(muc: LegacyMUC, iq: Iq):
63
- if iq.get_from().resource in muc.user_resources:
62
+ if iq.get_from().resource in muc.get_user_resources():
64
63
  iq.reply().send()
65
64
  else:
66
65
  raise XMPPError("not-acceptable", etype="cancel", by=muc.jid)
@@ -47,7 +47,7 @@ class PresenceHandlerMixin:
47
47
  contact = await self.__get_contact(pres)
48
48
  except _IsDirectedAtComponent as e:
49
49
  e.session.send_gateway_message("Bye bye!")
50
- await e.session.kill_by_jid(e.session.user.jid)
50
+ await e.session.kill_by_jid(e.session.user_jid)
51
51
  return
52
52
 
53
53
  contact.is_friend = False
@@ -1,9 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  from typing import TYPE_CHECKING, Optional
3
5
 
4
6
  from slixmpp import JID, Iq
7
+ from slixmpp.exceptions import XMPPError
5
8
 
6
- from ...util.db import user_store
9
+ from ...db import GatewayUser
7
10
 
8
11
  if TYPE_CHECKING:
9
12
  from .base import BaseGateway
@@ -12,42 +15,50 @@ if TYPE_CHECKING:
12
15
  class Registration:
13
16
  def __init__(self, xmpp: "BaseGateway"):
14
17
  self.xmpp = xmpp
15
- xmpp["xep_0077"].api.register(
16
- user_store.get,
17
- "user_get",
18
- )
19
- xmpp["xep_0077"].api.register(
20
- user_store.remove,
21
- "user_remove",
22
- )
23
18
  xmpp["xep_0077"].api.register(
24
19
  self.xmpp.make_registration_form, "make_registration_form"
25
20
  )
21
+ xmpp["xep_0077"].api.register(self._user_get, "user_get")
26
22
  xmpp["xep_0077"].api.register(self._user_validate, "user_validate")
27
23
  xmpp["xep_0077"].api.register(self._user_modify, "user_modify")
24
+ # kept for slixmpp internal API compat
25
+ # TODO: either fully use slixmpp internal API or rewrite registration without it at all
26
+ xmpp["xep_0077"].api.register(lambda *a: None, "user_remove")
27
+
28
+ def get_user(self, jid: JID) -> GatewayUser | None:
29
+ return self.xmpp.store.users.get(jid)
30
+
31
+ async def _user_get(
32
+ self, _gateway_jid, _node, ifrom: JID, iq: Iq
33
+ ) -> GatewayUser | None:
34
+ if ifrom is None:
35
+ ifrom = iq.get_from()
36
+ return self.get_user(ifrom)
28
37
 
29
38
  async def _user_validate(self, _gateway_jid, _node, ifrom: JID, iq: Iq):
30
- """
31
- SliXMPP internal API stuff
32
- """
33
39
  xmpp = self.xmpp
34
40
  log.debug("User validate: %s", ifrom.bare)
35
41
  form_dict = {f.var: iq.get(f.var) for f in xmpp.REGISTRATION_FIELDS}
36
42
  xmpp.raise_if_not_allowed_jid(ifrom)
37
- await xmpp.user_prevalidate(ifrom, form_dict)
38
- log.info("New user: %s", ifrom.bare)
39
- user_store.add(ifrom, form_dict)
43
+ legacy_module_data = await xmpp.user_prevalidate(ifrom, form_dict)
44
+ if legacy_module_data is None:
45
+ legacy_module_data = form_dict
46
+ user = self.xmpp.store.users.new(
47
+ jid=ifrom,
48
+ legacy_module_data=legacy_module_data, # type:ignore
49
+ )
50
+ log.info("New user: %s", user)
40
51
 
41
52
  async def _user_modify(
42
53
  self, _gateway_jid, _node, ifrom: JID, form_dict: dict[str, Optional[str]]
43
54
  ):
44
- """
45
- SliXMPP internal API stuff
46
- """
47
- user = user_store.get_by_jid(ifrom)
48
- log.debug("Modify user: %s", user)
49
55
  await self.xmpp.user_prevalidate(ifrom, form_dict)
50
- user_store.add(ifrom, form_dict)
56
+ log.debug("Modify user: %s", ifrom)
57
+ user = self.xmpp.store.users.get(ifrom)
58
+ if user is None:
59
+ raise XMPPError("internal-server-error", "User not found")
60
+ user.legacy_module_data.update(form_dict)
61
+ self.xmpp.store.users.update(user)
51
62
 
52
63
 
53
64
  log = logging.getLogger(__name__)
@@ -3,8 +3,6 @@ from typing import TYPE_CHECKING
3
3
  from slixmpp import JID, CoroutineCallback, Iq, StanzaPath
4
4
  from slixmpp.exceptions import XMPPError
5
5
 
6
- from ...util.db import user_store
7
-
8
6
  if TYPE_CHECKING:
9
7
  from .base import BaseGateway
10
8
 
@@ -29,7 +27,7 @@ class Search:
29
27
  """
30
28
  Prepare the search form using :attr:`.BaseSession.SEARCH_FIELDS`
31
29
  """
32
- user = user_store.get_by_jid(ifrom)
30
+ user = self.xmpp.store.users.get(ifrom)
33
31
  if user is None:
34
32
  raise XMPPError(text="Search is only allowed for registered users")
35
33
 
@@ -47,7 +45,7 @@ class Search:
47
45
  """
48
46
  Handles a search request
49
47
  """
50
- user = user_store.get_by_jid(ifrom)
48
+ user = self.xmpp.store.users.get(ifrom)
51
49
  if user is None:
52
50
  raise XMPPError(text="Search is only allowed for registered users")
53
51
 
@@ -70,7 +68,7 @@ class Search:
70
68
  if iq.get_to() != self.xmpp.boundjid.bare:
71
69
  raise XMPPError("bad-request", "This can only be used on the component JID")
72
70
 
73
- user = user_store.get_by_jid(iq.get_from())
71
+ user = self.xmpp.store.users.get(iq.get_from())
74
72
  if user is None:
75
73
  raise XMPPError("not-authorized", "Register to the gateway first")
76
74