slidge 0.1.3__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.
- slidge/__init__.py +3 -5
- slidge/__main__.py +2 -197
- slidge/__version__.py +5 -0
- slidge/command/adhoc.py +40 -17
- slidge/command/admin.py +24 -12
- slidge/command/base.py +10 -8
- slidge/command/categories.py +13 -3
- slidge/command/chat_command.py +29 -2
- slidge/command/register.py +32 -16
- slidge/command/user.py +106 -13
- slidge/contact/contact.py +254 -50
- slidge/contact/roster.py +124 -53
- slidge/core/config.py +19 -13
- slidge/core/dispatcher/__init__.py +3 -0
- slidge/core/{gateway → dispatcher}/caps.py +12 -8
- slidge/core/{gateway → dispatcher}/disco.py +10 -18
- slidge/core/dispatcher/message/__init__.py +10 -0
- slidge/core/dispatcher/message/chat_state.py +40 -0
- slidge/core/dispatcher/message/marker.py +62 -0
- slidge/core/dispatcher/message/message.py +397 -0
- slidge/core/dispatcher/muc/__init__.py +12 -0
- slidge/core/dispatcher/muc/admin.py +98 -0
- slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
- slidge/core/dispatcher/muc/misc.py +121 -0
- slidge/core/dispatcher/muc/owner.py +96 -0
- slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
- slidge/core/dispatcher/presence.py +176 -0
- slidge/core/dispatcher/registration.py +85 -0
- slidge/core/{gateway → dispatcher}/search.py +9 -16
- slidge/core/dispatcher/session_dispatcher.py +84 -0
- slidge/core/dispatcher/util.py +174 -0
- slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
- slidge/core/{gateway/base.py → gateway.py} +176 -153
- slidge/core/mixins/__init__.py +11 -1
- slidge/core/mixins/attachment.py +106 -67
- slidge/core/mixins/avatar.py +94 -25
- slidge/core/mixins/base.py +10 -4
- slidge/core/mixins/db.py +18 -0
- slidge/core/mixins/disco.py +0 -10
- slidge/core/mixins/lock.py +10 -8
- slidge/core/mixins/message.py +11 -195
- slidge/core/mixins/message_maker.py +17 -9
- slidge/core/mixins/message_text.py +211 -0
- slidge/core/mixins/presence.py +17 -4
- slidge/core/pubsub.py +114 -288
- slidge/core/session.py +101 -40
- slidge/db/__init__.py +4 -0
- slidge/db/alembic/__init__.py +0 -0
- slidge/db/alembic/env.py +64 -0
- slidge/db/alembic/old_user_store.py +183 -0
- slidge/db/alembic/script.py.mako +26 -0
- slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
- slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
- slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
- slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
- slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
- slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
- slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
- slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
- slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
- slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
- slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
- slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
- slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
- slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
- slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
- slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
- slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
- slidge/db/avatar.py +205 -0
- slidge/db/meta.py +72 -0
- slidge/db/models.py +405 -0
- slidge/db/store.py +1257 -0
- slidge/group/archive.py +58 -14
- slidge/group/bookmarks.py +89 -65
- slidge/group/participant.py +107 -40
- slidge/group/room.py +402 -213
- slidge/main.py +202 -0
- slidge/migration.py +45 -1
- slidge/slixfix/__init__.py +31 -1
- slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
- slidge/slixfix/roster.py +13 -4
- slidge/slixfix/xep_0292/vcard4.py +1 -87
- slidge/util/archive_msg.py +2 -1
- slidge/util/db.py +4 -228
- slidge/util/test.py +91 -4
- slidge/util/types.py +39 -4
- slidge/util/util.py +45 -2
- {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
- slidge-0.2.0.dist-info/RECORD +131 -0
- slidge-0.2.0.dist-info/entry_points.txt +3 -0
- slidge/core/cache.py +0 -183
- slidge/core/gateway/__init__.py +0 -3
- slidge/core/gateway/muc_admin.py +0 -35
- slidge/core/gateway/presence.py +0 -95
- slidge/core/gateway/registration.py +0 -53
- slidge/core/gateway/session_dispatcher.py +0 -804
- slidge/util/schema.sql +0 -126
- slidge/util/sql.py +0 -508
- slidge-0.1.3.dist-info/RECORD +0 -96
- slidge-0.1.3.dist-info/entry_points.txt +0 -3
- {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
- {slidge-0.1.3.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 .
|
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
|
-
|
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
|
-
|
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
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
|
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:
|
52
|
+
self, category: CommandCategory, iq: Iq, adhoc_session: AdhocSessionType
|
50
53
|
) -> AdhocSessionType:
|
51
|
-
|
52
|
-
|
53
|
-
|
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.
|
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
|
-
{
|
71
|
-
|
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:
|
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[
|
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
|
208
|
-
|
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=
|
212
|
-
name=
|
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[
|
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 ..
|
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
|
-
|
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
|
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.
|
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
|
-
|
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
|
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}
|
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
|
-
|
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 =
|
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 =
|
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
|
-
|
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:
|
60
|
+
fields: Sequence["FormField"]
|
59
61
|
"""
|
60
62
|
The 'columns names' of the table.
|
61
63
|
"""
|
62
|
-
items:
|
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:
|
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 =
|
387
|
+
user = self.xmpp.store.users.get(jid)
|
386
388
|
if user is None:
|
387
389
|
return None
|
388
390
|
|
slidge/command/categories.py
CHANGED
@@ -1,3 +1,13 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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")
|
slidge/command/chat_command.py
CHANGED
@@ -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(
|
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(),
|
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)
|