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
@@ -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)