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

Sign up to get free protection for your applications and to get access to all the features.
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