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.
- slidge/__init__.py +61 -0
- slidge/__main__.py +192 -0
- 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 +3 -0
- slidge/core/cache.py +183 -0
- slidge/core/config.py +209 -0
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +892 -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 +757 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +19 -0
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +31 -0
- slidge/core/mixins/disco.py +130 -0
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +398 -0
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +217 -0
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +525 -0
- slidge/core/session.py +752 -0
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +440 -0
- slidge/group/room.py +1095 -0
- slidge/migration.py +18 -0
- slidge/py.typed +0 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/slixfix/link_preview/__init__.py +10 -0
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/slixfix/xep_0077/__init__.py +10 -0
- slidge/slixfix/xep_0077/register.py +289 -0
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/slixfix/xep_0100/__init__.py +5 -0
- slidge/slixfix/xep_0100/gateway.py +121 -0
- slidge/slixfix/xep_0100/stanza.py +9 -0
- 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/slixfix/xep_0356_old/__init__.py +7 -0
- slidge/slixfix/xep_0356_old/privilege.py +167 -0
- slidge/slixfix/xep_0356_old/stanza.py +44 -0
- 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 +15 -0
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +206 -0
- slidge/util/db.py +229 -0
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +295 -0
- slidge/util/types.py +180 -0
- slidge/util/util.py +295 -0
- slidge-0.1.0.dist-info/LICENSE +661 -0
- slidge-0.1.0.dist-info/METADATA +109 -0
- slidge-0.1.0.dist-info/RECORD +96 -0
- slidge-0.1.0.dist-info/WHEEL +4 -0
- slidge-0.1.0.dist-info/entry_points.txt +3 -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)
|