slidge 0.1.2__tar.gz → 0.2.0a0__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. {slidge-0.1.2 → slidge-0.2.0a0}/PKG-INFO +3 -1
  2. {slidge-0.1.2 → slidge-0.2.0a0}/pyproject.toml +3 -1
  3. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/__init__.py +3 -5
  4. slidge-0.2.0a0/slidge/__main__.py +4 -0
  5. slidge-0.2.0a0/slidge/__version__.py +5 -0
  6. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/command/adhoc.py +8 -1
  7. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/command/admin.py +5 -6
  8. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/command/base.py +1 -2
  9. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/command/register.py +32 -16
  10. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/command/user.py +85 -5
  11. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/contact/contact.py +93 -31
  12. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/contact/roster.py +54 -39
  13. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/config.py +13 -7
  14. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/base.py +139 -34
  15. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/disco.py +2 -4
  16. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/mam.py +1 -4
  17. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/ping.py +2 -3
  18. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/presence.py +1 -1
  19. slidge-0.2.0a0/slidge/core/gateway/registration.py +64 -0
  20. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/search.py +3 -5
  21. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/session_dispatcher.py +109 -51
  22. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/vcard_temp.py +6 -4
  23. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/__init__.py +11 -1
  24. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/attachment.py +15 -10
  25. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/avatar.py +66 -18
  26. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/base.py +8 -2
  27. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/message.py +11 -7
  28. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/message_maker.py +17 -9
  29. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/presence.py +14 -4
  30. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/pubsub.py +54 -212
  31. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/session.py +65 -33
  32. slidge-0.2.0a0/slidge/db/__init__.py +4 -0
  33. slidge-0.2.0a0/slidge/db/alembic/env.py +64 -0
  34. slidge-0.2.0a0/slidge/db/alembic/script.py.mako +26 -0
  35. slidge-0.2.0a0/slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  36. slidge-0.2.0a0/slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  37. slidge-0.2.0a0/slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  38. slidge-0.2.0a0/slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +76 -0
  39. slidge-0.2.0a0/slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  40. slidge-0.2.0a0/slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  41. slidge-0.2.0a0/slidge/db/avatar.py +224 -0
  42. slidge-0.2.0a0/slidge/db/meta.py +65 -0
  43. slidge-0.2.0a0/slidge/db/models.py +365 -0
  44. slidge-0.2.0a0/slidge/db/store.py +976 -0
  45. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/group/archive.py +13 -14
  46. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/group/bookmarks.py +59 -56
  47. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/group/participant.py +81 -29
  48. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/group/room.py +242 -142
  49. slidge-0.1.2/slidge/__main__.py → slidge-0.2.0a0/slidge/main.py +16 -13
  50. slidge-0.2.0a0/slidge/migration.py +48 -0
  51. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/__init__.py +35 -2
  52. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/roster.py +11 -4
  53. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0292/vcard4.py +1 -0
  54. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/util/db.py +1 -47
  55. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/util/test.py +21 -4
  56. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/util/types.py +24 -4
  57. slidge-0.1.2/slidge/core/cache.py +0 -183
  58. slidge-0.1.2/slidge/core/gateway/registration.py +0 -53
  59. slidge-0.1.2/slidge/migration.py +0 -18
  60. slidge-0.1.2/slidge/util/schema.sql +0 -126
  61. slidge-0.1.2/slidge/util/sql.py +0 -508
  62. {slidge-0.1.2 → slidge-0.2.0a0}/LICENSE +0 -0
  63. {slidge-0.1.2 → slidge-0.2.0a0}/README.md +0 -0
  64. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/command/__init__.py +0 -0
  65. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/command/categories.py +0 -0
  66. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/command/chat_command.py +0 -0
  67. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/contact/__init__.py +0 -0
  68. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/__init__.py +0 -0
  69. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/__init__.py +0 -0
  70. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/caps.py +0 -0
  71. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/delivery_receipt.py +0 -0
  72. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/gateway/muc_admin.py +0 -0
  73. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/disco.py +0 -0
  74. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/lock.py +0 -0
  75. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/core/mixins/recipient.py +0 -0
  76. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/group/__init__.py +0 -0
  77. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/py.typed +0 -0
  78. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/link_preview/__init__.py +0 -0
  79. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/link_preview/link_preview.py +0 -0
  80. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/link_preview/stanza.py +0 -0
  81. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0077/__init__.py +0 -0
  82. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0077/register.py +0 -0
  83. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0077/stanza.py +0 -0
  84. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0100/__init__.py +0 -0
  85. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0100/gateway.py +0 -0
  86. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0100/stanza.py +0 -0
  87. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0153/__init__.py +0 -0
  88. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0153/stanza.py +0 -0
  89. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0153/vcard_avatar.py +0 -0
  90. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0264/__init__.py +0 -0
  91. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0264/stanza.py +0 -0
  92. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0264/thumbnail.py +0 -0
  93. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0292/__init__.py +0 -0
  94. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0313/__init__.py +0 -0
  95. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0313/mam.py +0 -0
  96. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0313/stanza.py +0 -0
  97. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0317/__init__.py +0 -0
  98. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0317/hats.py +0 -0
  99. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0317/stanza.py +0 -0
  100. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0356_old/__init__.py +0 -0
  101. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0356_old/privilege.py +0 -0
  102. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0356_old/stanza.py +0 -0
  103. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0424/__init__.py +0 -0
  104. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0424/retraction.py +0 -0
  105. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0424/stanza.py +0 -0
  106. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0490/__init__.py +0 -0
  107. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0490/mds.py +0 -0
  108. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/slixfix/xep_0490/stanza.py +0 -0
  109. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/util/__init__.py +0 -0
  110. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/util/archive_msg.py +0 -0
  111. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/util/conf.py +0 -0
  112. {slidge-0.1.2 → slidge-0.2.0a0}/slidge/util/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: slidge
3
- Version: 0.1.2
3
+ Version: 0.2.0a0
4
4
  Summary: XMPP bridging framework
5
5
  Home-page: https://sr.ht/~nicoco/slidge/
6
6
  License: AGPL-3.0-or-later
@@ -15,11 +15,13 @@ Classifier: Programming Language :: Python :: 3.11
15
15
  Requires-Dist: ConfigArgParse (>=1.5.3,<2.0.0)
16
16
  Requires-Dist: Pillow (>=10,<11)
17
17
  Requires-Dist: aiohttp[speedups] (>=3.8.3,<4.0.0)
18
+ Requires-Dist: alembic (>=1.13.1,<2.0.0)
18
19
  Requires-Dist: blurhash-python (>=1.2.1,<2.0.0)
19
20
  Requires-Dist: pickle-secure (>=0.99.9,<0.100.0)
20
21
  Requires-Dist: python-magic (>=0.4.27,<0.5.0)
21
22
  Requires-Dist: qrcode (>=7.4.1,<8.0.0)
22
23
  Requires-Dist: slixmpp (>=1.8.5,<2.0.0)
24
+ Requires-Dist: sqlalchemy (>=2.0.29,<3.0.0)
23
25
  Project-URL: Documentation, https://slidge.im/
24
26
  Project-URL: Repository, https://git.sr.ht/~nicoco/slidge/
25
27
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "slidge"
3
- version = "0.1.2"
3
+ version = "0.2.0alpha0"
4
4
  description = "XMPP bridging framework"
5
5
  authors = ["Nicolas Cedilnik <nicoco@nicoco.fr>"]
6
6
  readme = "README.md"
@@ -20,6 +20,8 @@ pickle-secure = "^0.99.9"
20
20
  python-magic = "^0.4.27"
21
21
  slixmpp = "^1.8.5"
22
22
  blurhash-python = "^1.2.1"
23
+ sqlalchemy = "^2.0.29"
24
+ alembic = "^1.13.1"
23
25
 
24
26
  [build-system]
25
27
  requires = ["poetry-core>=1.0.0"]
@@ -13,13 +13,12 @@ from .contact import LegacyContact, LegacyRoster # noqa: F401
13
13
  from .core import config as global_config # noqa: F401
14
14
  from .core.gateway import BaseGateway # noqa: F401
15
15
  from .core.session import BaseSession # noqa: F401
16
+ from .db import GatewayUser # noqa: F401
16
17
  from .group import LegacyBookmarks, LegacyMUC, LegacyParticipant # noqa: F401
17
- from .util.db import GatewayUser, user_store # noqa: F401
18
+ from .main import main as main_func
18
19
  from .util.types import MucType # noqa: F401
19
20
  from .util.util import addLoggingLevel
20
21
 
21
- from .__main__ import main # isort: skip
22
-
23
22
 
24
23
  def entrypoint(module_name: str) -> None:
25
24
  """
@@ -29,7 +28,7 @@ def entrypoint(module_name: str) -> None:
29
28
  :param module_name: An importable :term:`Legacy Module`.
30
29
  """
31
30
  sys.argv.extend(["--legacy", module_name])
32
- main()
31
+ main_func()
33
32
 
34
33
 
35
34
  def formatwarning(message, category, filename, lineno, line=""):
@@ -54,7 +53,6 @@ __all__ = [
54
53
  # "FormField",
55
54
  # "SearchResult",
56
55
  "entrypoint",
57
- "user_store",
58
56
  "global_config",
59
57
  ]
60
58
 
@@ -0,0 +1,4 @@
1
+ if __name__ == "__main__":
2
+ from slidge.main import main
3
+
4
+ main()
@@ -0,0 +1,5 @@
1
+ from slidge.util.util import get_version # noqa: F401
2
+
3
+ # this is modified before publish, but if someone cloned from the repo,
4
+ # it can help
5
+ __version__ = "0.2.0alpha0"
@@ -48,7 +48,10 @@ class AdhocProvider:
48
48
  async def __handle_category_list(
49
49
  self, category: str, iq: Iq, adhoc_session: AdhocSessionType
50
50
  ) -> AdhocSessionType:
51
- session = self.xmpp.get_session_from_stanza(iq)
51
+ try:
52
+ session = self.xmpp.get_session_from_stanza(iq)
53
+ except XMPPError:
54
+ session = None
52
55
  commands = []
53
56
  for command in self._categories[category]:
54
57
  try:
@@ -56,6 +59,10 @@ class AdhocProvider:
56
59
  except XMPPError:
57
60
  continue
58
61
  commands.append(command)
62
+ if len(commands) == 0:
63
+ raise XMPPError(
64
+ "not-authorized", "There is no command you can run in this category"
65
+ )
59
66
  return await self.__handle_result(
60
67
  session,
61
68
  Form(
@@ -7,7 +7,6 @@ from typing import Any, Optional
7
7
  from slixmpp import JID
8
8
  from slixmpp.exceptions import XMPPError
9
9
 
10
- from ..util.db import user_store
11
10
  from ..util.types import AnyBaseSession
12
11
  from .base import (
13
12
  Command,
@@ -33,13 +32,13 @@ class ListUsers(AdminCommand):
33
32
 
34
33
  async def run(self, _session, _ifrom, *_):
35
34
  items = []
36
- for u in user_store.get_all():
35
+ for u in self.xmpp.store.users.get_all():
37
36
  d = u.registration_date
38
37
  if d is None:
39
38
  joined = ""
40
39
  else:
41
40
  joined = d.isoformat(timespec="seconds")
42
- items.append({"jid": u.bare_jid, "joined": joined})
41
+ items.append({"jid": u.jid.bare, "joined": joined})
43
42
  return TableResult(
44
43
  description="List of registered users",
45
44
  fields=[FormField("jid", type="jid-single"), FormField("joined")],
@@ -54,7 +53,7 @@ class SlidgeInfo(AdminCommand):
54
53
  ACCESS = CommandAccess.ANY
55
54
 
56
55
  async def run(self, _session, _ifrom, *_):
57
- from ..__main__ import __version__
56
+ from slidge.__version__ import __version__
58
57
 
59
58
  start = self.xmpp.datetime_started
60
59
  uptime = datetime.now() - start
@@ -114,7 +113,7 @@ class DeleteUser(AdminCommand):
114
113
  self, form_values: FormValues, _session: AnyBaseSession, _ifrom: JID
115
114
  ) -> Confirmation:
116
115
  jid: JID = form_values.get("jid") # type:ignore
117
- user = user_store.get_by_jid(jid)
116
+ user = self.xmpp.store.users.get(jid)
118
117
  if user is None:
119
118
  raise XMPPError("item-not-found", text=f"There is no user '{jid}'")
120
119
 
@@ -127,7 +126,7 @@ class DeleteUser(AdminCommand):
127
126
  async def finish(
128
127
  self, _session: Optional[AnyBaseSession], _ifrom: JID, jid: JID
129
128
  ) -> None:
130
- user = user_store.get_by_jid(jid)
129
+ user = self.xmpp.store.users.get(jid)
131
130
  if user is None:
132
131
  raise XMPPError("bad-request", f"{jid} has no account here!")
133
132
  await self.xmpp.unregister_user(user)
@@ -23,7 +23,6 @@ from slixmpp.plugins.xep_0004 import (
23
23
  from slixmpp.types import JidStr
24
24
 
25
25
  from ..core import config
26
- from ..util.db import user_store
27
26
  from ..util.types import AnyBaseSession, FieldType
28
27
 
29
28
  if TYPE_CHECKING:
@@ -382,7 +381,7 @@ class Command(ABC):
382
381
  raise XMPPError("feature-not-implemented")
383
382
 
384
383
  def _get_session(self, jid: JID) -> Optional["BaseSession[Any, Any]"]:
385
- user = user_store.get_by_jid(jid)
384
+ user = self.xmpp.store.users.get(jid)
386
385
  if user is None:
387
386
  return None
388
387
 
@@ -6,7 +6,6 @@ step for a JID to become a slidge :term:`User`.
6
6
  import asyncio
7
7
  import functools
8
8
  import tempfile
9
- from datetime import datetime
10
9
  from enum import IntEnum
11
10
  from typing import Any
12
11
 
@@ -15,8 +14,10 @@ from slixmpp import JID, Iq
15
14
  from slixmpp.exceptions import XMPPError
16
15
 
17
16
  from ..core import config
18
- from ..util.db import GatewayUser
17
+ from ..db import GatewayUser
18
+ from ..util.types import UserPreferences
19
19
  from .base import Command, CommandAccess, Form, FormField, FormValues
20
+ from .user import Preferences
20
21
 
21
22
 
22
23
  class RegistrationType(IntEnum):
@@ -66,9 +67,12 @@ class Register(Command):
66
67
 
67
68
  SUCCESS_MESSAGE = "Success, welcome!"
68
69
 
69
- def _finalize(self, user: GatewayUser):
70
- user.commit()
71
- self.xmpp.event("user_register", Iq(sfrom=user.jid))
70
+ def _finalize(
71
+ self, form_values: UserPreferences, _session, ifrom: JID, user: GatewayUser, *_
72
+ ) -> str:
73
+ user.preferences = form_values # type: ignore
74
+ self.xmpp.store.users.update(user)
75
+ self.xmpp.event("user_register", Iq(sfrom=ifrom.bare))
72
76
  return self.SUCCESS_MESSAGE
73
77
 
74
78
  async def run(self, _session, _ifrom, *_):
@@ -82,26 +86,26 @@ class Register(Command):
82
86
  async def register(self, form_values: dict[str, Any], _session, ifrom: JID):
83
87
  two_fa_needed = True
84
88
  try:
85
- await self.xmpp.user_prevalidate(ifrom, form_values)
89
+ data = await self.xmpp.user_prevalidate(ifrom, form_values)
86
90
  except ValueError as e:
87
91
  raise XMPPError("bad-request", str(e))
88
92
  except TwoFactorNotRequired:
93
+ data = None
89
94
  if self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE:
90
95
  two_fa_needed = False
91
96
  else:
92
97
  raise
93
98
 
94
99
  user = GatewayUser(
95
- bare_jid=ifrom.bare,
96
- registration_form=form_values,
97
- registration_date=datetime.now(),
100
+ jid=ifrom.bare,
101
+ legacy_module_data=form_values if data is None else data,
98
102
  )
99
103
 
100
104
  if self.xmpp.REGISTRATION_TYPE == RegistrationType.SINGLE_STEP_FORM or (
101
105
  self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE
102
106
  and not two_fa_needed
103
107
  ):
104
- return self._finalize(user)
108
+ return await self.preferences(user)
105
109
 
106
110
  if self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE:
107
111
  return Form(
@@ -113,7 +117,7 @@ class Register(Command):
113
117
 
114
118
  elif self.xmpp.REGISTRATION_TYPE == RegistrationType.QRCODE:
115
119
  self.xmpp.qr_pending_registrations[ # type:ignore
116
- user.bare_jid
120
+ user.jid.bare
117
121
  ] = (
118
122
  self.xmpp.loop.create_future()
119
123
  )
@@ -159,13 +163,15 @@ class Register(Command):
159
163
  self, form_values: FormValues, _session, _ifrom, user: GatewayUser
160
164
  ):
161
165
  assert isinstance(form_values["code"], str)
162
- await self.xmpp.validate_two_factor_code(user, form_values["code"])
163
- return self._finalize(user)
166
+ data = await self.xmpp.validate_two_factor_code(user, form_values["code"])
167
+ if data is not None:
168
+ user.legacy_module_data.update(data)
169
+ return await self.preferences(user)
164
170
 
165
171
  async def qr(self, _form_values: FormValues, _session, _ifrom, user: GatewayUser):
166
172
  try:
167
- await asyncio.wait_for(
168
- self.xmpp.qr_pending_registrations[user.bare_jid], # type:ignore
173
+ data = await asyncio.wait_for(
174
+ self.xmpp.qr_pending_registrations[user.jid.bare], # type:ignore
169
175
  config.QR_TIMEOUT,
170
176
  )
171
177
  except asyncio.TimeoutError:
@@ -176,4 +182,14 @@ class Register(Command):
176
182
  "or you took too much time"
177
183
  ),
178
184
  )
179
- return self._finalize(user)
185
+ if data is not None:
186
+ user.legacy_module_data.update(data)
187
+ return await self.preferences(user)
188
+
189
+ async def preferences(self, user: GatewayUser) -> Form:
190
+ return Form(
191
+ title="Preferences",
192
+ instructions=Preferences.HELP,
193
+ fields=self.xmpp.PREFERENCES,
194
+ handler=functools.partial(self._finalize, user=user), # type:ignore
195
+ )
@@ -1,10 +1,12 @@
1
1
  # Commands available to users
2
+ from copy import deepcopy
2
3
  from typing import TYPE_CHECKING, Any, Optional, Union, cast
3
4
 
4
5
  from slixmpp import JID # type:ignore[attr-defined]
5
6
  from slixmpp.exceptions import XMPPError
6
7
 
7
- from ..util.types import AnyBaseSession, LegacyGroupIdType
8
+ from ..group.room import LegacyMUC
9
+ from ..util.types import AnyBaseSession, LegacyGroupIdType, UserPreferences
8
10
  from .base import (
9
11
  Command,
10
12
  CommandAccess,
@@ -76,7 +78,7 @@ class SyncContacts(Command):
76
78
  async def sync(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str:
77
79
  if session is None:
78
80
  raise RuntimeError
79
- roster_iq = await self.xmpp["xep_0356"].get_roster(session.user.bare_jid)
81
+ roster_iq = await self.xmpp["xep_0356"].get_roster(session.user_jid.bare)
80
82
 
81
83
  contacts = session.contacts.known_contacts()
82
84
 
@@ -90,13 +92,13 @@ class SyncContacts(Command):
90
92
  if contact is None:
91
93
  if len(groups) == 1:
92
94
  await self.xmpp["xep_0356"].set_roster(
93
- session.user.jid, {item["jid"]: {"subscription": "remove"}}
95
+ session.user_jid, {item["jid"]: {"subscription": "remove"}}
94
96
  )
95
97
  removed += 1
96
98
  else:
97
99
  groups.remove(self.xmpp.ROSTER_GROUP)
98
100
  await self.xmpp["xep_0356"].set_roster(
99
- session.user.jid,
101
+ session.user_jid,
100
102
  {
101
103
  item["jid"]: {
102
104
  "subscription": item["subscription"],
@@ -229,6 +231,39 @@ class CreateGroup(Command):
229
231
  )
230
232
 
231
233
 
234
+ class Preferences(Command):
235
+ NAME = "⚙️ Preferences"
236
+ HELP = "Customize the gateway behaviour to your liking"
237
+ NODE = CHAT_COMMAND = "preferences"
238
+ ACCESS = CommandAccess.USER
239
+
240
+ async def run(
241
+ self, session: Optional[AnyBaseSession], _ifrom: JID, *_: Any
242
+ ) -> Form:
243
+ fields = deepcopy(self.xmpp.PREFERENCES)
244
+ assert session is not None
245
+ current = session.user.preferences
246
+ for field in fields:
247
+ field.value = current.get(field.var) # type:ignore
248
+ return Form(
249
+ title="Preferences",
250
+ instructions=self.HELP,
251
+ fields=fields,
252
+ handler=self.finish, # type:ignore
253
+ )
254
+
255
+ async def finish(
256
+ self, form_values: UserPreferences, session: Optional[AnyBaseSession], *_
257
+ ) -> str:
258
+ assert session is not None
259
+ user = session.user
260
+ user.preferences.update(form_values) # type:ignore
261
+ self.xmpp.store.users.update(user)
262
+ if form_values["sync_avatar"]:
263
+ await self.xmpp.fetch_user_avatar(session)
264
+ return "Your preferences have been updated."
265
+
266
+
232
267
  class Unregister(Command):
233
268
  NAME = "❌ Unregister from the gateway"
234
269
  HELP = "Unregister from the gateway"
@@ -246,5 +281,50 @@ class Unregister(Command):
246
281
 
247
282
  async def unregister(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str:
248
283
  assert session is not None
249
- await self.xmpp.unregister_user(session.user)
284
+ user = self.xmpp.store.users.get(session.user_jid)
285
+ assert user is not None
286
+ await self.xmpp.unregister_user(user)
250
287
  return "OK"
288
+
289
+
290
+ class LeaveGroup(Command):
291
+ NAME = HELP = "❌ Leave a legacy group"
292
+ NODE = CHAT_COMMAND = "leave-group"
293
+ ACCESS = CommandAccess.USER_LOGGED
294
+ CATEGORY = GROUPS
295
+
296
+ async def run(self, session, _ifrom, *_):
297
+ assert session is not None
298
+ await session.bookmarks.fill()
299
+ groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold())
300
+ return Form(
301
+ title="Leave a group",
302
+ instructions="Select the group you want to leave",
303
+ fields=[
304
+ FormField(
305
+ "group",
306
+ "Group name",
307
+ options=[{"label": g.name, "value": g.name} for g in groups],
308
+ )
309
+ ],
310
+ handler=self.confirm, # type:ignore
311
+ handler_args=(groups,),
312
+ )
313
+
314
+ async def confirm(
315
+ self,
316
+ form_values: FormValues,
317
+ _session: AnyBaseSession,
318
+ _ifrom,
319
+ groups: list[LegacyMUC],
320
+ ):
321
+ group = groups[int(form_values["group"])] # type:ignore
322
+ return Confirmation(
323
+ prompt=f"Are you sure you want to leave the group '{group.name}'?",
324
+ handler=self.finish, # type:ignore
325
+ handler_args=(group,),
326
+ )
327
+
328
+ @staticmethod
329
+ async def finish(session: AnyBaseSession, _ifrom, group: LegacyMUC):
330
+ await session.on_leave_group(group.legacy_id)
@@ -2,7 +2,7 @@ import datetime
2
2
  import logging
3
3
  import warnings
4
4
  from datetime import date
5
- from typing import TYPE_CHECKING, Generic, Iterable, Optional, Union
5
+ from typing import TYPE_CHECKING, Generic, Iterable, Optional, Self, Union
6
6
 
7
7
  from slixmpp import JID, Message, Presence
8
8
  from slixmpp.exceptions import IqError
@@ -10,10 +10,10 @@ from slixmpp.plugins.xep_0292.stanza import VCard4
10
10
  from slixmpp.types import MessageTypes
11
11
 
12
12
  from ..core import config
13
- from ..core.mixins import FullCarbonMixin
14
- from ..core.mixins.avatar import AvatarMixin
13
+ from ..core.mixins import AvatarMixin, FullCarbonMixin, StoredAttributeMixin
15
14
  from ..core.mixins.disco import ContactAccountDiscoMixin
16
15
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
16
+ from ..db.models import Contact
17
17
  from ..util import SubclassableOnce
18
18
  from ..util.types import LegacyUserIdType, MessageOrPresenceTypeVar
19
19
 
@@ -24,6 +24,7 @@ if TYPE_CHECKING:
24
24
 
25
25
  class LegacyContact(
26
26
  Generic[LegacyUserIdType],
27
+ StoredAttributeMixin,
27
28
  AvatarMixin,
28
29
  ContactAccountDiscoMixin,
29
30
  FullCarbonMixin,
@@ -100,7 +101,6 @@ class LegacyContact(
100
101
  """
101
102
  super().__init__()
102
103
  self.session = session
103
- self.user = session.user
104
104
  self.legacy_id: LegacyUserIdType = legacy_id
105
105
  """
106
106
  The legacy identifier of the :term:`Legacy Contact`.
@@ -116,16 +116,40 @@ class LegacyContact(
116
116
 
117
117
  self._name: Optional[str] = None
118
118
 
119
- if self.xmpp.MARK_ALL_MESSAGES:
120
- self._sent_order = list[str]()
121
-
122
119
  self.xmpp = session.xmpp
123
120
  self.jid = JID(self.jid_username + "@" + self.xmpp.boundjid.bare)
124
121
  self.jid.resource = self.RESOURCE
125
- self.log = logging.getLogger(f"{self.user.bare_jid}:{self.jid.bare}")
126
- self.participants = set["LegacyParticipant"]()
127
- self.is_friend: bool = False
128
- self.__added_to_roster = False
122
+ self.log = logging.getLogger(f"{self.user_jid.bare}:{self.jid.bare}")
123
+ self._is_friend: bool = False
124
+ self.added_to_roster = False
125
+
126
+ @property
127
+ def is_friend(self):
128
+ return self._is_friend
129
+
130
+ @is_friend.setter
131
+ def is_friend(self, value: bool):
132
+ if value == self._is_friend:
133
+ return
134
+ self._is_friend = value
135
+ assert self.contact_pk is not None
136
+ self.xmpp.store.contacts.set_friend(self.contact_pk, value)
137
+
138
+ @property
139
+ def participants(self) -> list["LegacyParticipant"]:
140
+ assert self.contact_pk is not None
141
+ from ..group.participant import LegacyParticipant
142
+
143
+ return [
144
+ LegacyParticipant.get_self_or_unique_subclass().from_store(
145
+ self.session, stored, contact=self
146
+ )
147
+ for stored in self.xmpp.store.participants.get_for_contact(self.contact_pk)
148
+ ]
149
+
150
+ @property
151
+ def user_jid(self):
152
+ return self.session.user_jid
129
153
 
130
154
  def __repr__(self):
131
155
  return f"<Contact '{self.legacy_id}'/'{self.jid.bare}'>"
@@ -170,7 +194,7 @@ class LegacyContact(
170
194
  ) -> MessageOrPresenceTypeVar:
171
195
  if carbon and isinstance(stanza, Message):
172
196
  stanza["to"] = self.jid.bare
173
- stanza["from"] = self.user.jid
197
+ stanza["from"] = self.user_jid
174
198
  self._privileged_send(stanza)
175
199
  return stanza # type:ignore
176
200
 
@@ -186,12 +210,13 @@ class LegacyContact(
186
210
  n["nick"] = self.name
187
211
  stanza.append(n)
188
212
  if self.xmpp.MARK_ALL_MESSAGES and is_markable(stanza):
189
- self._sent_order.append(stanza["id"])
190
- stanza["to"] = self.user.jid
213
+ assert self.contact_pk is not None
214
+ self.xmpp.store.contacts.add_to_sent(self.contact_pk, stanza["id"])
215
+ stanza["to"] = self.user_jid
191
216
  stanza.send()
192
217
  return stanza
193
218
 
194
- def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str):
219
+ def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str) -> list[str]:
195
220
  """
196
221
  Return XMPP msg ids sent by this contact up to a given XMPP msg id.
197
222
 
@@ -205,15 +230,8 @@ class LegacyContact(
205
230
  :param horizon_xmpp_id: The latest message
206
231
  :return: A list of XMPP ids or None if horizon_xmpp_id was not found
207
232
  """
208
- for i, xmpp_id in enumerate(self._sent_order):
209
- if xmpp_id == horizon_xmpp_id:
210
- break
211
- else:
212
- return
213
- i += 1
214
- res = self._sent_order[:i]
215
- self._sent_order = self._sent_order[i:]
216
- return res
233
+ assert self.contact_pk is not None
234
+ return self.xmpp.store.contacts.pop_sent_up_to(self.contact_pk, horizon_xmpp_id)
217
235
 
218
236
  @property
219
237
  def name(self):
@@ -229,9 +247,19 @@ class LegacyContact(
229
247
  for p in self.participants:
230
248
  p.nickname = n
231
249
  self._name = n
232
- self.xmpp.pubsub.set_nick(user=self.user, jid=self.jid.bare, nick=n)
250
+ assert self.contact_pk is not None
251
+ self.xmpp.store.contacts.update_nick(self.contact_pk, n)
252
+ self.xmpp.pubsub.broadcast_nick(
253
+ user_jid=self.user_jid, jid=self.jid.bare, nick=n
254
+ )
255
+
256
+ def _get_cached_avatar_id(self):
257
+ assert self.contact_pk is not None
258
+ return self.xmpp.store.contacts.get_avatar_legacy_id(self.contact_pk)
233
259
 
234
260
  def _post_avatar_update(self):
261
+ assert self.contact_pk is not None
262
+ self.xmpp.store.contacts.set_avatar(self.contact_pk, self._avatar_pk)
235
263
  for p in self.participants:
236
264
  self.log.debug("Propagating new avatar to %s", p.muc)
237
265
  p.send_last_presence(force=True, no_cache_online=True)
@@ -283,7 +311,7 @@ class LegacyContact(
283
311
  elif country:
284
312
  vcard.add_address(country, locality)
285
313
 
286
- self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user.jid.bare})
314
+ self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user_jid.bare})
287
315
 
288
316
  async def add_to_roster(self, force=False):
289
317
  """
@@ -291,7 +319,7 @@ class LegacyContact(
291
319
 
292
320
  :param force: add even if the contact was already added successfully
293
321
  """
294
- if self.__added_to_roster and not force:
322
+ if self.added_to_roster and not force:
295
323
  return
296
324
  if config.NO_ROSTER_PUSH:
297
325
  log.debug("Roster push request by plugin ignored (--no-roster-push)")
@@ -303,7 +331,7 @@ class LegacyContact(
303
331
  if (n := self.name) is not None:
304
332
  item["name"] = n
305
333
  kw = dict(
306
- jid=self.user.jid,
334
+ jid=self.user_jid,
307
335
  roster_items={self.jid.bare: item},
308
336
  )
309
337
  try:
@@ -325,12 +353,26 @@ class LegacyContact(
325
353
  else:
326
354
  # we only broadcast pubsub events for contacts added to the roster
327
355
  # so if something was set before, we need to push it now
328
- self.__added_to_roster = True
356
+ self.added_to_roster = True
329
357
  self.session.create_task(self.__broadcast_pubsub_items())
330
358
  self.send_last_presence()
331
359
 
332
360
  async def __broadcast_pubsub_items(self):
333
- await self.xmpp.pubsub.broadcast_all(JID(self.jid.bare), self.user.jid)
361
+ cached_avatar = self.get_cached_avatar()
362
+ if cached_avatar is not None:
363
+ await self.xmpp.pubsub.broadcast_avatar(
364
+ self.jid.bare, self.session.user_jid, cached_avatar
365
+ )
366
+ nick = self.name
367
+ from ..core.pubsub import PepNick
368
+
369
+ if nick is not None:
370
+ pep_nick = PepNick(nick)
371
+ await self.xmpp.pubsub.broadcast(
372
+ pep_nick.nick,
373
+ self.jid.bare,
374
+ self.session.user_jid,
375
+ )
334
376
 
335
377
  async def _set_roster(self, **kw):
336
378
  try:
@@ -350,6 +392,8 @@ class LegacyContact(
350
392
  :param text: Optional message from the friend to the user
351
393
  """
352
394
  self.is_friend = True
395
+ assert self.contact_pk is not None
396
+ self.xmpp.store.contacts.set_friend(self.contact_pk, True)
353
397
  self.log.debug("Accepting friend request")
354
398
  presence = self._make_presence(ptype="subscribed", pstatus=text, bare=True)
355
399
  self._send(presence, nick=True)
@@ -415,7 +459,7 @@ class LegacyContact(
415
459
  their 'friends'".
416
460
  """
417
461
  for ptype in "unsubscribe", "unsubscribed", "unavailable":
418
- self.xmpp.send_presence(pfrom=self.jid, pto=self.user.jid.bare, ptype=ptype) # type: ignore
462
+ self.xmpp.send_presence(pfrom=self.jid, pto=self.user_jid.bare, ptype=ptype) # type: ignore
419
463
 
420
464
  async def update_info(self):
421
465
  """
@@ -442,6 +486,24 @@ class LegacyContact(
442
486
  """
443
487
  pass
444
488
 
489
+ @classmethod
490
+ def from_store(cls, session, stored: Contact, *args, **kwargs) -> Self:
491
+ contact = cls(
492
+ session,
493
+ cls.xmpp.LEGACY_CONTACT_ID_TYPE(stored.legacy_id),
494
+ stored.jid.username, # type: ignore
495
+ *args, # type: ignore
496
+ **kwargs, # type: ignore
497
+ )
498
+ contact.contact_pk = stored.id
499
+ contact._name = stored.nick
500
+ contact._is_friend = stored.is_friend
501
+ contact.added_to_roster = stored.added_to_roster
502
+ if (data := stored.extra_attributes) is not None:
503
+ contact.deserialize_extra_attributes(data)
504
+ contact._set_avatar_from_store(stored)
505
+ return contact
506
+
445
507
 
446
508
  def is_markable(stanza: Union[Message, Presence]):
447
509
  if isinstance(stanza, Presence):