slidge 0.1.0__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 (96) hide show
  1. slidge/__init__.py +61 -0
  2. slidge/__main__.py +192 -0
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +3 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +209 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +892 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +757 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +525 -0
  41. slidge/core/session.py +752 -0
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +440 -0
  46. slidge/group/room.py +1095 -0
  47. slidge/migration.py +18 -0
  48. slidge/py.typed +0 -0
  49. slidge/slixfix/__init__.py +68 -0
  50. slidge/slixfix/link_preview/__init__.py +10 -0
  51. slidge/slixfix/link_preview/link_preview.py +17 -0
  52. slidge/slixfix/link_preview/stanza.py +99 -0
  53. slidge/slixfix/roster.py +60 -0
  54. slidge/slixfix/xep_0077/__init__.py +10 -0
  55. slidge/slixfix/xep_0077/register.py +289 -0
  56. slidge/slixfix/xep_0077/stanza.py +104 -0
  57. slidge/slixfix/xep_0100/__init__.py +5 -0
  58. slidge/slixfix/xep_0100/gateway.py +121 -0
  59. slidge/slixfix/xep_0100/stanza.py +9 -0
  60. slidge/slixfix/xep_0153/__init__.py +10 -0
  61. slidge/slixfix/xep_0153/stanza.py +25 -0
  62. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  63. slidge/slixfix/xep_0264/__init__.py +5 -0
  64. slidge/slixfix/xep_0264/stanza.py +36 -0
  65. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  66. slidge/slixfix/xep_0292/__init__.py +5 -0
  67. slidge/slixfix/xep_0292/vcard4.py +100 -0
  68. slidge/slixfix/xep_0313/__init__.py +12 -0
  69. slidge/slixfix/xep_0313/mam.py +262 -0
  70. slidge/slixfix/xep_0313/stanza.py +359 -0
  71. slidge/slixfix/xep_0317/__init__.py +5 -0
  72. slidge/slixfix/xep_0317/hats.py +17 -0
  73. slidge/slixfix/xep_0317/stanza.py +28 -0
  74. slidge/slixfix/xep_0356_old/__init__.py +7 -0
  75. slidge/slixfix/xep_0356_old/privilege.py +167 -0
  76. slidge/slixfix/xep_0356_old/stanza.py +44 -0
  77. slidge/slixfix/xep_0424/__init__.py +9 -0
  78. slidge/slixfix/xep_0424/retraction.py +77 -0
  79. slidge/slixfix/xep_0424/stanza.py +28 -0
  80. slidge/slixfix/xep_0490/__init__.py +8 -0
  81. slidge/slixfix/xep_0490/mds.py +47 -0
  82. slidge/slixfix/xep_0490/stanza.py +17 -0
  83. slidge/util/__init__.py +15 -0
  84. slidge/util/archive_msg.py +61 -0
  85. slidge/util/conf.py +206 -0
  86. slidge/util/db.py +229 -0
  87. slidge/util/schema.sql +126 -0
  88. slidge/util/sql.py +508 -0
  89. slidge/util/test.py +295 -0
  90. slidge/util/types.py +180 -0
  91. slidge/util/util.py +295 -0
  92. slidge-0.1.0.dist-info/LICENSE +661 -0
  93. slidge-0.1.0.dist-info/METADATA +109 -0
  94. slidge-0.1.0.dist-info/RECORD +96 -0
  95. slidge-0.1.0.dist-info/WHEEL +4 -0
  96. slidge-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,193 @@
1
+ # Commands only accessible for slidge admins
2
+ import functools
3
+ import logging
4
+ from datetime import datetime
5
+ from typing import Any, Optional
6
+
7
+ from slixmpp import JID
8
+ from slixmpp.exceptions import XMPPError
9
+
10
+ from ..util.db import user_store
11
+ from ..util.types import AnyBaseSession
12
+ from .base import (
13
+ Command,
14
+ CommandAccess,
15
+ Confirmation,
16
+ Form,
17
+ FormField,
18
+ FormValues,
19
+ TableResult,
20
+ )
21
+ from .categories import ADMINISTRATION
22
+
23
+
24
+ class AdminCommand(Command):
25
+ ACCESS = CommandAccess.ADMIN_ONLY
26
+ CATEGORY = ADMINISTRATION
27
+
28
+
29
+ class ListUsers(AdminCommand):
30
+ NAME = "👤 List registered users"
31
+ HELP = "List the users registered to this gateway"
32
+ NODE = CHAT_COMMAND = "list_users"
33
+
34
+ async def run(self, _session, _ifrom, *_):
35
+ items = []
36
+ for u in user_store.get_all():
37
+ d = u.registration_date
38
+ if d is None:
39
+ joined = ""
40
+ else:
41
+ joined = d.isoformat(timespec="seconds")
42
+ items.append({"jid": u.bare_jid, "joined": joined})
43
+ return TableResult(
44
+ description="List of registered users",
45
+ fields=[FormField("jid", type="jid-single"), FormField("joined")],
46
+ items=items, # type:ignore
47
+ )
48
+
49
+
50
+ class SlidgeInfo(AdminCommand):
51
+ NAME = "ℹ️ Server information"
52
+ HELP = "List the users registered to this gateway"
53
+ NODE = CHAT_COMMAND = "info"
54
+ ACCESS = CommandAccess.ANY
55
+
56
+ async def run(self, _session, _ifrom, *_):
57
+ from ..__main__ import __version__
58
+
59
+ start = self.xmpp.datetime_started
60
+ uptime = datetime.now() - start
61
+
62
+ if uptime.days:
63
+ days_ago = f"{uptime.days} day{'s' if uptime.days != 1 else ''}"
64
+ else:
65
+ days_ago = None
66
+ hours, seconds = divmod(uptime.seconds, 3600)
67
+
68
+ if hours:
69
+ hours_ago = f"{hours} hour"
70
+ if hours != 1:
71
+ hours_ago += "s"
72
+ else:
73
+ hours_ago = None
74
+
75
+ minutes, seconds = divmod(seconds, 60)
76
+ if minutes:
77
+ minutes_ago = f"{minutes} minute"
78
+ if minutes_ago != 1:
79
+ minutes_ago += "s"
80
+ else:
81
+ minutes_ago = None
82
+
83
+ if any((days_ago, hours_ago, minutes_ago)):
84
+ seconds_ago = None
85
+ else:
86
+ seconds_ago = f"{seconds} second"
87
+ if seconds != 1:
88
+ seconds_ago += "s"
89
+
90
+ ago = ", ".join(
91
+ [a for a in (days_ago, hours_ago, minutes_ago, seconds_ago) if a]
92
+ )
93
+
94
+ return (
95
+ f"{self.xmpp.COMPONENT_NAME} version {__version__}\n"
96
+ f"Up since {start:%Y-%m-%d %H:%M} ({ago} ago)"
97
+ )
98
+
99
+
100
+ class DeleteUser(AdminCommand):
101
+ NAME = "❌ Delete a user"
102
+ HELP = "Unregister a user from the gateway"
103
+ NODE = CHAT_COMMAND = "delete_user"
104
+
105
+ async def run(self, _session, _ifrom, *_):
106
+ return Form(
107
+ title="Remove a slidge user",
108
+ instructions="Enter the bare JID of the user you want to delete",
109
+ fields=[FormField("jid", type="jid-single", label="JID", required=True)],
110
+ handler=self.delete,
111
+ )
112
+
113
+ async def delete(
114
+ self, form_values: FormValues, _session: AnyBaseSession, _ifrom: JID
115
+ ) -> Confirmation:
116
+ jid: JID = form_values.get("jid") # type:ignore
117
+ user = user_store.get_by_jid(jid)
118
+ if user is None:
119
+ raise XMPPError("item-not-found", text=f"There is no user '{jid}'")
120
+
121
+ return Confirmation(
122
+ prompt=f"Are you sure you want to unregister '{jid}' from slidge?",
123
+ success=f"User {jid} has been deleted",
124
+ handler=functools.partial(self.finish, jid=jid),
125
+ )
126
+
127
+ async def finish(
128
+ self, _session: Optional[AnyBaseSession], _ifrom: JID, jid: JID
129
+ ) -> None:
130
+ user = user_store.get_by_jid(jid)
131
+ if user is None:
132
+ raise XMPPError("bad-request", f"{jid} has no account here!")
133
+ await self.xmpp.unregister_user(user)
134
+
135
+
136
+ class ChangeLoglevel(AdminCommand):
137
+ NAME = "📋 Change the verbosity of the logs"
138
+ HELP = "Set the logging level"
139
+ NODE = CHAT_COMMAND = "loglevel"
140
+
141
+ async def run(self, _session, _ifrom, *_):
142
+ return Form(
143
+ title=self.NAME,
144
+ instructions=self.HELP,
145
+ fields=[
146
+ FormField(
147
+ "level",
148
+ label="Log level",
149
+ required=True,
150
+ type="list-single",
151
+ options=[
152
+ {"label": "WARNING (quiet)", "value": str(logging.WARNING)},
153
+ {"label": "INFO (normal)", "value": str(logging.INFO)},
154
+ {"label": "DEBUG (verbose)", "value": str(logging.DEBUG)},
155
+ ],
156
+ )
157
+ ],
158
+ handler=self.finish,
159
+ )
160
+
161
+ @staticmethod
162
+ async def finish(
163
+ form_values: FormValues, _session: AnyBaseSession, _ifrom: JID
164
+ ) -> None:
165
+ logging.getLogger().setLevel(int(form_values["level"])) # type:ignore
166
+
167
+
168
+ class Exec(AdminCommand):
169
+ NAME = HELP = "Exec arbitrary python code. SHOULD NEVER BE AVAILABLE IN PROD."
170
+ CHAT_COMMAND = "!"
171
+ NODE = "exec"
172
+ ACCESS = CommandAccess.ADMIN_ONLY
173
+
174
+ prev_snapshot = None
175
+
176
+ context = dict[str, Any]()
177
+
178
+ def __init__(self, xmpp):
179
+ super().__init__(xmpp)
180
+
181
+ async def run(self, session, ifrom: JID, *args):
182
+ from contextlib import redirect_stdout
183
+ from io import StringIO
184
+
185
+ f = StringIO()
186
+ with redirect_stdout(f):
187
+ exec(" ".join(args), self.context)
188
+
189
+ out = f.getvalue()
190
+ if out:
191
+ return f"```\n{out}\n```"
192
+ else:
193
+ return "No output"
slidge/command/base.py ADDED
@@ -0,0 +1,441 @@
1
+ from abc import ABC
2
+ from dataclasses import dataclass, field
3
+ from enum import Enum
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Awaitable,
8
+ Callable,
9
+ Collection,
10
+ Iterable,
11
+ Optional,
12
+ Type,
13
+ TypedDict,
14
+ Union,
15
+ )
16
+
17
+ from slixmpp import JID # type: ignore[attr-defined]
18
+ from slixmpp.exceptions import XMPPError
19
+ from slixmpp.plugins.xep_0004 import Form as SlixForm # type: ignore[attr-defined]
20
+ from slixmpp.plugins.xep_0004 import (
21
+ FormField as SlixFormField, # type: ignore[attr-defined]
22
+ )
23
+ from slixmpp.types import JidStr
24
+
25
+ from ..core import config
26
+ from ..util.db import user_store
27
+ from ..util.types import AnyBaseSession, FieldType
28
+
29
+ if TYPE_CHECKING:
30
+ from ..core.gateway import BaseGateway
31
+ from ..core.session import BaseSession
32
+
33
+
34
+ HandlerType = Union[
35
+ Callable[[AnyBaseSession, JID], "CommandResponseType"],
36
+ Callable[[AnyBaseSession, JID], Awaitable["CommandResponseType"]],
37
+ ]
38
+
39
+ FormValues = dict[str, Union[str, JID, bool]]
40
+
41
+
42
+ FormHandlerType = Callable[
43
+ [FormValues, AnyBaseSession, JID],
44
+ Awaitable["CommandResponseType"],
45
+ ]
46
+
47
+ ConfirmationHandlerType = Callable[
48
+ [Optional[AnyBaseSession], JID], Awaitable["CommandResponseType"]
49
+ ]
50
+
51
+
52
+ @dataclass
53
+ class TableResult:
54
+ """
55
+ Structured data as the result of a command
56
+ """
57
+
58
+ fields: Collection["FormField"]
59
+ """
60
+ The 'columns names' of the table.
61
+ """
62
+ items: Collection[dict[str, Union[str, JID]]]
63
+ """
64
+ The rows of the table. Each row is a dict where keys are the fields ``var``
65
+ attribute.
66
+ """
67
+ description: str
68
+ """
69
+ A description of the content of the table.
70
+ """
71
+
72
+ jids_are_mucs: bool = False
73
+
74
+ def get_xml(self) -> SlixForm:
75
+ """
76
+ Get a slixmpp "form" (with <reported> header)to represent the data
77
+
78
+ :return: some XML
79
+ """
80
+ form = SlixForm() # type: ignore[no-untyped-call]
81
+ form["type"] = "result"
82
+ form["title"] = self.description
83
+ for f in self.fields:
84
+ form.add_reported(f.var, label=f.label, type=f.type) # type: ignore[no-untyped-call]
85
+ for item in self.items:
86
+ form.add_item({k: str(v) for k, v in item.items()}) # type: ignore[no-untyped-call]
87
+ return form
88
+
89
+
90
+ @dataclass
91
+ class SearchResult(TableResult):
92
+ """
93
+ Results of the search command (search for contacts via Jabber Search)
94
+
95
+ Return type of :meth:`BaseSession.search`.
96
+ """
97
+
98
+ description: str = "Contact search results"
99
+
100
+
101
+ @dataclass
102
+ class Confirmation:
103
+ """
104
+ A confirmation 'dialog'
105
+ """
106
+
107
+ prompt: str
108
+ """
109
+ The text presented to the command triggering user
110
+ """
111
+ handler: ConfirmationHandlerType
112
+ """
113
+ An async function that should return a ResponseType
114
+ """
115
+ success: Optional[str] = None
116
+ """
117
+ Text in case of success, used if handler does not return anything
118
+ """
119
+ handler_args: Iterable[Any] = field(default_factory=list)
120
+ """
121
+ arguments passed to the handler
122
+ """
123
+ handler_kwargs: dict[str, Any] = field(default_factory=dict)
124
+ """
125
+ keyword arguments passed to the handler
126
+ """
127
+
128
+ def get_form(self) -> SlixForm:
129
+ """
130
+ Get the slixmpp form
131
+
132
+ :return: some xml
133
+ """
134
+ form = SlixForm() # type: ignore[no-untyped-call]
135
+ form["type"] = "form"
136
+ form["title"] = self.prompt
137
+ form.append(
138
+ FormField(
139
+ "confirm", type="boolean", value="true", label="Confirm"
140
+ ).get_xml()
141
+ )
142
+ return form
143
+
144
+
145
+ @dataclass
146
+ class Form:
147
+ """
148
+ A form, to request user input
149
+ """
150
+
151
+ title: str
152
+ instructions: str
153
+ fields: Collection["FormField"]
154
+ handler: FormHandlerType
155
+ handler_args: Iterable[Any] = field(default_factory=list)
156
+ handler_kwargs: dict[str, Any] = field(default_factory=dict)
157
+
158
+ def get_values(
159
+ self, slix_form: SlixForm
160
+ ) -> dict[str, Union[list[str], list[JID], str, JID, bool, None]]:
161
+ """
162
+ Parse form submission
163
+
164
+ :param slix_form: the xml received as the submission of a form
165
+ :return: A dict where keys=field.var and values are either strings
166
+ or JIDs (if field.type=jid-single)
167
+ """
168
+ str_values: dict[str, str] = slix_form.get_values() # type: ignore[no-untyped-call]
169
+ values = {}
170
+ for f in self.fields:
171
+ values[f.var] = f.validate(str_values.get(f.var))
172
+ return values
173
+
174
+ def get_xml(self) -> SlixForm:
175
+ """
176
+ Get the slixmpp "form"
177
+
178
+ :return: some XML
179
+ """
180
+ form = SlixForm() # type: ignore[no-untyped-call]
181
+ form["type"] = "form"
182
+ form["instructions"] = self.instructions
183
+ form["title"] = self.title
184
+ for fi in self.fields:
185
+ form.append(fi.get_xml())
186
+ return form
187
+
188
+
189
+ class CommandAccess(int, Enum):
190
+ """
191
+ Defines who can access a given Command
192
+ """
193
+
194
+ ADMIN_ONLY = 0
195
+ USER = 1
196
+ USER_LOGGED = 2
197
+ USER_NON_LOGGED = 3
198
+ NON_USER = 4
199
+ ANY = 5
200
+
201
+
202
+ class Option(TypedDict):
203
+ """
204
+ Options to be used for ``FormField``s of type ``list-*``
205
+ """
206
+
207
+ label: str
208
+ value: str
209
+
210
+
211
+ # TODO: support forms validation XEP-0122
212
+ @dataclass
213
+ class FormField:
214
+ """
215
+ Represents a field of the form that a user will see when registering to the gateway
216
+ via their XMPP client.
217
+ """
218
+
219
+ var: str = ""
220
+ """
221
+ Internal name of the field, will be used to retrieve via :py:attr:`slidge.GatewayUser.registration_form`
222
+ """
223
+ label: Optional[str] = None
224
+ """Description of the field that the user will see"""
225
+ required: bool = False
226
+ """Whether this field is mandatory or not"""
227
+ private: bool = False
228
+ """
229
+ For sensitive info that should not be displayed on screen while the user types.
230
+ Forces field_type to "text-private"
231
+ """
232
+ type: FieldType = "text-single"
233
+ """Type of the field, see `XEP-0004 <https://xmpp.org/extensions/xep-0004.html#protocol-fieldtypes>`_"""
234
+ value: str = ""
235
+ """Pre-filled value. Will be automatically pre-filled if a registered user modifies their subscription"""
236
+ options: Optional[list[Option]] = None
237
+
238
+ image_url: Optional[str] = None
239
+ """An image associated to this field, eg, a QR code"""
240
+
241
+ def __post_init__(self) -> None:
242
+ if self.private:
243
+ self.type = "text-private"
244
+
245
+ def __acceptable_options(self) -> list[str]:
246
+ if not self.options:
247
+ raise RuntimeError
248
+ return [x["value"] for x in self.options]
249
+
250
+ def validate(
251
+ self, value: Optional[Union[str, list[str]]]
252
+ ) -> Union[list[str], list[JID], str, JID, bool, None]:
253
+ """
254
+ Raise appropriate XMPPError if a given value is valid for this field
255
+
256
+ :param value: The value to test
257
+ :return: The same value OR a JID if ``self.type=jid-single``
258
+ """
259
+ if isinstance(value, list) and not self.type.endswith("multi"):
260
+ raise XMPPError("not-acceptable", "A single value was expected")
261
+
262
+ if self.type in ("list-multi", "jid-multi"):
263
+ if not value:
264
+ value = []
265
+ if isinstance(value, list):
266
+ return self.__validate_list_multi(value)
267
+ else:
268
+ raise XMPPError("not-acceptable", "Multiple values was expected")
269
+
270
+ assert isinstance(value, (str, bool, JID)) or value is None
271
+
272
+ if self.required and value is None:
273
+ raise XMPPError("not-acceptable", f"Missing field: '{self.label}'")
274
+
275
+ if value is None:
276
+ return None
277
+
278
+ if self.type == "jid-single":
279
+ try:
280
+ return JID(value)
281
+ except ValueError:
282
+ raise XMPPError("not-acceptable", f"Not a valid JID: '{value}'")
283
+
284
+ elif self.type == "list-single":
285
+ if value not in self.__acceptable_options():
286
+ raise XMPPError("not-acceptable", f"Not a valid option: '{value}'")
287
+
288
+ elif self.type == "boolean":
289
+ return value.lower() in ("1", "true") if isinstance(value, str) else value
290
+
291
+ return value
292
+
293
+ def __validate_list_multi(self, value: list[str]) -> Union[list[str], list[JID]]:
294
+ for v in value:
295
+ if v not in self.__acceptable_options():
296
+ raise XMPPError("not-acceptable", f"Not a valid option: '{v}'")
297
+ if self.type == "list-multi":
298
+ return value
299
+ return [JID(v) for v in value]
300
+
301
+ def get_xml(self) -> SlixFormField:
302
+ """
303
+ Get the field in slixmpp format
304
+
305
+ :return: some XML
306
+ """
307
+ f = SlixFormField()
308
+ f["var"] = self.var
309
+ f["label"] = self.label
310
+ f["required"] = self.required
311
+ f["type"] = self.type
312
+ if self.options:
313
+ for o in self.options:
314
+ f.add_option(**o) # type: ignore[no-untyped-call]
315
+ f["value"] = self.value
316
+ if self.image_url:
317
+ f["media"].add_uri(self.image_url, itype="image/png")
318
+ return f
319
+
320
+
321
+ CommandResponseType = Union[TableResult, Confirmation, Form, str, None]
322
+
323
+
324
+ class Command(ABC):
325
+ """
326
+ Abstract base class to implement gateway commands (chatbot and ad-hoc)
327
+ """
328
+
329
+ NAME: str = NotImplemented
330
+ """
331
+ Friendly name of the command, eg: "do something with stuff"
332
+ """
333
+ HELP: str = NotImplemented
334
+ """
335
+ Long description of what the command does
336
+ """
337
+ NODE: str = NotImplemented
338
+ """
339
+ Name of the node used for ad-hoc commands
340
+ """
341
+ CHAT_COMMAND: str = NotImplemented
342
+ """
343
+ Text to send to the gateway to trigger the command via a message
344
+ """
345
+
346
+ ACCESS: "CommandAccess" = NotImplemented
347
+ """
348
+ Who can use this command
349
+ """
350
+
351
+ CATEGORY: Optional[str] = None
352
+ """
353
+ If used, the command will be under this top-level category.
354
+ Use the same string for several commands to group them.
355
+ This hierarchy only used for the adhoc interface, not the chat command
356
+ interface.
357
+ """
358
+
359
+ subclasses = list[Type["Command"]]()
360
+
361
+ def __init__(self, xmpp: "BaseGateway"):
362
+ self.xmpp = xmpp
363
+
364
+ def __init_subclass__(cls, **kwargs: Any) -> None:
365
+ # store subclasses so subclassing is enough for the command to be
366
+ # picked up by slidge
367
+ cls.subclasses.append(cls)
368
+
369
+ async def run(
370
+ self, session: Optional["BaseSession[Any, Any]"], ifrom: JID, *args: str
371
+ ) -> CommandResponseType:
372
+ """
373
+ Entry point of the command
374
+
375
+ :param session: If triggered by a registered user, its slidge Session
376
+ :param ifrom: JID of the command-triggering entity
377
+ :param args: When triggered via chatbot type message, additional words
378
+ after the CHAT_COMMAND string was passed
379
+
380
+ :return: Either a TableResult, a Form, a Confirmation, a text, or None
381
+ """
382
+ raise XMPPError("feature-not-implemented")
383
+
384
+ def _get_session(self, jid: JID) -> Optional["BaseSession[Any, Any]"]:
385
+ user = user_store.get_by_jid(jid)
386
+ if user is None:
387
+ return None
388
+
389
+ return self.xmpp.get_session_from_user(user)
390
+
391
+ def __can_use_command(self, jid: JID):
392
+ j = jid.bare
393
+ return self.xmpp.jid_validator.match(j) or j in config.ADMINS
394
+
395
+ def raise_if_not_authorized(self, jid: JID) -> Optional["BaseSession[Any, Any]"]:
396
+ """
397
+ Raise an appropriate error is jid is not authorized to use the command
398
+
399
+ :param jid: jid of the entity trying to access the command
400
+ :return:session of JID if it exists
401
+ """
402
+ session = self._get_session(jid)
403
+ if not self.__can_use_command(jid):
404
+ raise XMPPError(
405
+ "bad-request", "Your JID is not allowed to use this gateway."
406
+ )
407
+
408
+ if self.ACCESS == CommandAccess.ADMIN_ONLY and not is_admin(jid):
409
+ raise XMPPError("not-authorized")
410
+ elif self.ACCESS == CommandAccess.NON_USER and session is not None:
411
+ raise XMPPError(
412
+ "bad-request", "This is only available for non-users. Unregister first."
413
+ )
414
+ elif self.ACCESS == CommandAccess.USER and session is None:
415
+ raise XMPPError(
416
+ "forbidden",
417
+ "This is only available for users that are registered to this gateway",
418
+ )
419
+ elif self.ACCESS == CommandAccess.USER_NON_LOGGED:
420
+ if session is None or session.logged:
421
+ raise XMPPError(
422
+ "forbidden",
423
+ (
424
+ "This is only available for users that are not logged to the"
425
+ " legacy service"
426
+ ),
427
+ )
428
+ elif self.ACCESS == CommandAccess.USER_LOGGED:
429
+ if session is None or not session.logged:
430
+ raise XMPPError(
431
+ "forbidden",
432
+ (
433
+ "This is only available when you are logged in to the legacy"
434
+ " service"
435
+ ),
436
+ )
437
+ return session
438
+
439
+
440
+ def is_admin(jid: JidStr) -> bool:
441
+ return JID(jid).bare in config.ADMINS
@@ -0,0 +1,3 @@
1
+ ADMINISTRATION = "🛷️ Slidge administration"
2
+ CONTACTS = "👤 Contacts"
3
+ GROUPS = "👥 Groups"