slidge 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +111 -44
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -795
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.2.dist-info/RECORD +0 -96
  100. slidge-0.1.2.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/WHEEL +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,3 @@
1
- """
2
- Slidge can be configured via CLI args, environment variables and/or INI files.
1
+ from slidge.main import main
3
2
 
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
- if __name__ == "__main__":
198
- main()
3
+ 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.0"
slidge/command/adhoc.py CHANGED
@@ -9,11 +9,14 @@ from slixmpp.exceptions import XMPPError
9
9
  from slixmpp.plugins.xep_0004 import Form as SlixForm # type: ignore[attr-defined]
10
10
  from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
11
11
 
12
+ from ..core import config
13
+ from ..util.util import strip_leading_emoji
12
14
  from . import Command, CommandResponseType, Confirmation, Form, TableResult
13
15
  from .base import FormField
16
+ from .categories import CommandCategory
14
17
 
15
18
  if TYPE_CHECKING:
16
- from ..core.gateway.base import BaseGateway
19
+ from ..core.gateway import BaseGateway
17
20
  from ..core.session import BaseSession
18
21
 
19
22
 
@@ -46,20 +49,27 @@ class AdhocProvider:
46
49
  return await self.__handle_result(session, result, adhoc_session)
47
50
 
48
51
  async def __handle_category_list(
49
- self, category: str, iq: Iq, adhoc_session: AdhocSessionType
52
+ self, category: CommandCategory, iq: Iq, adhoc_session: AdhocSessionType
50
53
  ) -> AdhocSessionType:
51
- session = self.xmpp.get_session_from_stanza(iq)
52
- commands = []
53
- for command in self._categories[category]:
54
+ try:
55
+ session = self.xmpp.get_session_from_stanza(iq)
56
+ except XMPPError:
57
+ session = None
58
+ commands: dict[str, Command] = {}
59
+ for command in self._categories[category.node]:
54
60
  try:
55
61
  command.raise_if_not_authorized(iq.get_from())
56
62
  except XMPPError:
57
63
  continue
58
- commands.append(command)
64
+ commands[command.NODE] = command
65
+ if len(commands) == 0:
66
+ raise XMPPError(
67
+ "not-authorized", "There is no command you can run in this category"
68
+ )
59
69
  return await self.__handle_result(
60
70
  session,
61
71
  Form(
62
- category,
72
+ category.name,
63
73
  "",
64
74
  [
65
75
  FormField(
@@ -67,8 +77,11 @@ class AdhocProvider:
67
77
  label="Command",
68
78
  type="list-single",
69
79
  options=[
70
- {"label": command.NAME, "value": str(i)}
71
- for i, command in enumerate(commands)
80
+ {
81
+ "label": strip_leading_emoji_if_needed(command.NAME),
82
+ "value": command.NODE,
83
+ }
84
+ for command in commands.values()
72
85
  ],
73
86
  )
74
87
  ],
@@ -79,12 +92,12 @@ class AdhocProvider:
79
92
 
80
93
  async def __handle_category_choice(
81
94
  self,
82
- commands: list[Command],
95
+ commands: dict[str, Command],
83
96
  form_values: dict[str, str],
84
97
  session: "BaseSession[Any, Any]",
85
98
  jid: JID,
86
99
  ):
87
- command = commands[int(form_values["command"])]
100
+ command = commands[form_values["command"]]
88
101
  result = await self.__wrap_handler(command.run, session, jid)
89
102
  return result
90
103
 
@@ -200,19 +213,23 @@ class AdhocProvider:
200
213
  self.xmpp.plugin["xep_0050"].add_command( # type: ignore[no-untyped-call]
201
214
  jid=jid,
202
215
  node=command.NODE,
203
- name=command.NAME,
216
+ name=strip_leading_emoji_if_needed(command.NAME),
204
217
  handler=partial(self.__wrap_initial_handler, command),
205
218
  )
206
219
  else:
207
- if category not in self._categories:
208
- self._categories[category] = list[Command]()
220
+ if isinstance(category, str):
221
+ category = CommandCategory(category, category)
222
+ node = category.node
223
+ name = category.name
224
+ if node not in self._categories:
225
+ self._categories[node] = list[Command]()
209
226
  self.xmpp.plugin["xep_0050"].add_command( # type: ignore[no-untyped-call]
210
227
  jid=jid,
211
- node=category,
212
- name=category,
228
+ node=node,
229
+ name=strip_leading_emoji_if_needed(name),
213
230
  handler=partial(self.__handle_category_list, category),
214
231
  )
215
- self._categories[category].append(command)
232
+ self._categories[node].append(command)
216
233
 
217
234
  async def get_items(self, jid: JID, node: str, iq: Iq) -> DiscoItems:
218
235
  """
@@ -255,4 +272,10 @@ class AdhocProvider:
255
272
  return filtered_items
256
273
 
257
274
 
275
+ def strip_leading_emoji_if_needed(text: str) -> str:
276
+ if config.STRIP_LEADING_EMOJI_ADHOC:
277
+ return strip_leading_emoji(text)
278
+ return text
279
+
280
+
258
281
  log = logging.getLogger(__name__)
slidge/command/admin.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Commands only accessible for slidge admins
2
2
  import functools
3
+ import importlib
3
4
  import logging
4
5
  from datetime import datetime
5
6
  from typing import Any, Optional
@@ -7,9 +8,10 @@ from typing import Any, Optional
7
8
  from slixmpp import JID
8
9
  from slixmpp.exceptions import XMPPError
9
10
 
10
- from ..util.db import user_store
11
+ from ..core import config
11
12
  from ..util.types import AnyBaseSession
12
13
  from .base import (
14
+ NODE_PREFIX,
13
15
  Command,
14
16
  CommandAccess,
15
17
  Confirmation,
@@ -20,6 +22,8 @@ from .base import (
20
22
  )
21
23
  from .categories import ADMINISTRATION
22
24
 
25
+ NODE_PREFIX = NODE_PREFIX + "admin/"
26
+
23
27
 
24
28
  class AdminCommand(Command):
25
29
  ACCESS = CommandAccess.ADMIN_ONLY
@@ -29,17 +33,18 @@ class AdminCommand(Command):
29
33
  class ListUsers(AdminCommand):
30
34
  NAME = "👤 List registered users"
31
35
  HELP = "List the users registered to this gateway"
32
- NODE = CHAT_COMMAND = "list_users"
36
+ CHAT_COMMAND = "list_users"
37
+ NODE = NODE_PREFIX + CHAT_COMMAND
33
38
 
34
39
  async def run(self, _session, _ifrom, *_):
35
40
  items = []
36
- for u in user_store.get_all():
41
+ for u in self.xmpp.store.users.get_all():
37
42
  d = u.registration_date
38
43
  if d is None:
39
44
  joined = ""
40
45
  else:
41
46
  joined = d.isoformat(timespec="seconds")
42
- items.append({"jid": u.bare_jid, "joined": joined})
47
+ items.append({"jid": u.jid.bare, "joined": joined})
43
48
  return TableResult(
44
49
  description="List of registered users",
45
50
  fields=[FormField("jid", type="jid-single"), FormField("joined")],
@@ -50,13 +55,14 @@ class ListUsers(AdminCommand):
50
55
  class SlidgeInfo(AdminCommand):
51
56
  NAME = "ℹ️ Server information"
52
57
  HELP = "List the users registered to this gateway"
53
- NODE = CHAT_COMMAND = "info"
58
+ CHAT_COMMAND = "info"
59
+ NODE = NODE_PREFIX + CHAT_COMMAND
54
60
  ACCESS = CommandAccess.ANY
55
61
 
56
62
  async def run(self, _session, _ifrom, *_):
57
- from ..__main__ import __version__
63
+ from slidge.__version__ import __version__
58
64
 
59
- start = self.xmpp.datetime_started
65
+ start = self.xmpp.datetime_started # type:ignore
60
66
  uptime = datetime.now() - start
61
67
 
62
68
  if uptime.days:
@@ -91,8 +97,12 @@ class SlidgeInfo(AdminCommand):
91
97
  [a for a in (days_ago, hours_ago, minutes_ago, seconds_ago) if a]
92
98
  )
93
99
 
100
+ legacy_module = importlib.import_module(config.LEGACY_MODULE)
101
+ version = getattr(legacy_module, "__version__", "No version")
102
+
94
103
  return (
95
- f"{self.xmpp.COMPONENT_NAME} version {__version__}\n"
104
+ f"{self.xmpp.COMPONENT_NAME} (slidge core {__version__},"
105
+ f" {config.LEGACY_MODULE} {version})\n"
96
106
  f"Up since {start:%Y-%m-%d %H:%M} ({ago} ago)"
97
107
  )
98
108
 
@@ -100,7 +110,8 @@ class SlidgeInfo(AdminCommand):
100
110
  class DeleteUser(AdminCommand):
101
111
  NAME = "❌ Delete a user"
102
112
  HELP = "Unregister a user from the gateway"
103
- NODE = CHAT_COMMAND = "delete_user"
113
+ CHAT_COMMAND = "delete_user"
114
+ NODE = NODE_PREFIX + CHAT_COMMAND
104
115
 
105
116
  async def run(self, _session, _ifrom, *_):
106
117
  return Form(
@@ -114,7 +125,7 @@ class DeleteUser(AdminCommand):
114
125
  self, form_values: FormValues, _session: AnyBaseSession, _ifrom: JID
115
126
  ) -> Confirmation:
116
127
  jid: JID = form_values.get("jid") # type:ignore
117
- user = user_store.get_by_jid(jid)
128
+ user = self.xmpp.store.users.get(jid)
118
129
  if user is None:
119
130
  raise XMPPError("item-not-found", text=f"There is no user '{jid}'")
120
131
 
@@ -127,7 +138,7 @@ class DeleteUser(AdminCommand):
127
138
  async def finish(
128
139
  self, _session: Optional[AnyBaseSession], _ifrom: JID, jid: JID
129
140
  ) -> None:
130
- user = user_store.get_by_jid(jid)
141
+ user = self.xmpp.store.users.get(jid)
131
142
  if user is None:
132
143
  raise XMPPError("bad-request", f"{jid} has no account here!")
133
144
  await self.xmpp.unregister_user(user)
@@ -136,7 +147,8 @@ class DeleteUser(AdminCommand):
136
147
  class ChangeLoglevel(AdminCommand):
137
148
  NAME = "📋 Change the verbosity of the logs"
138
149
  HELP = "Set the logging level"
139
- NODE = CHAT_COMMAND = "loglevel"
150
+ CHAT_COMMAND = "loglevel"
151
+ NODE = NODE_PREFIX + CHAT_COMMAND
140
152
 
141
153
  async def run(self, _session, _ifrom, *_):
142
154
  return Form(
slidge/command/base.py CHANGED
@@ -6,9 +6,9 @@ from typing import (
6
6
  Any,
7
7
  Awaitable,
8
8
  Callable,
9
- Collection,
10
9
  Iterable,
11
10
  Optional,
11
+ Sequence,
12
12
  Type,
13
13
  TypedDict,
14
14
  Union,
@@ -23,12 +23,14 @@ 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
 
28
+ NODE_PREFIX = "https://slidge.im/command/core/"
29
+
29
30
  if TYPE_CHECKING:
30
31
  from ..core.gateway import BaseGateway
31
32
  from ..core.session import BaseSession
33
+ from .categories import CommandCategory
32
34
 
33
35
 
34
36
  HandlerType = Union[
@@ -55,11 +57,11 @@ class TableResult:
55
57
  Structured data as the result of a command
56
58
  """
57
59
 
58
- fields: Collection["FormField"]
60
+ fields: Sequence["FormField"]
59
61
  """
60
62
  The 'columns names' of the table.
61
63
  """
62
- items: Collection[dict[str, Union[str, JID]]]
64
+ items: Sequence[dict[str, Union[str, JID]]]
63
65
  """
64
66
  The rows of the table. Each row is a dict where keys are the fields ``var``
65
67
  attribute.
@@ -150,7 +152,7 @@ class Form:
150
152
 
151
153
  title: str
152
154
  instructions: str
153
- fields: Collection["FormField"]
155
+ fields: Sequence["FormField"]
154
156
  handler: FormHandlerType
155
157
  handler_args: Iterable[Any] = field(default_factory=list)
156
158
  handler_kwargs: dict[str, Any] = field(default_factory=dict)
@@ -179,8 +181,8 @@ class Form:
179
181
  """
180
182
  form = SlixForm() # type: ignore[no-untyped-call]
181
183
  form["type"] = "form"
182
- form["instructions"] = self.instructions
183
184
  form["title"] = self.title
185
+ form["instructions"] = self.instructions
184
186
  for fi in self.fields:
185
187
  form.append(fi.get_xml())
186
188
  return form
@@ -348,7 +350,7 @@ class Command(ABC):
348
350
  Who can use this command
349
351
  """
350
352
 
351
- CATEGORY: Optional[str] = None
353
+ CATEGORY: Optional[Union[str, "CommandCategory"]] = None
352
354
  """
353
355
  If used, the command will be under this top-level category.
354
356
  Use the same string for several commands to group them.
@@ -382,7 +384,7 @@ class Command(ABC):
382
384
  raise XMPPError("feature-not-implemented")
383
385
 
384
386
  def _get_session(self, jid: JID) -> Optional["BaseSession[Any, Any]"]:
385
- user = user_store.get_by_jid(jid)
387
+ user = self.xmpp.store.users.get(jid)
386
388
  if user is None:
387
389
  return None
388
390
 
@@ -1,3 +1,13 @@
1
- ADMINISTRATION = "🛷️ Slidge administration"
2
- CONTACTS = "👤 Contacts"
3
- GROUPS = "👥 Groups"
1
+ from typing import NamedTuple
2
+
3
+ from .base import NODE_PREFIX
4
+
5
+
6
+ class CommandCategory(NamedTuple):
7
+ name: str
8
+ node: str
9
+
10
+
11
+ ADMINISTRATION = CommandCategory("🛷️ Slidge administration", NODE_PREFIX + "admin")
12
+ CONTACTS = CommandCategory("👤 Contacts", NODE_PREFIX + "contacts")
13
+ GROUPS = CommandCategory("👥 Groups", NODE_PREFIX + "groups")
@@ -13,6 +13,7 @@ from slixmpp.exceptions import XMPPError
13
13
  from slixmpp.types import JidStr, MessageTypes
14
14
 
15
15
  from . import Command, CommandResponseType, Confirmation, Form, TableResult
16
+ from .categories import CommandCategory
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from ..core.gateway import BaseGateway
@@ -153,6 +154,9 @@ class ChatCommandProvider:
153
154
  self.xmpp.delivery_receipt.ack(msg)
154
155
  return await self._handle_result(result, msg, session)
155
156
 
157
+ def __make_uri(self, body: str) -> str:
158
+ return f"xmpp:{self.xmpp.boundjid.bare}?message;body={body}"
159
+
156
160
  async def _handle_result(self, result: CommandResponseType, msg: Message, session):
157
161
  if isinstance(result, str) or result is None:
158
162
  reply = msg.reply()
@@ -175,9 +179,14 @@ class ChatCommandProvider:
175
179
  ).send()
176
180
  if f.options:
177
181
  for o in f.options:
178
- msg.reply(f"{o['value']} -- {o['label']}").send()
182
+ msg.reply(
183
+ f"{o['label']}: {self.__make_uri(o['value'])}"
184
+ ).send()
179
185
  if f.value:
180
186
  msg.reply(f"Default: {f.value}").send()
187
+ if f.type == "boolean":
188
+ msg.reply("yes: " + self.__make_uri("yes")).send()
189
+ msg.reply("no: " + self.__make_uri("no")).send()
181
190
 
182
191
  ans = await self.xmpp.input(
183
192
  msg.get_from(), (f.label or f.var) + "? (or 'abort')"
@@ -186,6 +195,12 @@ class ChatCommandProvider:
186
195
  return await self._handle_result(
187
196
  "Command aborted", msg, session
188
197
  )
198
+ if f.type == "boolean":
199
+ if ans.lower() == "yes":
200
+ ans = "true"
201
+ else:
202
+ ans = "false"
203
+
189
204
  if f.type.endswith("multi"):
190
205
  form_values[f.var] = f.validate(ans.split(" "))
191
206
  else:
@@ -266,7 +281,19 @@ class ChatCommandProvider:
266
281
  def _help(self, mfrom: JID):
267
282
  msg = "Available commands:"
268
283
  for c in sorted(
269
- self._commands.values(), key=lambda co: (co.CATEGORY or "", co.CHAT_COMMAND)
284
+ self._commands.values(),
285
+ key=lambda co: (
286
+ (
287
+ co.CATEGORY
288
+ if isinstance(co.CATEGORY, str)
289
+ else (
290
+ co.CATEGORY.name
291
+ if isinstance(co.CATEGORY, CommandCategory)
292
+ else ""
293
+ )
294
+ ),
295
+ co.CHAT_COMMAND,
296
+ ),
270
297
  ):
271
298
  try:
272
299
  c.raise_if_not_authorized(mfrom)