slidge 0.1.2__py3-none-any.whl → 0.2.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 (63) 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 +5 -6
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -5
  9. slidge/contact/contact.py +93 -31
  10. slidge/contact/roster.py +54 -39
  11. slidge/core/config.py +13 -7
  12. slidge/core/gateway/base.py +139 -34
  13. slidge/core/gateway/disco.py +2 -4
  14. slidge/core/gateway/mam.py +1 -4
  15. slidge/core/gateway/ping.py +2 -3
  16. slidge/core/gateway/presence.py +1 -1
  17. slidge/core/gateway/registration.py +32 -21
  18. slidge/core/gateway/search.py +3 -5
  19. slidge/core/gateway/session_dispatcher.py +109 -51
  20. slidge/core/gateway/vcard_temp.py +6 -4
  21. slidge/core/mixins/__init__.py +11 -1
  22. slidge/core/mixins/attachment.py +15 -10
  23. slidge/core/mixins/avatar.py +66 -18
  24. slidge/core/mixins/base.py +8 -2
  25. slidge/core/mixins/message.py +11 -7
  26. slidge/core/mixins/message_maker.py +17 -9
  27. slidge/core/mixins/presence.py +14 -4
  28. slidge/core/pubsub.py +54 -212
  29. slidge/core/session.py +65 -33
  30. slidge/db/__init__.py +4 -0
  31. slidge/db/alembic/env.py +64 -0
  32. slidge/db/alembic/script.py.mako +26 -0
  33. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  34. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  35. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  36. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +76 -0
  37. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  38. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  39. slidge/db/avatar.py +224 -0
  40. slidge/db/meta.py +65 -0
  41. slidge/db/models.py +365 -0
  42. slidge/db/store.py +976 -0
  43. slidge/group/archive.py +13 -14
  44. slidge/group/bookmarks.py +59 -56
  45. slidge/group/participant.py +81 -29
  46. slidge/group/room.py +242 -142
  47. slidge/main.py +201 -0
  48. slidge/migration.py +30 -0
  49. slidge/slixfix/__init__.py +35 -2
  50. slidge/slixfix/roster.py +11 -4
  51. slidge/slixfix/xep_0292/vcard4.py +1 -0
  52. slidge/util/db.py +1 -47
  53. slidge/util/test.py +21 -4
  54. slidge/util/types.py +24 -4
  55. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/METADATA +3 -1
  56. slidge-0.2.0a0.dist-info/RECORD +108 -0
  57. slidge/core/cache.py +0 -183
  58. slidge/util/schema.sql +0 -126
  59. slidge/util/sql.py +0 -508
  60. slidge-0.1.2.dist-info/RECORD +0 -96
  61. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/LICENSE +0 -0
  62. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/WHEEL +0 -0
  63. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/entry_points.txt +0 -0
slidge/__init__.py CHANGED
@@ -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
 
slidge/__main__.py CHANGED
@@ -1,198 +1,4 @@
1
- """
2
- Slidge can be configured via CLI args, environment variables and/or INI files.
3
-
4
- To use env vars, use this convention: ``--home-dir`` becomes ``HOME_DIR``.
5
-
6
- Everything in ``/etc/slidge/conf.d/*`` is automatically used.
7
- To use a plugin-specific INI file, put it in another dir,
8
- and launch slidge with ``-c /path/to/plugin-specific.conf``.
9
- Use the long version of the CLI arg without the double dash prefix inside this
10
- INI file, eg ``debug=true``.
11
-
12
- An example configuration file is available at
13
- https://git.sr.ht/~nicoco/slidge/tree/master/item/dev/confs/slidge-example.ini
14
- """
15
-
16
- import asyncio
17
- import importlib
18
- import logging
19
- import os
20
- import signal
21
- from pathlib import Path
22
-
23
- import configargparse
24
-
25
- from slidge import BaseGateway
26
- from slidge.core import config
27
- from slidge.core.cache import avatar_cache
28
- from slidge.migration import migrate
29
- from slidge.util.conf import ConfigModule
30
- from slidge.util.db import user_store
31
- from slidge.util.util import get_version # noqa: F401
32
-
33
-
34
- class MainConfig(ConfigModule):
35
- def update_dynamic_defaults(self, args):
36
- # force=True is needed in case we call a logger before this is reached,
37
- # or basicConfig has no effect
38
- logging.basicConfig(
39
- level=args.loglevel,
40
- filename=args.log_file,
41
- force=True,
42
- format=args.log_format,
43
- )
44
-
45
- if args.home_dir is None:
46
- args.home_dir = Path("/var/lib/slidge") / str(args.jid)
47
-
48
- if args.user_jid_validator is None:
49
- args.user_jid_validator = ".*@" + args.server
50
-
51
-
52
- class SigTermInterrupt(Exception):
53
- pass
54
-
55
-
56
- def get_configurator():
57
- p = configargparse.ArgumentParser(
58
- default_config_files=os.getenv(
59
- "SLIDGE_CONF_DIR", "/etc/slidge/conf.d/*.conf"
60
- ).split(":"),
61
- description=__doc__,
62
- )
63
- p.add_argument(
64
- "-c",
65
- "--config",
66
- help="Path to a INI config file.",
67
- env_var="SLIDGE_CONFIG",
68
- is_config_file=True,
69
- )
70
- p.add_argument(
71
- "-q",
72
- "--quiet",
73
- help="loglevel=WARNING",
74
- action="store_const",
75
- dest="loglevel",
76
- const=logging.WARNING,
77
- default=logging.INFO,
78
- env_var="SLIDGE_QUIET",
79
- )
80
- p.add_argument(
81
- "-d",
82
- "--debug",
83
- help="loglevel=DEBUG",
84
- action="store_const",
85
- dest="loglevel",
86
- const=logging.DEBUG,
87
- env_var="SLIDGE_DEBUG",
88
- )
89
- p.add_argument(
90
- "--version",
91
- action="version",
92
- version=f"%(prog)s {__version__}",
93
- )
94
- configurator = MainConfig(config, p)
95
- return configurator
96
-
97
-
98
- def get_parser():
99
- return get_configurator().parser
100
-
101
-
102
- def configure():
103
- configurator = get_configurator()
104
- args, unknown_argv = configurator.set_conf()
105
-
106
- if not (h := config.HOME_DIR).exists():
107
- logging.info("Creating directory '%s'", h)
108
- h.mkdir()
109
-
110
- db_file = config.HOME_DIR / "slidge.db"
111
- user_store.set_file(db_file, args.secret_key)
112
-
113
- avatar_cache.set_dir(h / "slidge_avatars_v2")
114
-
115
- config.UPLOAD_REQUESTER = config.UPLOAD_REQUESTER or config.JID.bare
116
-
117
- return unknown_argv
118
-
119
-
120
- def handle_sigterm(_signum, _frame):
121
- logging.info("Caught SIGTERM")
122
- raise SigTermInterrupt
123
-
124
-
125
- def main():
126
- signal.signal(signal.SIGTERM, handle_sigterm)
127
-
128
- unknown_argv = configure()
129
- logging.info("Starting slidge version %s", __version__)
130
-
131
- legacy_module = importlib.import_module(config.LEGACY_MODULE)
132
- logging.debug("Legacy module: %s", dir(legacy_module))
133
- logging.info(
134
- "Starting legacy module: '%s' version %s",
135
- config.LEGACY_MODULE,
136
- getattr(legacy_module, "__version__", "No version"),
137
- )
138
-
139
- if plugin_config_obj := getattr(
140
- legacy_module, "config", getattr(legacy_module, "Config", None)
141
- ):
142
- logging.debug("Found a config object in plugin: %r", plugin_config_obj)
143
- ConfigModule.ENV_VAR_PREFIX += (
144
- f"_{config.LEGACY_MODULE.split('.')[-1].upper()}_"
145
- )
146
- logging.debug("Env var prefix: %s", ConfigModule.ENV_VAR_PREFIX)
147
- ConfigModule(plugin_config_obj).set_conf(unknown_argv)
148
- else:
149
- if unknown_argv:
150
- raise RuntimeError("Some arguments have not been recognized", unknown_argv)
151
-
152
- migrate()
153
-
154
- gateway: BaseGateway = BaseGateway.get_unique_subclass()()
155
- avatar_cache.http = gateway.http
156
- gateway.connect()
157
-
158
- return_code = 0
159
- try:
160
- gateway.loop.run_forever()
161
- except KeyboardInterrupt:
162
- logging.debug("Received SIGINT")
163
- except SigTermInterrupt:
164
- logging.debug("Received SIGTERM")
165
- except SystemExit as e:
166
- return_code = e.code # type: ignore
167
- logging.debug("Exit called")
168
- except Exception as e:
169
- return_code = 2
170
- logging.exception("Exception in __main__")
171
- logging.exception(e)
172
- finally:
173
- if gateway.has_crashed:
174
- if return_code != 0:
175
- logging.warning("Return code has been set twice. Please report this.")
176
- return_code = 3
177
- if gateway.is_connected():
178
- logging.debug("Gateway is connected, cleaning up")
179
- gateway.loop.run_until_complete(asyncio.gather(*gateway.shutdown()))
180
- gateway.disconnect()
181
- gateway.loop.run_until_complete(gateway.disconnected)
182
- else:
183
- logging.debug("Gateway is not connected, no need to clean up")
184
- user_store.close()
185
- avatar_cache.close()
186
- gateway.loop.run_until_complete(gateway.http.close())
187
- logging.info("Successful clean shut down")
188
- logging.debug("Exiting with code %s", return_code)
189
- exit(return_code)
190
-
191
-
192
- # this should be modified before publish, but if someone cloned from the repo,
193
- # it can help
194
- __version__ = get_version()
195
-
196
-
197
1
  if __name__ == "__main__":
2
+ from slidge.main import main
3
+
198
4
  main()
slidge/__version__.py ADDED
@@ -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"
slidge/command/adhoc.py CHANGED
@@ -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(
slidge/command/admin.py CHANGED
@@ -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)
slidge/command/base.py CHANGED
@@ -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
+ )
slidge/command/user.py CHANGED
@@ -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)