Coverage for slidge/core/gateway.py: 66%

357 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-11-07 05:11 +0000

1""" 

2This module extends slixmpp.ComponentXMPP to make writing new LegacyClients easier 

3""" 

4 

5import asyncio 

6import logging 

7import re 

8import tempfile 

9from copy import copy 

10from datetime import datetime 

11from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Sequence, Union 

12 

13import aiohttp 

14import qrcode 

15from slixmpp import JID, ComponentXMPP, Iq, Message, Presence 

16from slixmpp.exceptions import IqError, IqTimeout, XMPPError 

17from slixmpp.plugins.xep_0060.stanza import OwnerAffiliation 

18from slixmpp.types import MessageTypes 

19from slixmpp.xmlstream.xmlstream import NotConnectedError 

20 

21from slidge import command # noqa: F401 

22from slidge.command.adhoc import AdhocProvider 

23from slidge.command.admin import Exec 

24from slidge.command.base import Command, FormField 

25from slidge.command.chat_command import ChatCommandProvider 

26from slidge.command.register import RegistrationType 

27from slidge.core import config 

28from slidge.core.dispatcher.session_dispatcher import SessionDispatcher 

29from slidge.core.mixins import MessageMixin 

30from slidge.core.pubsub import PubSubComponent 

31from slidge.core.session import BaseSession 

32from slidge.db import GatewayUser, SlidgeStore 

33from slidge.db.avatar import avatar_cache 

34from slidge.slixfix.delivery_receipt import DeliveryReceipt 

35from slidge.slixfix.roster import RosterBackend 

36from slidge.util import ABCSubclassableOnceAtMost 

37from slidge.util.types import AvatarType, MessageOrPresenceTypeVar 

38from slidge.util.util import timeit 

39 

40if TYPE_CHECKING: 

41 pass 

42 

43 

44class BaseGateway( 

45 ComponentXMPP, 

46 MessageMixin, 

47 metaclass=ABCSubclassableOnceAtMost, 

48): 

49 """ 

50 The gateway component, handling registrations and un-registrations. 

51 

52 On slidge launch, a singleton is instantiated, and it will be made available 

53 to public classes such :class:`.LegacyContact` or :class:`.BaseSession` as the 

54 ``.xmpp`` attribute. 

55 

56 Must be subclassed by a legacy module to set up various aspects of the XMPP 

57 component behaviour, such as its display name or welcome message, via 

58 class attributes :attr:`.COMPONENT_NAME` :attr:`.WELCOME_MESSAGE`. 

59 

60 Abstract methods related to the registration process must be overriden 

61 for a functional :term:`Legacy Module`: 

62 

63 - :meth:`.validate` 

64 - :meth:`.validate_two_factor_code` 

65 - :meth:`.get_qr_text` 

66 - :meth:`.confirm_qr` 

67 

68 NB: Not all of these must be overridden, it depends on the 

69 :attr:`REGISTRATION_TYPE`. 

70 

71 The other methods, such as :meth:`.send_text` or :meth:`.react` are the same 

72 as those of :class:`.LegacyContact` and :class:`.LegacyParticipant`, because 

73 the component itself is also a "messaging actor", ie, an :term:`XMPP Entity`. 

74 For these methods, you need to specify the JID of the recipient with the 

75 `mto` parameter. 

76 

77 Since it inherits from :class:`slixmpp.componentxmpp.ComponentXMPP`,you also 

78 have a hand on low-level XMPP interactions via slixmpp methods, e.g.: 

79 

80 .. code-block:: python 

81 

82 self.send_presence( 

83 pfrom="somebody@component.example.com", 

84 pto="someonwelse@anotherexample.com", 

85 ) 

86 

87 However, you should not need to do so often since the classes of the plugin 

88 API provides higher level abstractions around most commonly needed use-cases, such 

89 as sending messages, or displaying a custom status. 

90 

91 """ 

92 

93 COMPONENT_NAME: str = NotImplemented 

94 """Name of the component, as seen in service discovery by XMPP clients""" 

95 COMPONENT_TYPE: str = "" 

96 """Type of the gateway, should follow https://xmpp.org/registrar/disco-categories.html""" 

97 COMPONENT_AVATAR: Optional[AvatarType] = None 

98 """ 

99 Path, bytes or URL used by the component as an avatar. 

100 """ 

101 

102 REGISTRATION_FIELDS: Sequence[FormField] = [ 

103 FormField(var="username", label="User name", required=True), 

104 FormField(var="password", label="Password", required=True, private=True), 

105 ] 

106 """ 

107 Iterable of fields presented to the gateway user when registering using :xep:`0077` 

108 `extended <https://xmpp.org/extensions/xep-0077.html#extensibility>`_ by :xep:`0004`. 

109 """ 

110 REGISTRATION_INSTRUCTIONS: str = "Enter your credentials" 

111 """ 

112 The text presented to a user that wants to register (or modify) their legacy account 

113 configuration. 

114 """ 

115 REGISTRATION_TYPE: RegistrationType = RegistrationType.SINGLE_STEP_FORM 

116 """ 

117 This attribute determines how users register to the gateway, ie, how they 

118 login to the :term:`legacy service <Legacy Service>`. 

119 The credentials are then stored persistently, so this process should happen 

120 once per user (unless they unregister). 

121 

122 The registration process always start with a basic data form (:xep:`0004`) 

123 presented to the user. 

124 But the legacy login flow might require something more sophisticated, see 

125 :class:`.RegistrationType` for more details. 

126 """ 

127 

128 REGISTRATION_2FA_TITLE = "Enter your 2FA code" 

129 REGISTRATION_2FA_INSTRUCTIONS = ( 

130 "You should have received something via email or SMS, or something" 

131 ) 

132 REGISTRATION_QR_INSTRUCTIONS = "Flash this code or follow this link" 

133 

134 PREFERENCES = [ 

135 FormField( 

136 var="sync_presence", 

137 label="Propagate your XMPP presence to the legacy network.", 

138 value="true", 

139 required=True, 

140 type="boolean", 

141 ), 

142 FormField( 

143 var="sync_avatar", 

144 label="Propagate your XMPP avatar to the legacy network.", 

145 value="true", 

146 required=True, 

147 type="boolean", 

148 ), 

149 ] 

150 

151 ROSTER_GROUP: str = "slidge" 

152 """ 

153 Name of the group assigned to a :class:`.LegacyContact` automagically 

154 added to the :term:`User`'s roster with :meth:`.LegacyContact.add_to_roster`. 

155 """ 

156 WELCOME_MESSAGE = ( 

157 "Thank you for registering. Type 'help' to list the available commands, " 

158 "or just start messaging away!" 

159 ) 

160 """ 

161 A welcome message displayed to users on registration. 

162 This is useful notably for clients that don't consider component JIDs as a 

163 valid recipient in their UI, yet still open a functional chat window on 

164 incoming messages from components. 

165 """ 

166 

167 SEARCH_FIELDS: Sequence[FormField] = [ 

168 FormField(var="first", label="First name", required=True), 

169 FormField(var="last", label="Last name", required=True), 

170 FormField(var="phone", label="Phone number", required=False), 

171 ] 

172 """ 

173 Fields used for searching items via the component, through :xep:`0055` (jabber search). 

174 A common use case is to allow users to search for legacy contacts by something else than 

175 their usernames, eg their phone number. 

176 

177 Plugins should implement search by overriding :meth:`.BaseSession.search` 

178 (restricted to registered users). 

179 

180 If there is only one field, it can also be used via the ``jabber:iq:gateway`` protocol 

181 described in :xep:`0100`. Limitation: this only works if the search request returns 

182 one result item, and if this item has a 'jid' var. 

183 """ 

184 SEARCH_TITLE: str = "Search for legacy contacts" 

185 """ 

186 Title of the search form. 

187 """ 

188 SEARCH_INSTRUCTIONS: str = "" 

189 """ 

190 Instructions of the search form. 

191 """ 

192 

193 MARK_ALL_MESSAGES = False 

194 """ 

195 Set this to True for :term:`legacy networks <Legacy Network>` that expects 

196 read marks for *all* messages and not just the latest one that was read 

197 (as most XMPP clients will only send a read mark for the latest msg). 

198 """ 

199 

200 PROPER_RECEIPTS = False 

201 """ 

202 Set this to True if the legacy service provides a real equivalent of message delivery receipts 

203 (:xep:`0184`), meaning that there is an event thrown when the actual device of a contact receives 

204 a message. Make sure to call Contact.received() adequately if this is set to True. 

205 """ 

206 

207 GROUPS = False 

208 

209 mtype: MessageTypes = "chat" 

210 is_group = False 

211 _can_send_carbon = False 

212 store: SlidgeStore 

213 avatar_pk: int 

214 

215 AVATAR_ID_TYPE: Callable[[str], Any] = str 

216 """ 

217 Modify this if the legacy network uses unique avatar IDs that are not strings. 

218 

219 This is required because we store those IDs as TEXT in the persistent SQL DB. 

220 The callable specified here will receive is responsible for converting the 

221 serialised-as-text version of the avatar unique ID back to the proper type. 

222 Common example: ``int``. 

223 """ 

224 # FIXME: do we really need this since we have session.xmpp_to_legacy_msg_id? 

225 # (maybe we do) 

226 LEGACY_MSG_ID_TYPE: Callable[[str], Any] = str 

227 """ 

228 Modify this if the legacy network uses unique message IDs that are not strings. 

229 

230 This is required because we store those IDs as TEXT in the persistent SQL DB. 

231 The callable specified here will receive is responsible for converting the 

232 serialised-as-text version of the message unique ID back to the proper type. 

233 Common example: ``int``. 

234 """ 

235 LEGACY_CONTACT_ID_TYPE: Callable[[str], Any] = str 

236 """ 

237 Modify this if the legacy network uses unique contact IDs that are not strings. 

238 

239 This is required because we store those IDs as TEXT in the persistent SQL DB. 

240 The callable specified here is responsible for converting the 

241 serialised-as-text version of the contact unique ID back to the proper type. 

242 Common example: ``int``. 

243 """ 

244 LEGACY_ROOM_ID_TYPE: Callable[[str], Any] = str 

245 """ 

246 Modify this if the legacy network uses unique room IDs that are not strings. 

247 

248 This is required because we store those IDs as TEXT in the persistent SQL DB. 

249 The callable specified here is responsible for converting the 

250 serialised-as-text version of the room unique ID back to the proper type. 

251 Common example: ``int``. 

252 """ 

253 

254 http: aiohttp.ClientSession 

255 

256 def __init__(self): 

257 self.log = log 

258 self.datetime_started = datetime.now() 

259 self.xmpp = self # ugly hack to work with the BaseSender mixin :/ 

260 self.default_ns = "jabber:component:accept" 

261 super().__init__( 

262 config.JID, 

263 config.SECRET, 

264 config.SERVER, 

265 config.PORT, 

266 plugin_whitelist=SLIXMPP_PLUGINS, 

267 plugin_config={ 

268 "xep_0077": { 

269 "form_fields": None, 

270 "form_instructions": self.REGISTRATION_INSTRUCTIONS, 

271 "enable_subscription": self.REGISTRATION_TYPE 

272 == RegistrationType.SINGLE_STEP_FORM, 

273 }, 

274 "xep_0100": { 

275 "component_name": self.COMPONENT_NAME, 

276 "type": self.COMPONENT_TYPE, 

277 }, 

278 "xep_0184": { 

279 "auto_ack": False, 

280 "auto_request": False, 

281 }, 

282 "xep_0363": { 

283 "upload_service": config.UPLOAD_SERVICE, 

284 }, 

285 }, 

286 fix_error_ns=True, 

287 ) 

288 self.loop.set_exception_handler(self.__exception_handler) 

289 self.loop.create_task(self.__set_http()) 

290 self.has_crashed: bool = False 

291 self.use_origin_id = False 

292 

293 self.jid_validator: re.Pattern = re.compile(config.USER_JID_VALIDATOR) 

294 self.qr_pending_registrations = dict[str, asyncio.Future[Optional[dict]]]() 

295 

296 self.session_cls: BaseSession = BaseSession.get_unique_subclass() 

297 self.session_cls.xmpp = self 

298 

299 from ..group.room import LegacyMUC 

300 

301 LegacyMUC.get_self_or_unique_subclass().xmpp = self 

302 

303 self.get_session_from_stanza: Callable[ 

304 [Union[Message, Presence, Iq]], BaseSession 

305 ] = self.session_cls.from_stanza # type: ignore 

306 self.get_session_from_user: Callable[[GatewayUser], BaseSession] = ( 

307 self.session_cls.from_user 

308 ) 

309 

310 self.register_plugins() 

311 self.__register_slixmpp_events() 

312 self.__register_slixmpp_api() 

313 self.roster.set_backend(RosterBackend(self)) 

314 

315 self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME}) 

316 self.pubsub: PubSubComponent = self["pubsub"] 

317 self.delivery_receipt: DeliveryReceipt = DeliveryReceipt(self) 

318 

319 # with this we receive user avatar updates 

320 self.plugin["xep_0030"].add_feature("urn:xmpp:avatar:metadata+notify") 

321 

322 self.plugin["xep_0030"].add_feature("urn:xmpp:chat-markers:0") 

323 

324 if self.GROUPS: 

325 self.plugin["xep_0030"].add_feature("http://jabber.org/protocol/muc") 

326 self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2") 

327 self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2#extended") 

328 self.plugin["xep_0030"].add_feature(self.plugin["xep_0421"].namespace) 

329 self.plugin["xep_0030"].add_feature(self["xep_0317"].stanza.NS) 

330 self.plugin["xep_0030"].add_identity( 

331 category="conference", 

332 name=self.COMPONENT_NAME, 

333 itype="text", 

334 jid=self.boundjid, 

335 ) 

336 

337 # why does mypy need these type annotations? no idea 

338 self.__adhoc_handler: AdhocProvider = AdhocProvider(self) 

339 self.__chat_commands_handler: ChatCommandProvider = ChatCommandProvider(self) 

340 

341 self.__dispatcher = SessionDispatcher(self) 

342 

343 self.__register_commands() 

344 

345 MessageMixin.__init__(self) # ComponentXMPP does not call super().__init__() 

346 

347 async def __set_http(self): 

348 self.http = aiohttp.ClientSession() 

349 if getattr(self, "_test_mode", False): 

350 return 

351 avatar_cache.http = self.http 

352 

353 def __register_commands(self): 

354 for cls in Command.subclasses: 

355 if any(x is NotImplemented for x in [cls.CHAT_COMMAND, cls.NODE, cls.NAME]): 

356 log.debug("Not adding command '%s' because it looks abstract", cls) 

357 continue 

358 if cls is Exec: 

359 if config.DEV_MODE: 

360 log.warning(r"/!\ DEV MODE ENABLED /!\\") 

361 else: 

362 continue 

363 c = cls(self) 

364 log.debug("Registering %s", cls) 

365 self.__adhoc_handler.register(c) 

366 self.__chat_commands_handler.register(c) 

367 

368 def __exception_handler(self, loop: asyncio.AbstractEventLoop, context): 

369 """ 

370 Called when a task created by loop.create_task() raises an Exception 

371 

372 :param loop: 

373 :param context: 

374 :return: 

375 """ 

376 log.debug("Context in the exception handler: %s", context) 

377 exc = context.get("exception") 

378 if exc is None: 

379 log.debug("No exception in this context: %s", context) 

380 elif isinstance(exc, SystemExit): 

381 log.debug("SystemExit called in an asyncio task") 

382 else: 

383 log.error("Crash in an asyncio task: %s", context) 

384 log.exception("Crash in task", exc_info=exc) 

385 self.has_crashed = True 

386 loop.stop() 

387 

388 def __register_slixmpp_events(self): 

389 self.del_event_handler("presence_subscribe", self._handle_subscribe) 

390 self.del_event_handler("presence_unsubscribe", self._handle_unsubscribe) 

391 self.del_event_handler("presence_subscribed", self._handle_subscribed) 

392 self.del_event_handler("presence_unsubscribed", self._handle_unsubscribed) 

393 self.del_event_handler( 

394 "roster_subscription_request", self._handle_new_subscription 

395 ) 

396 self.del_event_handler("presence_probe", self._handle_probe) 

397 self.add_event_handler("session_start", self.__on_session_start) 

398 self.add_event_handler("disconnected", self.connect) 

399 

400 def __register_slixmpp_api(self) -> None: 

401 self.plugin["xep_0231"].api.register(self.store.bob.get_bob, "get_bob") 

402 self.plugin["xep_0231"].api.register(self.store.bob.set_bob, "set_bob") 

403 self.plugin["xep_0231"].api.register(self.store.bob.del_bob, "del_bob") 

404 

405 @property # type: ignore 

406 def jid(self): 

407 # Override to avoid slixmpp deprecation warnings. 

408 return self.boundjid 

409 

410 async def __on_session_start(self, event): 

411 log.debug("Gateway session start: %s", event) 

412 

413 # prevents XMPP clients from considering the gateway as an HTTP upload 

414 disco = self.plugin["xep_0030"] 

415 await disco.del_feature(feature="urn:xmpp:http:upload:0", jid=self.boundjid) 

416 await self.plugin["xep_0115"].update_caps(jid=self.boundjid) 

417 

418 if self.COMPONENT_AVATAR is not None: 

419 cached_avatar = await avatar_cache.convert_or_get(self.COMPONENT_AVATAR) 

420 self.avatar_pk = cached_avatar.pk 

421 else: 

422 cached_avatar = None 

423 

424 for user in self.store.users.get_all(): 

425 # TODO: before this, we should check if the user has removed us from their roster 

426 # while we were offline and trigger unregister from there. Presence probe does not seem 

427 # to work in this case, there must be another way. privileged entity could be used 

428 # as last resort. 

429 try: 

430 await self["xep_0100"].add_component_to_roster(user.jid) 

431 await self.__add_component_to_mds_whitelist(user.jid) 

432 except (IqError, IqTimeout) as e: 

433 # TODO: remove the user when this happens? or at least 

434 # this can happen when the user has unsubscribed from the XMPP server 

435 log.warning( 

436 "Error with user %s, not logging them automatically", 

437 user, 

438 exc_info=e, 

439 ) 

440 continue 

441 self.send_presence( 

442 pto=user.jid.bare, ptype="probe" 

443 ) # ensure we get all resources for user 

444 session = self.session_cls.from_user(user) 

445 session.create_task(self.login_wrap(session)) 

446 if cached_avatar is not None: 

447 await self.pubsub.broadcast_avatar( 

448 self.boundjid.bare, session.user_jid, cached_avatar 

449 ) 

450 

451 log.info("Slidge has successfully started") 

452 

453 async def __add_component_to_mds_whitelist(self, user_jid: JID): 

454 # Uses privileged entity to add ourselves to the whitelist of the PEP 

455 # MDS node so we receive MDS events 

456 iq_creation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set") 

457 iq_creation["pubsub"]["create"]["node"] = self["xep_0490"].stanza.NS 

458 

459 try: 

460 await self["xep_0356"].send_privileged_iq(iq_creation) 

461 except PermissionError: 

462 log.warning( 

463 "IQ privileges not granted for pubsub namespace, we cannot " 

464 "create the MDS node of %s", 

465 user_jid, 

466 ) 

467 except (IqError, IqTimeout) as e: 

468 # conflict this means the node already exists, we can ignore that 

469 if e.condition != "conflict": 

470 log.exception( 

471 "Could not create the MDS node of %s", user_jid, exc_info=e 

472 ) 

473 except Exception as e: 

474 log.exception( 

475 "Error while trying to create to the MDS node of %s", 

476 user_jid, 

477 exc_info=e, 

478 ) 

479 

480 iq_affiliation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set") 

481 iq_affiliation["pubsub_owner"]["affiliations"]["node"] = self[ 

482 "xep_0490" 

483 ].stanza.NS 

484 

485 aff = OwnerAffiliation() 

486 aff["jid"] = self.boundjid.bare 

487 aff["affiliation"] = "member" 

488 iq_affiliation["pubsub_owner"]["affiliations"].append(aff) 

489 

490 try: 

491 await self["xep_0356"].send_privileged_iq(iq_affiliation) 

492 except PermissionError: 

493 log.warning( 

494 "IQ privileges not granted for pubsub#owner namespace, we cannot " 

495 "listen to the MDS events of %s", 

496 user_jid, 

497 ) 

498 except Exception as e: 

499 log.exception( 

500 "Error while trying to subscribe to the MDS node of %s", 

501 user_jid, 

502 exc_info=e, 

503 ) 

504 

505 @timeit 

506 async def login_wrap(self, session: "BaseSession"): 

507 session.send_gateway_status("Logging in…", show="dnd") 

508 try: 

509 status = await session.login() 

510 except Exception as e: 

511 log.warning("Login problem for %s", session.user_jid, exc_info=e) 

512 log.exception(e) 

513 session.send_gateway_status(f"Could not login: {e}", show="busy") 

514 session.send_gateway_message( 

515 "You are not connected to this gateway! " 

516 f"Maybe this message will tell you why: {e}" 

517 ) 

518 return 

519 

520 log.info("Login success for %s", session.user_jid) 

521 session.logged = True 

522 session.send_gateway_status("Syncing contacts…", show="dnd") 

523 await session.contacts._fill() 

524 if not (r := session.contacts.ready).done(): 

525 r.set_result(True) 

526 if self.GROUPS: 

527 session.send_gateway_status("Syncing groups…", show="dnd") 

528 await session.bookmarks.fill() 

529 if not (r := session.bookmarks.ready).done(): 

530 r.set_result(True) 

531 for c in session.contacts: 

532 # we need to receive presences directed at the contacts, in 

533 # order to send pubsub events for their +notify features 

534 self.send_presence(pfrom=c.jid, pto=session.user_jid.bare, ptype="probe") 

535 if status is None: 

536 session.send_gateway_status("Logged in", show="chat") 

537 else: 

538 session.send_gateway_status(status, show="chat") 

539 if session.user.preferences.get("sync_avatar", False): 

540 session.create_task(self.fetch_user_avatar(session)) 

541 else: 

542 self.xmpp.store.users.set_avatar_hash(session.user_pk, None) 

543 

544 async def fetch_user_avatar(self, session: BaseSession): 

545 try: 

546 iq = await self.xmpp.plugin["xep_0060"].get_items( 

547 session.user_jid.bare, 

548 self.xmpp.plugin["xep_0084"].stanza.MetaData.namespace, 

549 ifrom=self.boundjid.bare, 

550 ) 

551 except (IqError, IqTimeout): 

552 self.xmpp.store.users.set_avatar_hash(session.user_pk, None) 

553 return 

554 await self.__dispatcher.on_avatar_metadata_info( 

555 session, iq["pubsub"]["items"]["item"]["avatar_metadata"]["info"] 

556 ) 

557 

558 def _send( 

559 self, stanza: MessageOrPresenceTypeVar, **send_kwargs 

560 ) -> MessageOrPresenceTypeVar: 

561 stanza.set_from(self.boundjid.bare) 

562 if mto := send_kwargs.get("mto"): 

563 stanza.set_to(mto) 

564 stanza.send() 

565 return stanza 

566 

567 def raise_if_not_allowed_jid(self, jid: JID): 

568 if not self.jid_validator.match(jid.bare): 

569 raise XMPPError( 

570 condition="not-allowed", 

571 text="Your account is not allowed to use this gateway.", 

572 ) 

573 

574 def send_raw(self, data: Union[str, bytes]): 

575 # overridden from XMLStream to strip base64-encoded data from the logs 

576 # to make them more readable. 

577 if log.isEnabledFor(level=logging.DEBUG): 

578 if isinstance(data, str): 

579 stripped = copy(data) 

580 else: 

581 stripped = data.decode("utf-8") 

582 # there is probably a way to do that in a single RE, 

583 # but since it's only for debugging, the perf penalty 

584 # does not matter much 

585 for el in LOG_STRIP_ELEMENTS: 

586 stripped = re.sub( 

587 f"(<{el}.*?>)(.*)(</{el}>)", 

588 "\1[STRIPPED]\3", 

589 stripped, 

590 flags=re.DOTALL | re.IGNORECASE, 

591 ) 

592 log.debug("SEND: %s", stripped) 

593 if not self.transport: 

594 raise NotConnectedError() 

595 if isinstance(data, str): 

596 data = data.encode("utf-8") 

597 self.transport.write(data) 

598 

599 def get_session_from_jid(self, j: JID): 

600 try: 

601 return self.session_cls.from_jid(j) 

602 except XMPPError: 

603 pass 

604 

605 def exception(self, exception: Exception): 

606 # """ 

607 # Called when a task created by slixmpp's internal (eg, on slix events) raises an Exception. 

608 # 

609 # Stop the event loop and exit on unhandled exception. 

610 # 

611 # The default :class:`slixmpp.basexmpp.BaseXMPP` behaviour is just to 

612 # log the exception, but we want to avoid undefined behaviour. 

613 # 

614 # :param exception: An unhandled :class:`Exception` object. 

615 # """ 

616 if isinstance(exception, IqError): 

617 iq = exception.iq 

618 log.error("%s: %s", iq["error"]["condition"], iq["error"]["text"]) 

619 log.warning("You should catch IqError exceptions") 

620 elif isinstance(exception, IqTimeout): 

621 iq = exception.iq 

622 log.error("Request timed out: %s", iq) 

623 log.warning("You should catch IqTimeout exceptions") 

624 elif isinstance(exception, SyntaxError): 

625 # Hide stream parsing errors that occur when the 

626 # stream is disconnected (they've been handled, we 

627 # don't need to make a mess in the logs). 

628 pass 

629 else: 

630 if exception: 

631 log.exception(exception) 

632 self.loop.stop() 

633 exit(1) 

634 

635 def re_login(self, session: "BaseSession"): 

636 async def w(): 

637 session.cancel_all_tasks() 

638 await session.logout() 

639 await self.login_wrap(session) 

640 

641 session.create_task(w()) 

642 

643 async def make_registration_form(self, _jid, _node, _ifrom, iq: Iq): 

644 self.raise_if_not_allowed_jid(iq.get_from()) 

645 reg = iq["register"] 

646 user = self.store.users.get_by_stanza(iq) 

647 log.debug("User found: %s", user) 

648 

649 form = reg["form"] 

650 form.add_field( 

651 "FORM_TYPE", 

652 ftype="hidden", 

653 value="jabber:iq:register", 

654 ) 

655 form["title"] = f"Registration to '{self.COMPONENT_NAME}'" 

656 form["instructions"] = self.REGISTRATION_INSTRUCTIONS 

657 

658 if user is not None: 

659 reg["registered"] = False 

660 form.add_field( 

661 "remove", 

662 label="Remove my registration", 

663 required=True, 

664 ftype="boolean", 

665 value=False, 

666 ) 

667 

668 for field in self.REGISTRATION_FIELDS: 

669 if field.var in reg.interfaces: 

670 val = None if user is None else user.get(field.var) 

671 if val is None: 

672 reg.add_field(field.var) 

673 else: 

674 reg[field.var] = val 

675 

676 reg["instructions"] = self.REGISTRATION_INSTRUCTIONS 

677 

678 for field in self.REGISTRATION_FIELDS: 

679 form.add_field( 

680 field.var, 

681 label=field.label, 

682 required=field.required, 

683 ftype=field.type, 

684 options=field.options, 

685 value=field.value if user is None else user.get(field.var, field.value), 

686 ) 

687 

688 reply = iq.reply() 

689 reply.set_payload(reg) 

690 return reply 

691 

692 async def user_prevalidate( 

693 self, ifrom: JID, form_dict: dict[str, Optional[str]] 

694 ) -> Optional[Mapping]: 

695 # Pre validate a registration form using the content of self.REGISTRATION_FIELDS 

696 # before passing it to the plugin custom validation logic. 

697 for field in self.REGISTRATION_FIELDS: 

698 if field.required and not form_dict.get(field.var): 

699 raise ValueError(f"Missing field: '{field.label}'") 

700 

701 return await self.validate(ifrom, form_dict) 

702 

703 async def validate( 

704 self, user_jid: JID, registration_form: dict[str, Optional[str]] 

705 ) -> Optional[Mapping]: 

706 """ 

707 Validate a user's initial registration form. 

708 

709 Should raise the appropriate :class:`slixmpp.exceptions.XMPPError` 

710 if the registration does not allow to continue the registration process. 

711 

712 If :py:attr:`REGISTRATION_TYPE` is a 

713 :attr:`.RegistrationType.SINGLE_STEP_FORM`, 

714 this method should raise something if it wasn't possible to successfully 

715 log in to the legacy service with the registration form content. 

716 

717 It is also used for other types of :py:attr:`REGISTRATION_TYPE` too, since 

718 the first step is always a form. If :attr:`.REGISTRATION_FIELDS` is an 

719 empty list (ie, it declares no :class:`.FormField`), the "form" is 

720 effectively a confirmation dialog displaying 

721 :attr:`.REGISTRATION_INSTRUCTIONS`. 

722 

723 :param user_jid: JID of the user that has just registered 

724 :param registration_form: A dict where keys are the :attr:`.FormField.var` attributes 

725 of the :attr:`.BaseGateway.REGISTRATION_FIELDS` iterable. 

726 This dict can be modified and will be accessible as the ``legacy_module_data`` 

727 of the 

728 

729 :return : A dict that will be stored as the persistent "legacy_module_data" 

730 for this user. If you don't return anything here, the whole registration_form 

731 content will be stored. 

732 """ 

733 raise NotImplementedError 

734 

735 async def validate_two_factor_code( 

736 self, user: GatewayUser, code: str 

737 ) -> Optional[dict]: 

738 """ 

739 Called when the user enters their 2FA code. 

740 

741 Should raise the appropriate :class:`slixmpp.exceptions.XMPPError` 

742 if the login fails, and return successfully otherwise. 

743 

744 Only used when :attr:`REGISTRATION_TYPE` is 

745 :attr:`.RegistrationType.TWO_FACTOR_CODE`. 

746 

747 :param user: The :class:`.GatewayUser` whose registration is pending 

748 Use their :attr:`.GatewayUser.bare_jid` and/or 

749 :attr:`.registration_form` attributes to get what you need. 

750 :param code: The code they entered, either via "chatbot" message or 

751 adhoc command 

752 

753 :return : A dict which keys and values will be added to the persistent "legacy_module_data" 

754 for this user. 

755 """ 

756 raise NotImplementedError 

757 

758 async def get_qr_text(self, user: GatewayUser) -> str: 

759 """ 

760 This is where slidge gets the QR code content for the QR-based 

761 registration process. It will turn it into a QR code image and send it 

762 to the not-yet-fully-registered :class:`.GatewayUser`. 

763 

764 Only used in when :attr:`BaseGateway.REGISTRATION_TYPE` is 

765 :attr:`.RegistrationType.QRCODE`. 

766 

767 :param user: The :class:`.GatewayUser` whose registration is pending 

768 Use their :attr:`.GatewayUser.bare_jid` and/or 

769 :attr:`.registration_form` attributes to get what you need. 

770 """ 

771 raise NotImplementedError 

772 

773 async def confirm_qr( 

774 self, 

775 user_bare_jid: str, 

776 exception: Optional[Exception] = None, 

777 legacy_data: Optional[dict] = None, 

778 ): 

779 """ 

780 This method is meant to be called to finalize QR code-based registration 

781 flows, once the legacy service confirms the QR flashing. 

782 

783 Only used in when :attr:`BaseGateway.REGISTRATION_TYPE` is 

784 :attr:`.RegistrationType.QRCODE`. 

785 

786 :param user_bare_jid: The bare JID of the almost-registered 

787 :class:`GatewayUser` instance 

788 :param exception: Optionally, an XMPPError to be raised to **not** confirm 

789 QR code flashing. 

790 :param legacy_data: dict which keys and values will be added to the persistent 

791 "legacy_module_data" for this user. 

792 """ 

793 fut = self.qr_pending_registrations[user_bare_jid] 

794 if exception is None: 

795 fut.set_result(legacy_data) 

796 else: 

797 fut.set_exception(exception) 

798 

799 async def unregister_user(self, user: GatewayUser): 

800 self.send_presence( 

801 pshow="busy", pstatus="You are not registered to this gateway anymore." 

802 ) 

803 await self.xmpp.plugin["xep_0077"].api["user_remove"](None, None, user.jid) 

804 await self.xmpp.session_cls.kill_by_jid(user.jid) 

805 

806 async def unregister(self, user: GatewayUser): 

807 """ 

808 Optionally override this if you need to clean additional 

809 stuff after a user has been removed from the persistent user store. 

810 

811 By default, this just calls :meth:`BaseSession.logout`. 

812 

813 :param user: 

814 """ 

815 session = self.get_session_from_user(user) 

816 try: 

817 await session.logout() 

818 except NotImplementedError: 

819 pass 

820 

821 async def input( 

822 self, jid: JID, text=None, mtype: MessageTypes = "chat", **msg_kwargs 

823 ) -> str: 

824 """ 

825 Request arbitrary user input using a simple chat message, and await the result. 

826 

827 You shouldn't need to call this directly bust instead use 

828 :meth:`.BaseSession.input` to directly target a user. 

829 

830 :param jid: The JID we want input from 

831 :param text: A prompt to display for the user 

832 :param mtype: Message type 

833 :return: The user's reply 

834 """ 

835 return await self.__chat_commands_handler.input(jid, text, mtype, **msg_kwargs) 

836 

837 async def send_qr(self, text: str, **msg_kwargs): 

838 """ 

839 Sends a QR Code to a JID 

840 

841 You shouldn't need to call directly bust instead use 

842 :meth:`.BaseSession.send_qr` to directly target a user. 

843 

844 :param text: The text that will be converted to a QR Code 

845 :param msg_kwargs: Optional additional arguments to pass to 

846 :meth:`.BaseGateway.send_file`, such as the recipient of the QR, 

847 code 

848 """ 

849 qr = qrcode.make(text) 

850 with tempfile.NamedTemporaryFile( 

851 suffix=".png", delete=config.NO_UPLOAD_METHOD != "move" 

852 ) as f: 

853 qr.save(f.name) 

854 await self.send_file(f.name, **msg_kwargs) 

855 

856 def shutdown(self) -> list[asyncio.Task]: 

857 # """ 

858 # Called by the slidge entrypoint on normal exit. 

859 # 

860 # Sends offline presences from all contacts of all user sessions and from 

861 # the gateway component itself. 

862 # No need to call this manually, :func:`slidge.__main__.main` should take care of it. 

863 # """ 

864 log.debug("Shutting down") 

865 tasks = [] 

866 for user in self.store.users.get_all(): 

867 tasks.append(self.session_cls.from_jid(user.jid).shutdown()) 

868 self.send_presence(ptype="unavailable", pto=user.jid) 

869 return tasks 

870 

871 

872SLIXMPP_PLUGINS = [ 

873 "link_preview", # https://wiki.soprani.ca/CheogramApp/LinkPreviews 

874 "xep_0030", # Service discovery 

875 "xep_0045", # Multi-User Chat 

876 "xep_0050", # Adhoc commands 

877 "xep_0054", # VCard-temp (for MUC avatars) 

878 "xep_0055", # Jabber search 

879 "xep_0059", # Result Set Management 

880 "xep_0066", # Out of Band Data 

881 "xep_0071", # XHTML-IM (for stickers and custom emojis maybe later) 

882 "xep_0077", # In-band registration 

883 "xep_0084", # User Avatar 

884 "xep_0085", # Chat state notifications 

885 "xep_0100", # Gateway interaction 

886 "xep_0106", # JID Escaping 

887 "xep_0115", # Entity capabilities 

888 "xep_0122", # Data Forms Validation 

889 "xep_0153", # vCard-Based Avatars (for MUC avatars) 

890 "xep_0172", # User nickname 

891 "xep_0184", # Message Delivery Receipts 

892 "xep_0199", # XMPP Ping 

893 "xep_0221", # Data Forms Media Element 

894 "xep_0231", # Bits of Binary (for stickers and custom emojis maybe later) 

895 "xep_0249", # Direct MUC Invitations 

896 "xep_0264", # Jingle Content Thumbnails 

897 "xep_0280", # Carbons 

898 "xep_0292_provider", # VCard4 

899 "xep_0308", # Last message correction 

900 "xep_0313", # Message Archive Management 

901 "xep_0317", # Hats 

902 "xep_0319", # Last User Interaction in Presence 

903 "xep_0333", # Chat markers 

904 "xep_0334", # Message Processing Hints 

905 "xep_0356", # Privileged Entity 

906 "xep_0356_old", # Privileged Entity (old namespace) 

907 "xep_0363", # HTTP file upload 

908 "xep_0385", # Stateless in-line media sharing 

909 "xep_0402", # PEP Native Bookmarks 

910 "xep_0421", # Anonymous unique occupant identifiers for MUCs 

911 "xep_0424", # Message retraction 

912 "xep_0425", # Message moderation 

913 "xep_0444", # Message reactions 

914 "xep_0447", # Stateless File Sharing 

915 "xep_0461", # Message replies 

916 "xep_0490", # Message Displayed Synchronization 

917] 

918 

919LOG_STRIP_ELEMENTS = ["data", "binval"] 

920 

921log = logging.getLogger(__name__)