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.
- slidge/__init__.py +54 -31
- slidge/__main__.py +51 -5
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +2 -0
- slidge/core/cache.py +121 -39
- slidge/core/config.py +116 -11
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +895 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +795 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +9 -1
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +6 -19
- slidge/core/mixins/disco.py +66 -15
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +254 -252
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +128 -31
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +275 -116
- slidge/core/session.py +586 -518
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +458 -0
- slidge/group/room.py +1103 -0
- slidge/migration.py +18 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/{util → slixfix}/xep_0077/register.py +1 -2
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +4 -6
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +25 -4
- slidge/util/db.py +23 -69
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +136 -86
- slidge/util/types.py +155 -14
- slidge/util/util.py +225 -51
- slidge-0.1.2.dist-info/METADATA +111 -0
- slidge-0.1.2.dist-info/RECORD +96 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
- slidge/core/adhoc.py +0 -492
- slidge/core/chat_command.py +0 -197
- slidge/core/contact.py +0 -441
- slidge/core/disco.py +0 -59
- slidge/core/gateway.py +0 -899
- slidge/core/muc/__init__.py +0 -3
- slidge/core/muc/bookmarks.py +0 -74
- slidge/core/muc/participant.py +0 -152
- slidge/core/muc/room.py +0 -348
- slidge/plugins/discord/__init__.py +0 -121
- slidge/plugins/discord/client.py +0 -121
- slidge/plugins/discord/session.py +0 -172
- slidge/plugins/dummy.py +0 -334
- slidge/plugins/facebook.py +0 -591
- slidge/plugins/hackernews.py +0 -209
- slidge/plugins/mattermost/__init__.py +0 -1
- slidge/plugins/mattermost/api.py +0 -288
- slidge/plugins/mattermost/gateway.py +0 -417
- slidge/plugins/mattermost/websocket.py +0 -248
- slidge/plugins/signal/__init__.py +0 -4
- slidge/plugins/signal/config.py +0 -4
- slidge/plugins/signal/contact.py +0 -104
- slidge/plugins/signal/gateway.py +0 -379
- slidge/plugins/signal/group.py +0 -76
- slidge/plugins/signal/session.py +0 -515
- slidge/plugins/signal/txt.py +0 -13
- slidge/plugins/signal/util.py +0 -32
- slidge/plugins/skype.py +0 -310
- slidge/plugins/steam.py +0 -400
- slidge/plugins/telegram/__init__.py +0 -6
- slidge/plugins/telegram/client.py +0 -325
- slidge/plugins/telegram/config.py +0 -21
- slidge/plugins/telegram/contact.py +0 -154
- slidge/plugins/telegram/gateway.py +0 -182
- slidge/plugins/telegram/group.py +0 -184
- slidge/plugins/telegram/session.py +0 -275
- slidge/plugins/telegram/util.py +0 -153
- slidge/plugins/whatsapp/__init__.py +0 -6
- slidge/plugins/whatsapp/config.py +0 -17
- slidge/plugins/whatsapp/contact.py +0 -33
- slidge/plugins/whatsapp/event.go +0 -455
- slidge/plugins/whatsapp/gateway.go +0 -156
- slidge/plugins/whatsapp/gateway.py +0 -69
- slidge/plugins/whatsapp/go.mod +0 -17
- slidge/plugins/whatsapp/go.sum +0 -22
- slidge/plugins/whatsapp/session.go +0 -371
- slidge/plugins/whatsapp/session.py +0 -370
- slidge/util/xep_0030/__init__.py +0 -13
- slidge/util/xep_0030/disco.py +0 -811
- slidge/util/xep_0030/stanza/__init__.py +0 -7
- slidge/util/xep_0030/stanza/info.py +0 -270
- slidge/util/xep_0030/stanza/items.py +0 -147
- slidge/util/xep_0030/static.py +0 -467
- slidge/util/xep_0050/adhoc.py +0 -631
- slidge/util/xep_0050/stanza.py +0 -180
- slidge/util/xep_0077/stanza.py +0 -71
- slidge/util/xep_0292/__init__.py +0 -1
- slidge/util/xep_0292/stanza.py +0 -167
- slidge/util/xep_0292/vcard4.py +0 -74
- slidge/util/xep_0356/__init__.py +0 -7
- slidge/util/xep_0356/permissions.py +0 -35
- slidge/util/xep_0356/privilege.py +0 -160
- slidge/util/xep_0356/stanza.py +0 -44
- slidge/util/xep_0461/__init__.py +0 -6
- slidge/util/xep_0461/reply.py +0 -48
- slidge/util/xep_0461/stanza.py +0 -80
- slidge-0.1.0rc1.dist-info/METADATA +0 -171
- slidge-0.1.0rc1.dist-info/RECORD +0 -99
- /slidge/{plugins/__init__.py → py.typed} +0 -0
- /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
- /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,288 @@
|
|
1
|
+
# Handle slidge commands by exchanging chat messages with the gateway components.
|
2
|
+
|
3
|
+
# Ad-hoc methods should provide a better UX, but some clients do not support them,
|
4
|
+
# so this is mostly a fallback.
|
5
|
+
import asyncio
|
6
|
+
import functools
|
7
|
+
import logging
|
8
|
+
from typing import TYPE_CHECKING, Callable, Literal, Optional, Union, overload
|
9
|
+
from urllib.parse import quote as url_quote
|
10
|
+
|
11
|
+
from slixmpp import JID, CoroutineCallback, Message, StanzaPath
|
12
|
+
from slixmpp.exceptions import XMPPError
|
13
|
+
from slixmpp.types import JidStr, MessageTypes
|
14
|
+
|
15
|
+
from . import Command, CommandResponseType, Confirmation, Form, TableResult
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from ..core.gateway import BaseGateway
|
19
|
+
|
20
|
+
|
21
|
+
class ChatCommandProvider:
|
22
|
+
UNKNOWN = "Wut? I don't know that command: {}"
|
23
|
+
|
24
|
+
def __init__(self, xmpp: "BaseGateway"):
|
25
|
+
self.xmpp = xmpp
|
26
|
+
self._keywords = list[str]()
|
27
|
+
self._commands: dict[str, Command] = {}
|
28
|
+
self._input_futures = dict[str, asyncio.Future[str]]()
|
29
|
+
self.xmpp.register_handler(
|
30
|
+
CoroutineCallback(
|
31
|
+
"chat_command_handler",
|
32
|
+
StanzaPath(f"message@to={self.xmpp.boundjid.bare}"),
|
33
|
+
self._handle_message, # type: ignore
|
34
|
+
)
|
35
|
+
)
|
36
|
+
|
37
|
+
def register(self, command: Command):
|
38
|
+
"""
|
39
|
+
Register a command to be used via chat messages with the gateway
|
40
|
+
|
41
|
+
Plugins should not call this, any class subclassing Command should be
|
42
|
+
automatically added by slidge core.
|
43
|
+
|
44
|
+
:param command: the new command
|
45
|
+
"""
|
46
|
+
t = command.CHAT_COMMAND
|
47
|
+
if t in self._commands:
|
48
|
+
raise RuntimeError("There is already a command triggered by '%s'", t)
|
49
|
+
self._commands[t] = command
|
50
|
+
|
51
|
+
@overload
|
52
|
+
async def input(
|
53
|
+
self, jid: JidStr, text: Optional[str], blocking: Literal[False]
|
54
|
+
) -> asyncio.Future[str]: ...
|
55
|
+
|
56
|
+
@overload
|
57
|
+
async def input(
|
58
|
+
self,
|
59
|
+
jid: JidStr,
|
60
|
+
text: Optional[str],
|
61
|
+
mtype: MessageTypes = ...,
|
62
|
+
blocking: Literal[True] = ...,
|
63
|
+
) -> str: ...
|
64
|
+
|
65
|
+
async def input(
|
66
|
+
self,
|
67
|
+
jid,
|
68
|
+
text=None,
|
69
|
+
mtype="chat",
|
70
|
+
timeout=60,
|
71
|
+
blocking=True,
|
72
|
+
**msg_kwargs,
|
73
|
+
):
|
74
|
+
"""
|
75
|
+
Request arbitrary user input using a simple chat message, and await the result.
|
76
|
+
|
77
|
+
You shouldn't need to call directly bust instead use :meth:`.BaseSession.input`
|
78
|
+
to directly target a user.
|
79
|
+
|
80
|
+
NB: When using this, the next message that the user sent to the component will
|
81
|
+
not be transmitted to :meth:`.BaseGateway.on_gateway_message`, but rather intercepted.
|
82
|
+
Await the coroutine to get its content.
|
83
|
+
|
84
|
+
:param jid: The JID we want input from
|
85
|
+
:param text: A prompt to display for the user
|
86
|
+
:param mtype: Message type
|
87
|
+
:param timeout:
|
88
|
+
:param blocking: If set to False, timeout has no effect and an :class:`asyncio.Future`
|
89
|
+
is returned instead of a str
|
90
|
+
:return: The user's reply
|
91
|
+
"""
|
92
|
+
jid = JID(jid)
|
93
|
+
if text is not None:
|
94
|
+
self.xmpp.send_message(
|
95
|
+
mto=jid,
|
96
|
+
mbody=text,
|
97
|
+
mtype=mtype,
|
98
|
+
mfrom=self.xmpp.boundjid.bare,
|
99
|
+
**msg_kwargs,
|
100
|
+
)
|
101
|
+
f = asyncio.get_event_loop().create_future()
|
102
|
+
self._input_futures[jid.bare] = f
|
103
|
+
if not blocking:
|
104
|
+
return f
|
105
|
+
try:
|
106
|
+
await asyncio.wait_for(f, timeout)
|
107
|
+
except asyncio.TimeoutError:
|
108
|
+
self.xmpp.send_message(
|
109
|
+
mto=jid,
|
110
|
+
mbody="You took too much time to reply",
|
111
|
+
mtype=mtype,
|
112
|
+
mfrom=self.xmpp.boundjid.bare,
|
113
|
+
)
|
114
|
+
del self._input_futures[jid.bare]
|
115
|
+
raise XMPPError("remote-server-timeout", "You took too much time to reply")
|
116
|
+
|
117
|
+
return f.result()
|
118
|
+
|
119
|
+
async def _handle_message(self, msg: Message):
|
120
|
+
if not msg["body"]:
|
121
|
+
return
|
122
|
+
|
123
|
+
if not msg.get_from().node:
|
124
|
+
return # ignore component and server messages
|
125
|
+
|
126
|
+
f = self._input_futures.pop(msg.get_from().bare, None)
|
127
|
+
if f is not None:
|
128
|
+
f.set_result(msg["body"])
|
129
|
+
return
|
130
|
+
|
131
|
+
c = msg["body"]
|
132
|
+
first_word, *rest = c.split(" ")
|
133
|
+
first_word = first_word.lower()
|
134
|
+
|
135
|
+
if first_word == "help":
|
136
|
+
return self._handle_help(msg, *rest)
|
137
|
+
|
138
|
+
mfrom = msg.get_from()
|
139
|
+
|
140
|
+
command = self._commands.get(first_word)
|
141
|
+
if command is None:
|
142
|
+
return self._not_found(msg, first_word)
|
143
|
+
|
144
|
+
try:
|
145
|
+
session = command.raise_if_not_authorized(mfrom)
|
146
|
+
except XMPPError as e:
|
147
|
+
reply = msg.reply()
|
148
|
+
reply["body"] = e.text
|
149
|
+
reply.send()
|
150
|
+
raise
|
151
|
+
|
152
|
+
result = await self.__wrap_handler(msg, command.run, session, mfrom, *rest)
|
153
|
+
self.xmpp.delivery_receipt.ack(msg)
|
154
|
+
return await self._handle_result(result, msg, session)
|
155
|
+
|
156
|
+
async def _handle_result(self, result: CommandResponseType, msg: Message, session):
|
157
|
+
if isinstance(result, str) or result is None:
|
158
|
+
reply = msg.reply()
|
159
|
+
reply["body"] = result or "End of command."
|
160
|
+
reply.send()
|
161
|
+
return
|
162
|
+
|
163
|
+
if isinstance(result, Form):
|
164
|
+
form_values = {}
|
165
|
+
for t in result.title, result.instructions:
|
166
|
+
if t:
|
167
|
+
msg.reply(t).send()
|
168
|
+
for f in result.fields:
|
169
|
+
if f.type == "fixed":
|
170
|
+
msg.reply(f"{f.label or f.var}: {f.value}").send()
|
171
|
+
else:
|
172
|
+
if f.type == "list-multi":
|
173
|
+
msg.reply(
|
174
|
+
"Multiple selection allowed, use a single space as a separator"
|
175
|
+
).send()
|
176
|
+
if f.options:
|
177
|
+
for o in f.options:
|
178
|
+
msg.reply(f"{o['value']} -- {o['label']}").send()
|
179
|
+
if f.value:
|
180
|
+
msg.reply(f"Default: {f.value}").send()
|
181
|
+
|
182
|
+
ans = await self.xmpp.input(
|
183
|
+
msg.get_from(), (f.label or f.var) + "? (or 'abort')"
|
184
|
+
)
|
185
|
+
if ans.lower() == "abort":
|
186
|
+
return await self._handle_result(
|
187
|
+
"Command aborted", msg, session
|
188
|
+
)
|
189
|
+
if f.type.endswith("multi"):
|
190
|
+
form_values[f.var] = f.validate(ans.split(" "))
|
191
|
+
else:
|
192
|
+
form_values[f.var] = f.validate(ans)
|
193
|
+
result = await self.__wrap_handler(
|
194
|
+
msg,
|
195
|
+
result.handler,
|
196
|
+
form_values,
|
197
|
+
session,
|
198
|
+
msg.get_from(),
|
199
|
+
*result.handler_args,
|
200
|
+
**result.handler_kwargs,
|
201
|
+
)
|
202
|
+
return await self._handle_result(result, msg, session)
|
203
|
+
|
204
|
+
if isinstance(result, Confirmation):
|
205
|
+
yes_or_no = await self.input(msg.get_from(), result.prompt)
|
206
|
+
if not yes_or_no.lower().startswith("y"):
|
207
|
+
reply = msg.reply()
|
208
|
+
reply["body"] = "Canceled"
|
209
|
+
reply.send()
|
210
|
+
return
|
211
|
+
result = await self.__wrap_handler(
|
212
|
+
msg,
|
213
|
+
result.handler,
|
214
|
+
session,
|
215
|
+
msg.get_from(),
|
216
|
+
*result.handler_args,
|
217
|
+
**result.handler_kwargs,
|
218
|
+
)
|
219
|
+
return await self._handle_result(result, msg, session)
|
220
|
+
|
221
|
+
if isinstance(result, TableResult):
|
222
|
+
if len(result.items) == 0:
|
223
|
+
msg.reply("Empty results").send()
|
224
|
+
return
|
225
|
+
|
226
|
+
body = result.description + "\n"
|
227
|
+
for item in result.items:
|
228
|
+
for f in result.fields:
|
229
|
+
if f.type == "jid-single":
|
230
|
+
j = JID(item[f.var])
|
231
|
+
value = f"xmpp:{percent_encode(j)}"
|
232
|
+
if result.jids_are_mucs:
|
233
|
+
value += "?join"
|
234
|
+
else:
|
235
|
+
value = item[f.var] # type:ignore
|
236
|
+
body += f"\n{f.label or f.var}: {value}"
|
237
|
+
msg.reply(body).send()
|
238
|
+
|
239
|
+
@staticmethod
|
240
|
+
async def __wrap_handler(msg, f: Union[Callable, functools.partial], *a, **k):
|
241
|
+
try:
|
242
|
+
if asyncio.iscoroutinefunction(f):
|
243
|
+
return await f(*a, **k)
|
244
|
+
elif hasattr(f, "func") and asyncio.iscoroutinefunction(f.func):
|
245
|
+
return await f(*a, **k)
|
246
|
+
else:
|
247
|
+
return f(*a, **k)
|
248
|
+
except Exception as e:
|
249
|
+
log.debug("Error in %s", f, exc_info=e)
|
250
|
+
reply = msg.reply()
|
251
|
+
reply["body"] = f"Error: {e}"
|
252
|
+
reply.send()
|
253
|
+
|
254
|
+
def _handle_help(self, msg: Message, *rest):
|
255
|
+
if len(rest) == 0:
|
256
|
+
reply = msg.reply()
|
257
|
+
reply["body"] = self._help(msg.get_from())
|
258
|
+
reply.send()
|
259
|
+
elif len(rest) == 1 and (command := self._commands.get(rest[0])):
|
260
|
+
reply = msg.reply()
|
261
|
+
reply["body"] = f"{command.CHAT_COMMAND}: {command.NAME}\n{command.HELP}"
|
262
|
+
reply.send()
|
263
|
+
else:
|
264
|
+
self._not_found(msg, str(rest))
|
265
|
+
|
266
|
+
def _help(self, mfrom: JID):
|
267
|
+
msg = "Available commands:"
|
268
|
+
for c in sorted(
|
269
|
+
self._commands.values(), key=lambda co: (co.CATEGORY or "", co.CHAT_COMMAND)
|
270
|
+
):
|
271
|
+
try:
|
272
|
+
c.raise_if_not_authorized(mfrom)
|
273
|
+
except XMPPError:
|
274
|
+
continue
|
275
|
+
msg += f"\n{c.CHAT_COMMAND} -- {c.NAME}"
|
276
|
+
return msg
|
277
|
+
|
278
|
+
def _not_found(self, msg: Message, word: str):
|
279
|
+
e = self.UNKNOWN.format(word)
|
280
|
+
msg.reply(e).send()
|
281
|
+
raise XMPPError("item-not-found", e)
|
282
|
+
|
283
|
+
|
284
|
+
def percent_encode(jid: JID):
|
285
|
+
return f"{url_quote(jid.user)}@{jid.server}" # type:ignore
|
286
|
+
|
287
|
+
|
288
|
+
log = logging.getLogger(__name__)
|
@@ -0,0 +1,179 @@
|
|
1
|
+
"""
|
2
|
+
This module handles the registration :term:`Command`, which is a necessary
|
3
|
+
step for a JID to become a slidge :term:`User`.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import asyncio
|
7
|
+
import functools
|
8
|
+
import tempfile
|
9
|
+
from datetime import datetime
|
10
|
+
from enum import IntEnum
|
11
|
+
from typing import Any
|
12
|
+
|
13
|
+
import qrcode
|
14
|
+
from slixmpp import JID, Iq
|
15
|
+
from slixmpp.exceptions import XMPPError
|
16
|
+
|
17
|
+
from ..core import config
|
18
|
+
from ..util.db import GatewayUser
|
19
|
+
from .base import Command, CommandAccess, Form, FormField, FormValues
|
20
|
+
|
21
|
+
|
22
|
+
class RegistrationType(IntEnum):
|
23
|
+
"""
|
24
|
+
An :class:`Enum` to define the registration flow.
|
25
|
+
"""
|
26
|
+
|
27
|
+
SINGLE_STEP_FORM = 0
|
28
|
+
"""
|
29
|
+
1 step, 1 form, the only flow compatible with :xep:`0077`.
|
30
|
+
Using this, the whole flow is defined
|
31
|
+
by :attr:`slidge.BaseGateway.REGISTRATION_FIELDS` and
|
32
|
+
:attr:`.REGISTRATION_INSTRUCTIONS`.
|
33
|
+
"""
|
34
|
+
|
35
|
+
QRCODE = 10
|
36
|
+
"""
|
37
|
+
The registration requires flashing a QR code in an official client.
|
38
|
+
See :meth:`slidge.BaseGateway.send_qr`, :meth:`.get_qr_text`
|
39
|
+
and :meth:`.confirm_qr`.
|
40
|
+
"""
|
41
|
+
|
42
|
+
TWO_FACTOR_CODE = 20
|
43
|
+
"""
|
44
|
+
The registration requires confirming login with a 2FA code,
|
45
|
+
eg something received by email or SMS to finalize the authentication.
|
46
|
+
See :meth:`.validate_two_factor_code`.
|
47
|
+
"""
|
48
|
+
|
49
|
+
|
50
|
+
class TwoFactorNotRequired(Exception):
|
51
|
+
"""
|
52
|
+
Should be raised in :meth:`slidge.BaseGateway.validate` if the code is not
|
53
|
+
required after all. This can happen for a :term:`Legacy Network` where 2FA
|
54
|
+
is optional.
|
55
|
+
"""
|
56
|
+
|
57
|
+
pass
|
58
|
+
|
59
|
+
|
60
|
+
class Register(Command):
|
61
|
+
NAME = "📝 Register to the gateway"
|
62
|
+
HELP = "Link your JID to this gateway"
|
63
|
+
NODE = "jabber:iq:register"
|
64
|
+
CHAT_COMMAND = "register"
|
65
|
+
ACCESS = CommandAccess.NON_USER
|
66
|
+
|
67
|
+
SUCCESS_MESSAGE = "Success, welcome!"
|
68
|
+
|
69
|
+
def _finalize(self, user: GatewayUser):
|
70
|
+
user.commit()
|
71
|
+
self.xmpp.event("user_register", Iq(sfrom=user.jid))
|
72
|
+
return self.SUCCESS_MESSAGE
|
73
|
+
|
74
|
+
async def run(self, _session, _ifrom, *_):
|
75
|
+
return Form(
|
76
|
+
title=f"Registration to '{self.xmpp.COMPONENT_NAME}'",
|
77
|
+
instructions=self.xmpp.REGISTRATION_INSTRUCTIONS,
|
78
|
+
fields=self.xmpp.REGISTRATION_FIELDS,
|
79
|
+
handler=self.register,
|
80
|
+
)
|
81
|
+
|
82
|
+
async def register(self, form_values: dict[str, Any], _session, ifrom: JID):
|
83
|
+
two_fa_needed = True
|
84
|
+
try:
|
85
|
+
await self.xmpp.user_prevalidate(ifrom, form_values)
|
86
|
+
except ValueError as e:
|
87
|
+
raise XMPPError("bad-request", str(e))
|
88
|
+
except TwoFactorNotRequired:
|
89
|
+
if self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE:
|
90
|
+
two_fa_needed = False
|
91
|
+
else:
|
92
|
+
raise
|
93
|
+
|
94
|
+
user = GatewayUser(
|
95
|
+
bare_jid=ifrom.bare,
|
96
|
+
registration_form=form_values,
|
97
|
+
registration_date=datetime.now(),
|
98
|
+
)
|
99
|
+
|
100
|
+
if self.xmpp.REGISTRATION_TYPE == RegistrationType.SINGLE_STEP_FORM or (
|
101
|
+
self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE
|
102
|
+
and not two_fa_needed
|
103
|
+
):
|
104
|
+
return self._finalize(user)
|
105
|
+
|
106
|
+
if self.xmpp.REGISTRATION_TYPE == RegistrationType.TWO_FACTOR_CODE:
|
107
|
+
return Form(
|
108
|
+
title=self.xmpp.REGISTRATION_2FA_TITLE,
|
109
|
+
instructions=self.xmpp.REGISTRATION_2FA_INSTRUCTIONS,
|
110
|
+
fields=[FormField("code", label="Code", required=True)],
|
111
|
+
handler=functools.partial(self.two_fa, user=user),
|
112
|
+
)
|
113
|
+
|
114
|
+
elif self.xmpp.REGISTRATION_TYPE == RegistrationType.QRCODE:
|
115
|
+
self.xmpp.qr_pending_registrations[ # type:ignore
|
116
|
+
user.bare_jid
|
117
|
+
] = (
|
118
|
+
self.xmpp.loop.create_future()
|
119
|
+
)
|
120
|
+
qr_text = await self.xmpp.get_qr_text(user)
|
121
|
+
qr = qrcode.make(qr_text)
|
122
|
+
with tempfile.NamedTemporaryFile(
|
123
|
+
suffix=".png", delete=config.NO_UPLOAD_METHOD != "move"
|
124
|
+
) as f:
|
125
|
+
qr.save(f.name)
|
126
|
+
img_url, _ = await self.xmpp.send_file(f.name, mto=ifrom)
|
127
|
+
if img_url is None:
|
128
|
+
raise XMPPError(
|
129
|
+
"internal-server-error", "Slidge cannot send attachments"
|
130
|
+
)
|
131
|
+
self.xmpp.send_text(qr_text, mto=ifrom)
|
132
|
+
return Form(
|
133
|
+
title="Flash this",
|
134
|
+
instructions="Flash this QR in the appropriate place",
|
135
|
+
fields=[
|
136
|
+
FormField(
|
137
|
+
"qr_img",
|
138
|
+
type="fixed",
|
139
|
+
value=qr_text,
|
140
|
+
image_url=img_url,
|
141
|
+
),
|
142
|
+
FormField(
|
143
|
+
"qr_text",
|
144
|
+
type="fixed",
|
145
|
+
value=qr_text,
|
146
|
+
label="Text encoded in the QR code",
|
147
|
+
),
|
148
|
+
FormField(
|
149
|
+
"qr_img_url",
|
150
|
+
type="fixed",
|
151
|
+
value=img_url,
|
152
|
+
label="URL of the QR code image",
|
153
|
+
),
|
154
|
+
],
|
155
|
+
handler=functools.partial(self.qr, user=user),
|
156
|
+
)
|
157
|
+
|
158
|
+
async def two_fa(
|
159
|
+
self, form_values: FormValues, _session, _ifrom, user: GatewayUser
|
160
|
+
):
|
161
|
+
assert isinstance(form_values["code"], str)
|
162
|
+
await self.xmpp.validate_two_factor_code(user, form_values["code"])
|
163
|
+
return self._finalize(user)
|
164
|
+
|
165
|
+
async def qr(self, _form_values: FormValues, _session, _ifrom, user: GatewayUser):
|
166
|
+
try:
|
167
|
+
await asyncio.wait_for(
|
168
|
+
self.xmpp.qr_pending_registrations[user.bare_jid], # type:ignore
|
169
|
+
config.QR_TIMEOUT,
|
170
|
+
)
|
171
|
+
except asyncio.TimeoutError:
|
172
|
+
raise XMPPError(
|
173
|
+
"remote-server-timeout",
|
174
|
+
(
|
175
|
+
"It does not seem that the QR code was correctly used, "
|
176
|
+
"or you took too much time"
|
177
|
+
),
|
178
|
+
)
|
179
|
+
return self._finalize(user)
|