slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. slidge/__init__.py +54 -31
  2. slidge/__main__.py +51 -5
  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 +2 -0
  15. slidge/core/cache.py +121 -39
  16. slidge/core/config.py +116 -11
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -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 +795 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +9 -1
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +6 -19
  34. slidge/core/mixins/disco.py +66 -15
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +254 -252
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +128 -31
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +275 -116
  41. slidge/core/session.py +586 -518
  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 +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +1 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
  56. slidge/slixfix/xep_0153/__init__.py +10 -0
  57. slidge/slixfix/xep_0153/stanza.py +25 -0
  58. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  59. slidge/slixfix/xep_0264/__init__.py +5 -0
  60. slidge/slixfix/xep_0264/stanza.py +36 -0
  61. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  62. slidge/slixfix/xep_0292/__init__.py +5 -0
  63. slidge/slixfix/xep_0292/vcard4.py +100 -0
  64. slidge/slixfix/xep_0313/__init__.py +12 -0
  65. slidge/slixfix/xep_0313/mam.py +262 -0
  66. slidge/slixfix/xep_0313/stanza.py +359 -0
  67. slidge/slixfix/xep_0317/__init__.py +5 -0
  68. slidge/slixfix/xep_0317/hats.py +17 -0
  69. slidge/slixfix/xep_0317/stanza.py +28 -0
  70. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  71. slidge/slixfix/xep_0424/__init__.py +9 -0
  72. slidge/slixfix/xep_0424/retraction.py +77 -0
  73. slidge/slixfix/xep_0424/stanza.py +28 -0
  74. slidge/slixfix/xep_0490/__init__.py +8 -0
  75. slidge/slixfix/xep_0490/mds.py +47 -0
  76. slidge/slixfix/xep_0490/stanza.py +17 -0
  77. slidge/util/__init__.py +4 -6
  78. slidge/util/archive_msg.py +61 -0
  79. slidge/util/conf.py +25 -4
  80. slidge/util/db.py +23 -69
  81. slidge/util/schema.sql +126 -0
  82. slidge/util/sql.py +508 -0
  83. slidge/util/test.py +136 -86
  84. slidge/util/types.py +155 -14
  85. slidge/util/util.py +225 -51
  86. slidge-0.1.2.dist-info/METADATA +111 -0
  87. slidge-0.1.2.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
  89. slidge/core/adhoc.py +0 -492
  90. slidge/core/chat_command.py +0 -197
  91. slidge/core/contact.py +0 -441
  92. slidge/core/disco.py +0 -59
  93. slidge/core/gateway.py +0 -899
  94. slidge/core/muc/__init__.py +0 -3
  95. slidge/core/muc/bookmarks.py +0 -74
  96. slidge/core/muc/participant.py +0 -152
  97. slidge/core/muc/room.py +0 -348
  98. slidge/plugins/discord/__init__.py +0 -121
  99. slidge/plugins/discord/client.py +0 -121
  100. slidge/plugins/discord/session.py +0 -172
  101. slidge/plugins/dummy.py +0 -334
  102. slidge/plugins/facebook.py +0 -591
  103. slidge/plugins/hackernews.py +0 -209
  104. slidge/plugins/mattermost/__init__.py +0 -1
  105. slidge/plugins/mattermost/api.py +0 -288
  106. slidge/plugins/mattermost/gateway.py +0 -417
  107. slidge/plugins/mattermost/websocket.py +0 -248
  108. slidge/plugins/signal/__init__.py +0 -4
  109. slidge/plugins/signal/config.py +0 -4
  110. slidge/plugins/signal/contact.py +0 -104
  111. slidge/plugins/signal/gateway.py +0 -379
  112. slidge/plugins/signal/group.py +0 -76
  113. slidge/plugins/signal/session.py +0 -515
  114. slidge/plugins/signal/txt.py +0 -13
  115. slidge/plugins/signal/util.py +0 -32
  116. slidge/plugins/skype.py +0 -310
  117. slidge/plugins/steam.py +0 -400
  118. slidge/plugins/telegram/__init__.py +0 -6
  119. slidge/plugins/telegram/client.py +0 -325
  120. slidge/plugins/telegram/config.py +0 -21
  121. slidge/plugins/telegram/contact.py +0 -154
  122. slidge/plugins/telegram/gateway.py +0 -182
  123. slidge/plugins/telegram/group.py +0 -184
  124. slidge/plugins/telegram/session.py +0 -275
  125. slidge/plugins/telegram/util.py +0 -153
  126. slidge/plugins/whatsapp/__init__.py +0 -6
  127. slidge/plugins/whatsapp/config.py +0 -17
  128. slidge/plugins/whatsapp/contact.py +0 -33
  129. slidge/plugins/whatsapp/event.go +0 -455
  130. slidge/plugins/whatsapp/gateway.go +0 -156
  131. slidge/plugins/whatsapp/gateway.py +0 -69
  132. slidge/plugins/whatsapp/go.mod +0 -17
  133. slidge/plugins/whatsapp/go.sum +0 -22
  134. slidge/plugins/whatsapp/session.go +0 -371
  135. slidge/plugins/whatsapp/session.py +0 -370
  136. slidge/util/xep_0030/__init__.py +0 -13
  137. slidge/util/xep_0030/disco.py +0 -811
  138. slidge/util/xep_0030/stanza/__init__.py +0 -7
  139. slidge/util/xep_0030/stanza/info.py +0 -270
  140. slidge/util/xep_0030/stanza/items.py +0 -147
  141. slidge/util/xep_0030/static.py +0 -467
  142. slidge/util/xep_0050/adhoc.py +0 -631
  143. slidge/util/xep_0050/stanza.py +0 -180
  144. slidge/util/xep_0077/stanza.py +0 -71
  145. slidge/util/xep_0292/__init__.py +0 -1
  146. slidge/util/xep_0292/stanza.py +0 -167
  147. slidge/util/xep_0292/vcard4.py +0 -74
  148. slidge/util/xep_0356/__init__.py +0 -7
  149. slidge/util/xep_0356/permissions.py +0 -35
  150. slidge/util/xep_0356/privilege.py +0 -160
  151. slidge/util/xep_0356/stanza.py +0 -44
  152. slidge/util/xep_0461/__init__.py +0 -6
  153. slidge/util/xep_0461/reply.py +0 -48
  154. slidge/util/xep_0461/stanza.py +0 -80
  155. slidge-0.1.0rc1.dist-info/METADATA +0 -171
  156. slidge-0.1.0rc1.dist-info/RECORD +0 -99
  157. /slidge/{plugins/__init__.py → py.typed} +0 -0
  158. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  159. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  160. /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
  161. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  162. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  163. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
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"