slidge 0.2.12__py3-none-any.whl → 0.3.0a0__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 (77) hide show
  1. slidge/__init__.py +5 -2
  2. slidge/command/adhoc.py +9 -3
  3. slidge/command/admin.py +16 -12
  4. slidge/command/base.py +16 -12
  5. slidge/command/chat_command.py +25 -16
  6. slidge/command/user.py +7 -8
  7. slidge/contact/contact.py +119 -209
  8. slidge/contact/roster.py +106 -105
  9. slidge/core/config.py +2 -43
  10. slidge/core/dispatcher/caps.py +9 -2
  11. slidge/core/dispatcher/disco.py +13 -3
  12. slidge/core/dispatcher/message/__init__.py +1 -1
  13. slidge/core/dispatcher/message/chat_state.py +17 -8
  14. slidge/core/dispatcher/message/marker.py +7 -5
  15. slidge/core/dispatcher/message/message.py +117 -92
  16. slidge/core/dispatcher/muc/__init__.py +1 -1
  17. slidge/core/dispatcher/muc/admin.py +4 -4
  18. slidge/core/dispatcher/muc/mam.py +10 -6
  19. slidge/core/dispatcher/muc/misc.py +4 -2
  20. slidge/core/dispatcher/muc/owner.py +5 -3
  21. slidge/core/dispatcher/muc/ping.py +3 -1
  22. slidge/core/dispatcher/presence.py +21 -15
  23. slidge/core/dispatcher/registration.py +20 -12
  24. slidge/core/dispatcher/search.py +7 -3
  25. slidge/core/dispatcher/session_dispatcher.py +13 -5
  26. slidge/core/dispatcher/util.py +37 -27
  27. slidge/core/dispatcher/vcard.py +7 -4
  28. slidge/core/gateway.py +168 -84
  29. slidge/core/mixins/__init__.py +1 -11
  30. slidge/core/mixins/attachment.py +163 -148
  31. slidge/core/mixins/avatar.py +100 -177
  32. slidge/core/mixins/db.py +50 -2
  33. slidge/core/mixins/message.py +19 -17
  34. slidge/core/mixins/message_maker.py +29 -15
  35. slidge/core/mixins/message_text.py +38 -30
  36. slidge/core/mixins/presence.py +91 -35
  37. slidge/core/pubsub.py +42 -47
  38. slidge/core/session.py +88 -57
  39. slidge/db/alembic/versions/0337c90c0b96_unify_legacy_xmpp_id_mappings.py +183 -0
  40. slidge/db/alembic/versions/4dbd23a3f868_new_avatar_store.py +56 -0
  41. slidge/db/alembic/versions/54ce3cde350c_use_hash_for_avatar_filenames.py +50 -0
  42. slidge/db/alembic/versions/58b98dacf819_refactor.py +118 -0
  43. slidge/db/alembic/versions/75a62b74b239_ditch_hats_table.py +74 -0
  44. slidge/db/avatar.py +150 -119
  45. slidge/db/meta.py +33 -22
  46. slidge/db/models.py +68 -117
  47. slidge/db/store.py +412 -1094
  48. slidge/group/archive.py +61 -54
  49. slidge/group/bookmarks.py +74 -55
  50. slidge/group/participant.py +135 -142
  51. slidge/group/room.py +315 -312
  52. slidge/main.py +28 -18
  53. slidge/migration.py +2 -12
  54. slidge/slixfix/__init__.py +20 -4
  55. slidge/slixfix/delivery_receipt.py +6 -4
  56. slidge/slixfix/link_preview/link_preview.py +1 -1
  57. slidge/slixfix/link_preview/stanza.py +1 -1
  58. slidge/slixfix/roster.py +5 -7
  59. slidge/slixfix/xep_0077/register.py +8 -8
  60. slidge/slixfix/xep_0077/stanza.py +7 -7
  61. slidge/slixfix/xep_0100/gateway.py +12 -13
  62. slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
  63. slidge/slixfix/xep_0292/vcard4.py +1 -1
  64. slidge/util/archive_msg.py +11 -5
  65. slidge/util/conf.py +23 -20
  66. slidge/util/jid_escaping.py +1 -1
  67. slidge/{core/mixins → util}/lock.py +6 -6
  68. slidge/util/test.py +30 -29
  69. slidge/util/types.py +22 -18
  70. slidge/util/util.py +19 -22
  71. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/METADATA +1 -1
  72. slidge-0.3.0a0.dist-info/RECORD +117 -0
  73. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/WHEEL +1 -1
  74. slidge-0.2.12.dist-info/RECORD +0 -112
  75. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/entry_points.txt +0 -0
  76. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
  77. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/top_level.txt +0 -0
slidge/core/gateway.py CHANGED
@@ -8,17 +8,19 @@ import re
8
8
  import tempfile
9
9
  from copy import copy
10
10
  from datetime import datetime
11
- from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Sequence, Union
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence, Type, Union, cast
12
13
 
13
14
  import aiohttp
14
15
  import qrcode
15
16
  from slixmpp import JID, ComponentXMPP, Iq, Message, Presence
16
17
  from slixmpp.exceptions import IqError, IqTimeout, XMPPError
18
+ from slixmpp.plugins.xep_0004 import Form
17
19
  from slixmpp.plugins.xep_0060.stanza import OwnerAffiliation
18
20
  from slixmpp.types import MessageTypes
19
21
  from slixmpp.xmlstream.xmlstream import NotConnectedError
20
22
 
21
- from slidge import command # noqa: F401
23
+ from slidge import LegacyContact, command # noqa: F401
22
24
  from slidge.command.adhoc import AdhocProvider
23
25
  from slidge.command.admin import Exec
24
26
  from slidge.command.base import Command, FormField
@@ -27,15 +29,17 @@ from slidge.command.register import RegistrationType
27
29
  from slidge.core import config
28
30
  from slidge.core.dispatcher.session_dispatcher import SessionDispatcher
29
31
  from slidge.core.mixins import MessageMixin
32
+ from slidge.core.mixins.avatar import convert_avatar
30
33
  from slidge.core.pubsub import PubSubComponent
31
34
  from slidge.core.session import BaseSession
32
35
  from slidge.db import GatewayUser, SlidgeStore
33
- from slidge.db.avatar import avatar_cache
36
+ from slidge.db.avatar import CachedAvatar, avatar_cache
37
+ from slidge.db.meta import JSONSerializable
34
38
  from slidge.slixfix import PrivilegedIqError
35
39
  from slidge.slixfix.delivery_receipt import DeliveryReceipt
36
40
  from slidge.slixfix.roster import RosterBackend
37
41
  from slidge.util import ABCSubclassableOnceAtMost
38
- from slidge.util.types import AvatarType, MessageOrPresenceTypeVar
42
+ from slidge.util.types import Avatar, MessageOrPresenceTypeVar
39
43
  from slidge.util.util import timeit
40
44
 
41
45
  if TYPE_CHECKING:
@@ -95,7 +99,7 @@ class BaseGateway(
95
99
  """Name of the component, as seen in service discovery by XMPP clients"""
96
100
  COMPONENT_TYPE: str = ""
97
101
  """Type of the gateway, should follow https://xmpp.org/registrar/disco-categories.html"""
98
- COMPONENT_AVATAR: Optional[AvatarType] = None
102
+ COMPONENT_AVATAR: Optional[Avatar | Path | str] = None
99
103
  """
100
104
  Path, bytes or URL used by the component as an avatar.
101
105
  """
@@ -147,6 +151,27 @@ class BaseGateway(
147
151
  required=True,
148
152
  type="boolean",
149
153
  ),
154
+ FormField(
155
+ var="always_invite_when_adding_bookmarks",
156
+ label="Send an invitation to join MUCs after adding them to the bookmarks.",
157
+ value="true",
158
+ required=True,
159
+ type="boolean",
160
+ ),
161
+ FormField(
162
+ var="last_seen_fallback",
163
+ label="Use contact presence status message to show when they were last seen.",
164
+ value="true",
165
+ required=True,
166
+ type="boolean",
167
+ ),
168
+ FormField(
169
+ var="roster_push",
170
+ label="Add contacts to your roster.",
171
+ value="true",
172
+ required=True,
173
+ type="boolean",
174
+ ),
150
175
  ]
151
176
 
152
177
  ROSTER_GROUP: str = "slidge"
@@ -211,7 +236,6 @@ class BaseGateway(
211
236
  is_group = False
212
237
  _can_send_carbon = False
213
238
  store: SlidgeStore
214
- avatar_pk: int
215
239
 
216
240
  AVATAR_ID_TYPE: Callable[[str], Any] = str
217
241
  """
@@ -253,8 +277,9 @@ class BaseGateway(
253
277
  """
254
278
 
255
279
  http: aiohttp.ClientSession
280
+ avatar: CachedAvatar | None = None
256
281
 
257
- def __init__(self):
282
+ def __init__(self) -> None:
258
283
  if config.COMPONENT_NAME:
259
284
  self.COMPONENT_NAME = config.COMPONENT_NAME
260
285
  if config.WELCOME_MESSAGE:
@@ -298,18 +323,13 @@ class BaseGateway(
298
323
  self.jid_validator: re.Pattern = re.compile(config.USER_JID_VALIDATOR)
299
324
  self.qr_pending_registrations = dict[str, asyncio.Future[Optional[dict]]]()
300
325
 
301
- self.session_cls: BaseSession = BaseSession.get_unique_subclass()
302
- self.session_cls.xmpp = self
303
-
304
- from ..group.room import LegacyMUC
305
-
306
- LegacyMUC.get_self_or_unique_subclass().xmpp = self
326
+ self.__setup_legacy_module_subclasses()
307
327
 
308
328
  self.get_session_from_stanza: Callable[
309
329
  [Union[Message, Presence, Iq]], BaseSession
310
- ] = self.session_cls.from_stanza # type: ignore
330
+ ] = self._session_cls.from_stanza
311
331
  self.get_session_from_user: Callable[[GatewayUser], BaseSession] = (
312
- self.session_cls.from_user
332
+ self._session_cls.from_user
313
333
  )
314
334
 
315
335
  self.register_plugins()
@@ -349,13 +369,41 @@ class BaseGateway(
349
369
 
350
370
  MessageMixin.__init__(self) # ComponentXMPP does not call super().__init__()
351
371
 
352
- async def __set_http(self):
372
+ def __setup_legacy_module_subclasses(self):
373
+ from ..contact.roster import LegacyRoster
374
+ from ..group.bookmarks import LegacyBookmarks
375
+ from ..group.room import LegacyMUC, LegacyParticipant
376
+
377
+ session_cls: Type[BaseSession] = cast(
378
+ Type[BaseSession], BaseSession.get_unique_subclass()
379
+ )
380
+ contact_cls = LegacyContact.get_self_or_unique_subclass()
381
+ muc_cls = LegacyMUC.get_self_or_unique_subclass()
382
+ participant_cls = LegacyParticipant.get_self_or_unique_subclass()
383
+ bookmarks_cls = LegacyBookmarks.get_self_or_unique_subclass()
384
+ roster_cls = LegacyRoster.get_self_or_unique_subclass()
385
+
386
+ session_cls.xmpp = self # type:ignore[attr-defined]
387
+ contact_cls.xmpp = self # type:ignore[attr-defined]
388
+ muc_cls.xmpp = self # type:ignore[attr-defined]
389
+
390
+ self._session_cls = session_cls
391
+ session_cls._bookmarks_cls = bookmarks_cls # type:ignore[misc,assignment]
392
+ session_cls._roster_cls = roster_cls # type:ignore[misc,assignment]
393
+ LegacyRoster._contact_cls = contact_cls # type:ignore[misc]
394
+ LegacyBookmarks._muc_cls = muc_cls # type:ignore[misc]
395
+ LegacyMUC._participant_cls = participant_cls # type:ignore[misc]
396
+
397
+ async def kill_session(self, jid: JID) -> None:
398
+ await self._session_cls.kill_by_jid(jid)
399
+
400
+ async def __set_http(self) -> None:
353
401
  self.http = aiohttp.ClientSession()
354
402
  if getattr(self, "_test_mode", False):
355
403
  return
356
404
  avatar_cache.http = self.http
357
405
 
358
- def __register_commands(self):
406
+ def __register_commands(self) -> None:
359
407
  for cls in Command.subclasses:
360
408
  if any(x is NotImplemented for x in [cls.CHAT_COMMAND, cls.NODE, cls.NAME]):
361
409
  log.debug("Not adding command '%s' because it looks abstract", cls)
@@ -370,7 +418,7 @@ class BaseGateway(
370
418
  self.__adhoc_handler.register(c)
371
419
  self.__chat_commands_handler.register(c)
372
420
 
373
- def __exception_handler(self, loop: asyncio.AbstractEventLoop, context):
421
+ def __exception_handler(self, loop: asyncio.AbstractEventLoop, context) -> None:
374
422
  """
375
423
  Called when a task created by loop.create_task() raises an Exception
376
424
 
@@ -390,7 +438,7 @@ class BaseGateway(
390
438
  self.has_crashed = True
391
439
  loop.stop()
392
440
 
393
- def __register_slixmpp_events(self):
441
+ def __register_slixmpp_events(self) -> None:
394
442
  self.del_event_handler("presence_subscribe", self._handle_subscribe)
395
443
  self.del_event_handler("presence_unsubscribe", self._handle_unsubscribe)
396
444
  self.del_event_handler("presence_subscribed", self._handle_subscribed)
@@ -403,16 +451,32 @@ class BaseGateway(
403
451
  self.add_event_handler("disconnected", self.connect)
404
452
 
405
453
  def __register_slixmpp_api(self) -> None:
406
- self.plugin["xep_0231"].api.register(self.store.bob.get_bob, "get_bob")
407
- self.plugin["xep_0231"].api.register(self.store.bob.set_bob, "set_bob")
408
- self.plugin["xep_0231"].api.register(self.store.bob.del_bob, "del_bob")
454
+ def with_session(func, commit: bool = True):
455
+ def wrapped(*a, **kw):
456
+ with self.store.session() as orm:
457
+ res = func(orm, *a, **kw)
458
+ if commit:
459
+ orm.commit()
460
+ return res
461
+
462
+ return wrapped
463
+
464
+ self.plugin["xep_0231"].api.register(
465
+ with_session(self.store.bob.get_bob, False), "get_bob"
466
+ )
467
+ self.plugin["xep_0231"].api.register(
468
+ with_session(self.store.bob.set_bob), "set_bob"
469
+ )
470
+ self.plugin["xep_0231"].api.register(
471
+ with_session(self.store.bob.del_bob), "del_bob"
472
+ )
409
473
 
410
474
  @property # type: ignore
411
475
  def jid(self):
412
476
  # Override to avoid slixmpp deprecation warnings.
413
477
  return self.boundjid
414
478
 
415
- async def __on_session_start(self, event):
479
+ async def __on_session_start(self, event) -> None:
416
480
  log.debug("Gateway session start: %s", event)
417
481
 
418
482
  # prevents XMPP clients from considering the gateway as an HTTP upload
@@ -421,47 +485,48 @@ class BaseGateway(
421
485
  await self.plugin["xep_0115"].update_caps(jid=self.boundjid)
422
486
 
423
487
  if self.COMPONENT_AVATAR is not None:
488
+ log.debug("Setting gateway avatar…")
489
+ avatar = convert_avatar(self.COMPONENT_AVATAR, "!!---slidge---special---")
490
+ assert avatar is not None
424
491
  try:
425
- cached_avatar = await avatar_cache.convert_or_get(self.COMPONENT_AVATAR)
492
+ cached_avatar = await avatar_cache.convert_or_get(avatar)
426
493
  except Exception as e:
427
494
  log.exception("Could not set the component avatar.", exc_info=e)
428
495
  cached_avatar = None
429
496
  else:
430
497
  assert cached_avatar is not None
431
- self.avatar_pk = cached_avatar.pk
498
+ self.avatar = cached_avatar
432
499
  else:
433
500
  cached_avatar = None
434
501
 
435
- for user in self.store.users.get_all():
436
- # TODO: before this, we should check if the user has removed us from their roster
437
- # while we were offline and trigger unregister from there. Presence probe does not seem
438
- # to work in this case, there must be another way. privileged entity could be used
439
- # as last resort.
440
- try:
441
- await self["xep_0100"].add_component_to_roster(user.jid)
442
- await self.__add_component_to_mds_whitelist(user.jid)
443
- except (IqError, IqTimeout) as e:
444
- # TODO: remove the user when this happens? or at least
445
- # this can happen when the user has unsubscribed from the XMPP server
446
- log.warning(
447
- "Error with user %s, not logging them automatically",
448
- user,
449
- exc_info=e,
450
- )
451
- continue
452
- self.send_presence(
453
- pto=user.jid.bare, ptype="probe"
454
- ) # ensure we get all resources for user
455
- session = self.session_cls.from_user(user)
456
- session.create_task(self.login_wrap(session))
457
- if cached_avatar is not None:
458
- await self.pubsub.broadcast_avatar(
459
- self.boundjid.bare, session.user_jid, cached_avatar
460
- )
502
+ with self.store.session() as orm:
503
+ for user in orm.query(GatewayUser).all():
504
+ # TODO: before this, we should check if the user has removed us from their roster
505
+ # while we were offline and trigger unregister from there. Presence probe does not seem
506
+ # to work in this case, there must be another way. privileged entity could be used
507
+ # as last resort.
508
+ try:
509
+ await self["xep_0100"].add_component_to_roster(user.jid)
510
+ await self.__add_component_to_mds_whitelist(user.jid)
511
+ except (IqError, IqTimeout) as e:
512
+ # TODO: remove the user when this happens? or at least
513
+ # this can happen when the user has unsubscribed from the XMPP server
514
+ log.warning(
515
+ "Error with user %s, not logging them automatically",
516
+ user,
517
+ exc_info=e,
518
+ )
519
+ continue
520
+ session = self._session_cls.from_user(user)
521
+ session.create_task(self.login_wrap(session))
522
+ if cached_avatar is not None:
523
+ await self.pubsub.broadcast_avatar(
524
+ self.boundjid.bare, session.user_jid, cached_avatar
525
+ )
461
526
 
462
527
  log.info("Slidge has successfully started")
463
528
 
464
- async def __add_component_to_mds_whitelist(self, user_jid: JID):
529
+ async def __add_component_to_mds_whitelist(self, user_jid: JID) -> None:
465
530
  # Uses privileged entity to add ourselves to the whitelist of the PEP
466
531
  # MDS node so we receive MDS events
467
532
  iq_creation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set")
@@ -515,7 +580,7 @@ class BaseGateway(
515
580
  )
516
581
 
517
582
  @timeit
518
- async def login_wrap(self, session: "BaseSession"):
583
+ async def login_wrap(self, session: "BaseSession") -> None:
519
584
  session.send_gateway_status("Logging in…", show="dnd")
520
585
  session.is_logging_in = True
521
586
  try:
@@ -534,7 +599,8 @@ class BaseGateway(
534
599
  log.info("Login success for %s", session.user_jid)
535
600
  session.logged = True
536
601
  session.send_gateway_status("Syncing contacts…", show="dnd")
537
- await session.contacts._fill()
602
+ with self.store.session() as orm:
603
+ await session.contacts._fill(orm)
538
604
  if not (r := session.contacts.ready).done():
539
605
  r.set_result(True)
540
606
  if self.GROUPS:
@@ -542,10 +608,7 @@ class BaseGateway(
542
608
  await session.bookmarks.fill()
543
609
  if not (r := session.bookmarks.ready).done():
544
610
  r.set_result(True)
545
- for c in session.contacts:
546
- # we need to receive presences directed at the contacts, in
547
- # order to send pubsub events for their +notify features
548
- self.send_presence(pfrom=c.jid, pto=session.user_jid.bare, ptype="probe")
611
+ self.send_presence(pto=session.user.jid.bare, ptype="probe")
549
612
  if status is None:
550
613
  session.send_gateway_status("Logged in", show="chat")
551
614
  else:
@@ -553,9 +616,12 @@ class BaseGateway(
553
616
  if session.user.preferences.get("sync_avatar", False):
554
617
  session.create_task(self.fetch_user_avatar(session))
555
618
  else:
556
- self.xmpp.store.users.set_avatar_hash(session.user_pk, None)
619
+ with self.store.session(expire_on_commit=False) as orm:
620
+ session.user.avatar_hash = None
621
+ orm.add(session.user)
622
+ orm.commit()
557
623
 
558
- async def fetch_user_avatar(self, session: BaseSession):
624
+ async def fetch_user_avatar(self, session: BaseSession) -> None:
559
625
  try:
560
626
  iq = await self.xmpp.plugin["xep_0060"].get_items(
561
627
  session.user_jid.bare,
@@ -573,7 +639,10 @@ class BaseGateway(
573
639
  except NotImplementedError:
574
640
  pass
575
641
  else:
576
- self.xmpp.store.users.set_avatar_hash(session.user_pk, None)
642
+ with self.store.session(expire_on_commit=False) as orm:
643
+ session.user.avatar_hash = None
644
+ orm.add(session.user)
645
+ orm.commit()
577
646
  return
578
647
  await self.__dispatcher.on_avatar_metadata_info(
579
648
  session, iq["pubsub"]["items"]["item"]["avatar_metadata"]["info"]
@@ -622,11 +691,11 @@ class BaseGateway(
622
691
 
623
692
  def get_session_from_jid(self, j: JID):
624
693
  try:
625
- return self.session_cls.from_jid(j)
694
+ return self._session_cls.from_jid(j)
626
695
  except XMPPError:
627
696
  pass
628
697
 
629
- def exception(self, exception: Exception):
698
+ def exception(self, exception: Exception) -> None:
630
699
  # """
631
700
  # Called when a task created by slixmpp's internal (eg, on slix events) raises an Exception.
632
701
  #
@@ -656,18 +725,26 @@ class BaseGateway(
656
725
  self.loop.stop()
657
726
  exit(1)
658
727
 
659
- def re_login(self, session: "BaseSession"):
660
- async def w():
728
+ def re_login(self, session: "BaseSession") -> None:
729
+ async def w() -> None:
661
730
  session.cancel_all_tasks()
662
- await session.logout()
731
+ try:
732
+ await session.logout()
733
+ except NotImplementedError:
734
+ pass
663
735
  await self.login_wrap(session)
664
736
 
665
737
  session.create_task(w())
666
738
 
667
- async def make_registration_form(self, _jid, _node, _ifrom, iq: Iq):
739
+ async def make_registration_form(
740
+ self, _jid: JID, _node: str, _ifrom: JID, iq: Iq
741
+ ) -> Form:
668
742
  self.raise_if_not_allowed_jid(iq.get_from())
669
743
  reg = iq["register"]
670
- user = self.store.users.get_by_stanza(iq)
744
+ with self.store.session() as orm:
745
+ user = (
746
+ orm.query(GatewayUser).filter_by(jid=iq.get_from().bare).one_or_none()
747
+ )
671
748
  log.debug("User found: %s", user)
672
749
 
673
750
  form = reg["form"]
@@ -715,7 +792,7 @@ class BaseGateway(
715
792
 
716
793
  async def user_prevalidate(
717
794
  self, ifrom: JID, form_dict: dict[str, Optional[str]]
718
- ) -> Optional[Mapping]:
795
+ ) -> JSONSerializable | None:
719
796
  # Pre validate a registration form using the content of self.REGISTRATION_FIELDS
720
797
  # before passing it to the plugin custom validation logic.
721
798
  for field in self.REGISTRATION_FIELDS:
@@ -726,7 +803,7 @@ class BaseGateway(
726
803
 
727
804
  async def validate(
728
805
  self, user_jid: JID, registration_form: dict[str, Optional[str]]
729
- ) -> Optional[Mapping]:
806
+ ) -> JSONSerializable | None:
730
807
  """
731
808
  Validate a user's initial registration form.
732
809
 
@@ -758,7 +835,7 @@ class BaseGateway(
758
835
 
759
836
  async def validate_two_factor_code(
760
837
  self, user: GatewayUser, code: str
761
- ) -> Optional[dict]:
838
+ ) -> JSONSerializable | None:
762
839
  """
763
840
  Called when the user enters their 2FA code.
764
841
 
@@ -798,8 +875,8 @@ class BaseGateway(
798
875
  self,
799
876
  user_bare_jid: str,
800
877
  exception: Optional[Exception] = None,
801
- legacy_data: Optional[dict] = None,
802
- ):
878
+ legacy_data: Optional[JSONSerializable] = None,
879
+ ) -> None:
803
880
  """
804
881
  This method is meant to be called to finalize QR code-based registration
805
882
  flows, once the legacy service confirms the QR flashing.
@@ -820,16 +897,16 @@ class BaseGateway(
820
897
  else:
821
898
  fut.set_exception(exception)
822
899
 
823
- async def unregister_user(self, user: GatewayUser):
900
+ async def unregister_user(self, user: GatewayUser) -> None:
824
901
  self.send_presence(
825
902
  pshow="dnd",
826
903
  pstatus="You unregistered from this gateway.",
827
904
  pto=user.jid,
828
905
  )
829
906
  await self.xmpp.plugin["xep_0077"].api["user_remove"](None, None, user.jid)
830
- await self.xmpp.session_cls.kill_by_jid(user.jid)
907
+ await self.xmpp._session_cls.kill_by_jid(user.jid)
831
908
 
832
- async def unregister(self, user: GatewayUser):
909
+ async def unregister(self, user: GatewayUser) -> None:
833
910
  """
834
911
  Optionally override this if you need to clean additional
835
912
  stuff after a user has been removed from the persistent user store.
@@ -845,7 +922,11 @@ class BaseGateway(
845
922
  pass
846
923
 
847
924
  async def input(
848
- self, jid: JID, text=None, mtype: MessageTypes = "chat", **msg_kwargs
925
+ self,
926
+ jid: JID,
927
+ text: str | None = None,
928
+ mtype: MessageTypes = "chat",
929
+ **input_kwargs: Any,
849
930
  ) -> str:
850
931
  """
851
932
  Request arbitrary user input using a simple chat message, and await the result.
@@ -858,9 +939,11 @@ class BaseGateway(
858
939
  :param mtype: Message type
859
940
  :return: The user's reply
860
941
  """
861
- return await self.__chat_commands_handler.input(jid, text, mtype, **msg_kwargs)
942
+ return await self.__chat_commands_handler.input(
943
+ jid, text, mtype=mtype, **input_kwargs
944
+ )
862
945
 
863
- async def send_qr(self, text: str, **msg_kwargs):
946
+ async def send_qr(self, text: str, **msg_kwargs: Any) -> None:
864
947
  """
865
948
  Sends a QR Code to a JID
866
949
 
@@ -877,9 +960,9 @@ class BaseGateway(
877
960
  suffix=".png", delete=config.NO_UPLOAD_METHOD != "move"
878
961
  ) as f:
879
962
  qr.save(f.name)
880
- await self.send_file(f.name, **msg_kwargs)
963
+ await self.send_file(Path(f.name), **msg_kwargs)
881
964
 
882
- def shutdown(self) -> list[asyncio.Task]:
965
+ def shutdown(self) -> list[asyncio.Task[None]]:
883
966
  # """
884
967
  # Called by the slidge entrypoint on normal exit.
885
968
  #
@@ -889,9 +972,10 @@ class BaseGateway(
889
972
  # """
890
973
  log.debug("Shutting down")
891
974
  tasks = []
892
- for user in self.store.users.get_all():
893
- tasks.append(self.session_cls.from_jid(user.jid).shutdown())
894
- self.send_presence(ptype="unavailable", pto=user.jid)
975
+ with self.store.session() as orm:
976
+ for user in orm.query(GatewayUser).all():
977
+ tasks.append(self._session_cls.from_jid(user.jid).shutdown())
978
+ self.send_presence(ptype="unavailable", pto=user.jid)
895
979
  return tasks
896
980
 
897
981
 
@@ -2,8 +2,6 @@
2
2
  Mixins
3
3
  """
4
4
 
5
- from typing import Optional
6
-
7
5
  from .avatar import AvatarMixin
8
6
  from .disco import ChatterDiscoMixin
9
7
  from .message import MessageCarbonMixin, MessageMixin
@@ -18,12 +16,4 @@ class FullCarbonMixin(ChatterDiscoMixin, MessageCarbonMixin, PresenceMixin):
18
16
  pass
19
17
 
20
18
 
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")
19
+ __all__ = ("AvatarMixin", "FullCarbonMixin")