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
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.0alpha1"
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,9 +53,9 @@ 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
- start = self.xmpp.datetime_started
58
+ start = self.xmpp.datetime_started # type:ignore
60
59
  uptime = datetime.now() - start
61
60
 
62
61
  if uptime.days:
@@ -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"],
@@ -129,7 +131,6 @@ class ListContacts(Command):
129
131
  self, session: Optional[AnyBaseSession], _ifrom: JID, *_
130
132
  ) -> TableResult:
131
133
  assert session is not None
132
- await session.contacts.fill()
133
134
  contacts = sorted(
134
135
  session.contacts, key=lambda c: c.name.casefold() if c.name else ""
135
136
  )
@@ -229,6 +230,39 @@ class CreateGroup(Command):
229
230
  )
230
231
 
231
232
 
233
+ class Preferences(Command):
234
+ NAME = "⚙️ Preferences"
235
+ HELP = "Customize the gateway behaviour to your liking"
236
+ NODE = CHAT_COMMAND = "preferences"
237
+ ACCESS = CommandAccess.USER
238
+
239
+ async def run(
240
+ self, session: Optional[AnyBaseSession], _ifrom: JID, *_: Any
241
+ ) -> Form:
242
+ fields = deepcopy(self.xmpp.PREFERENCES)
243
+ assert session is not None
244
+ current = session.user.preferences
245
+ for field in fields:
246
+ field.value = current.get(field.var) # type:ignore
247
+ return Form(
248
+ title="Preferences",
249
+ instructions=self.HELP,
250
+ fields=fields,
251
+ handler=self.finish, # type:ignore
252
+ )
253
+
254
+ async def finish(
255
+ self, form_values: UserPreferences, session: Optional[AnyBaseSession], *_
256
+ ) -> str:
257
+ assert session is not None
258
+ user = session.user
259
+ user.preferences.update(form_values) # type:ignore
260
+ self.xmpp.store.users.update(user)
261
+ if form_values["sync_avatar"]:
262
+ await self.xmpp.fetch_user_avatar(session)
263
+ return "Your preferences have been updated."
264
+
265
+
232
266
  class Unregister(Command):
233
267
  NAME = "❌ Unregister from the gateway"
234
268
  HELP = "Unregister from the gateway"
@@ -246,5 +280,50 @@ class Unregister(Command):
246
280
 
247
281
  async def unregister(self, session: Optional[AnyBaseSession], _ifrom: JID) -> str:
248
282
  assert session is not None
249
- await self.xmpp.unregister_user(session.user)
283
+ user = self.xmpp.store.users.get(session.user_jid)
284
+ assert user is not None
285
+ await self.xmpp.unregister_user(user)
250
286
  return "OK"
287
+
288
+
289
+ class LeaveGroup(Command):
290
+ NAME = HELP = "❌ Leave a legacy group"
291
+ NODE = CHAT_COMMAND = "leave-group"
292
+ ACCESS = CommandAccess.USER_LOGGED
293
+ CATEGORY = GROUPS
294
+
295
+ async def run(self, session, _ifrom, *_):
296
+ assert session is not None
297
+ await session.bookmarks.fill()
298
+ groups = sorted(session.bookmarks, key=lambda g: g.DISCO_NAME.casefold())
299
+ return Form(
300
+ title="Leave a group",
301
+ instructions="Select the group you want to leave",
302
+ fields=[
303
+ FormField(
304
+ "group",
305
+ "Group name",
306
+ options=[{"label": g.name, "value": g.name} for g in groups],
307
+ )
308
+ ],
309
+ handler=self.confirm, # type:ignore
310
+ handler_args=(groups,),
311
+ )
312
+
313
+ async def confirm(
314
+ self,
315
+ form_values: FormValues,
316
+ _session: AnyBaseSession,
317
+ _ifrom,
318
+ groups: list[LegacyMUC],
319
+ ):
320
+ group = groups[int(form_values["group"])] # type:ignore
321
+ return Confirmation(
322
+ prompt=f"Are you sure you want to leave the group '{group.name}'?",
323
+ handler=self.finish, # type:ignore
324
+ handler_args=(group,),
325
+ )
326
+
327
+ @staticmethod
328
+ async def finish(session: AnyBaseSession, _ifrom, group: LegacyMUC):
329
+ await session.on_leave_group(group.legacy_id)