Coverage for slidge/core/session.py: 84%

234 statements  

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

1import asyncio 

2import logging 

3from typing import ( 

4 TYPE_CHECKING, 

5 Any, 

6 Generic, 

7 Iterable, 

8 NamedTuple, 

9 Optional, 

10 Union, 

11 cast, 

12) 

13 

14import aiohttp 

15from slixmpp import JID, Message 

16from slixmpp.exceptions import XMPPError 

17from slixmpp.types import PresenceShows 

18 

19from ..command import SearchResult 

20from ..contact import LegacyContact, LegacyRoster 

21from ..db.models import GatewayUser 

22from ..group.bookmarks import LegacyBookmarks 

23from ..group.room import LegacyMUC 

24from ..util import ABCSubclassableOnceAtMost 

25from ..util.types import ( 

26 LegacyGroupIdType, 

27 LegacyMessageType, 

28 LegacyThreadType, 

29 LinkPreview, 

30 Mention, 

31 PseudoPresenceShow, 

32 RecipientType, 

33 ResourceDict, 

34 Sticker, 

35) 

36from ..util.util import deprecated 

37 

38if TYPE_CHECKING: 

39 from ..group.participant import LegacyParticipant 

40 from ..util.types import Sender 

41 from .gateway import BaseGateway 

42 

43 

44class CachedPresence(NamedTuple): 

45 status: Optional[str] 

46 show: Optional[str] 

47 kwargs: dict[str, Any] 

48 

49 

50class BaseSession( 

51 Generic[LegacyMessageType, RecipientType], metaclass=ABCSubclassableOnceAtMost 

52): 

53 """ 

54 The session of a registered :term:`User`. 

55 

56 Represents a gateway user logged in to the legacy network and performing actions. 

57 

58 Will be instantiated automatically on slidge startup for each registered user, 

59 or upon registration for new (validated) users. 

60 

61 Must be subclassed for a functional :term:`Legacy Module`. 

62 """ 

63 

64 """ 

65 Since we cannot set the XMPP ID of messages sent by XMPP clients, we need to keep a mapping 

66 between XMPP IDs and legacy message IDs if we want to further refer to a message that was sent 

67 by the user. This also applies to 'carboned' messages, ie, messages sent by the user from 

68 the official client of a legacy network. 

69 """ 

70 

71 xmpp: "BaseGateway" 

72 """ 

73 The gateway instance singleton. Use it for low-level XMPP calls or custom methods that are not 

74 session-specific. 

75 """ 

76 

77 MESSAGE_IDS_ARE_THREAD_IDS = False 

78 """ 

79 Set this to True if the legacy service uses message IDs as thread IDs, 

80 eg Mattermost, where you can only 'create a thread' by replying to the message, 

81 in which case the message ID is also a thread ID (and all messages are potential 

82 threads). 

83 """ 

84 SPECIAL_MSG_ID_PREFIX: Optional[str] = None 

85 """ 

86 If you set this, XMPP message IDs starting with this won't be converted to legacy ID, 

87 but passed as is to :meth:`.on_react`, and usual checks for emoji restriction won't be 

88 applied. 

89 This can be used to implement voting in polls in a hacky way. 

90 """ 

91 

92 def __init__(self, user: GatewayUser): 

93 self.log = logging.getLogger(user.jid.bare) 

94 

95 self.user_jid = user.jid 

96 self.user_pk = user.id 

97 

98 self.ignore_messages = set[str]() 

99 

100 self.contacts: LegacyRoster = LegacyRoster.get_self_or_unique_subclass()(self) 

101 self._logged = False 

102 self.__reset_ready() 

103 

104 self.bookmarks: LegacyBookmarks = LegacyBookmarks.get_self_or_unique_subclass()( 

105 self 

106 ) 

107 

108 self.thread_creation_lock = asyncio.Lock() 

109 

110 self.__cached_presence: Optional[CachedPresence] = None 

111 

112 self.__tasks = set[asyncio.Task]() 

113 

114 @property 

115 def user(self) -> GatewayUser: 

116 return self.xmpp.store.users.get(self.user_jid) # type:ignore 

117 

118 @property 

119 def http(self) -> aiohttp.ClientSession: 

120 return self.xmpp.http 

121 

122 def __remove_task(self, fut): 

123 self.log.debug("Removing fut %s", fut) 

124 self.__tasks.remove(fut) 

125 

126 def create_task(self, coro) -> asyncio.Task: 

127 task = self.xmpp.loop.create_task(coro) 

128 self.__tasks.add(task) 

129 self.log.debug("Creating task %s", task) 

130 task.add_done_callback(lambda _: self.__remove_task(task)) 

131 return task 

132 

133 def cancel_all_tasks(self): 

134 for task in self.__tasks: 

135 task.cancel() 

136 

137 async def login(self) -> Optional[str]: 

138 """ 

139 Logs in the gateway user to the legacy network. 

140 

141 Triggered when the gateway start and on user registration. 

142 It is recommended that this function returns once the user is logged in, 

143 so if you need to await forever (for instance to listen to incoming events), 

144 it's a good idea to wrap your listener in an asyncio.Task. 

145 

146 :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'" 

147 """ 

148 raise NotImplementedError 

149 

150 async def logout(self): 

151 """ 

152 Logs out the gateway user from the legacy network. 

153 

154 Called on gateway shutdown. 

155 """ 

156 raise NotImplementedError 

157 

158 async def on_text( 

159 self, 

160 chat: RecipientType, 

161 text: str, 

162 *, 

163 reply_to_msg_id: Optional[LegacyMessageType] = None, 

164 reply_to_fallback_text: Optional[str] = None, 

165 reply_to: Optional["Sender"] = None, 

166 thread: Optional[LegacyThreadType] = None, 

167 link_previews: Iterable[LinkPreview] = (), 

168 mentions: Optional[list[Mention]] = None, 

169 ) -> Optional[LegacyMessageType]: 

170 """ 

171 Triggered when the user sends a text message from XMPP to a bridged entity, e.g. 

172 to ``translated_user_name@slidge.example.com``, or ``translated_group_name@slidge.example.com`` 

173 

174 Override this and implement sending a message to the legacy network in this method. 

175 

176 :param text: Content of the message 

177 :param chat: Recipient of the message. :class:`.LegacyContact` instance for 1:1 chat, 

178 :class:`.MUC` instance for groups. 

179 :param reply_to_msg_id: A legacy message ID if the message references (quotes) 

180 another message (:xep:`0461`) 

181 :param reply_to_fallback_text: Content of the quoted text. Not necessarily set 

182 by XMPP clients 

183 :param reply_to: Author of the quoted message. :class:`LegacyContact` instance for 

184 1:1 chat, :class:`LegacyParticipant` instance for groups. 

185 If `None`, should be interpreted as a self-reply if reply_to_msg_id is not None. 

186 :param link_previews: A list of sender-generated link previews. 

187 At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_ 

188 supports it. 

189 :param mentions: (only for groups) A list of Contacts mentioned by their 

190 nicknames. 

191 :param thread: 

192 

193 :return: An ID of some sort that can be used later to ack and mark the message 

194 as read by the user 

195 """ 

196 raise NotImplementedError 

197 

198 send_text = deprecated("BaseSession.send_text", on_text) 

199 

200 async def on_file( 

201 self, 

202 chat: RecipientType, 

203 url: str, 

204 *, 

205 http_response: aiohttp.ClientResponse, 

206 reply_to_msg_id: Optional[LegacyMessageType] = None, 

207 reply_to_fallback_text: Optional[str] = None, 

208 reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None, 

209 thread: Optional[LegacyThreadType] = None, 

210 ) -> Optional[LegacyMessageType]: 

211 """ 

212 Triggered when the user sends a file using HTTP Upload (:xep:`0363`) 

213 

214 :param url: URL of the file 

215 :param chat: See :meth:`.BaseSession.on_text` 

216 :param http_response: The HTTP GET response object on the URL 

217 :param reply_to_msg_id: See :meth:`.BaseSession.on_text` 

218 :param reply_to_fallback_text: See :meth:`.BaseSession.on_text` 

219 :param reply_to: See :meth:`.BaseSession.on_text` 

220 :param thread: 

221 

222 :return: An ID of some sort that can be used later to ack and mark the message 

223 as read by the user 

224 """ 

225 raise NotImplementedError 

226 

227 send_file = deprecated("BaseSession.send_file", on_file) 

228 

229 async def on_sticker( 

230 self, 

231 chat: RecipientType, 

232 sticker: Sticker, 

233 *, 

234 reply_to_msg_id: Optional[LegacyMessageType] = None, 

235 reply_to_fallback_text: Optional[str] = None, 

236 reply_to: Optional[Union["LegacyContact", "LegacyParticipant"]] = None, 

237 thread: Optional[LegacyThreadType] = None, 

238 ) -> Optional[LegacyMessageType]: 

239 """ 

240 Triggered when the user sends a file using HTTP Upload (:xep:`0363`) 

241 

242 :param chat: See :meth:`.BaseSession.on_text` 

243 :param sticker: The sticker sent by the user. 

244 :param reply_to_msg_id: See :meth:`.BaseSession.on_text` 

245 :param reply_to_fallback_text: See :meth:`.BaseSession.on_text` 

246 :param reply_to: See :meth:`.BaseSession.on_text` 

247 :param thread: 

248 

249 :return: An ID of some sort that can be used later to ack and mark the message 

250 as read by the user 

251 """ 

252 raise NotImplementedError 

253 

254 async def on_active( 

255 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None 

256 ): 

257 """ 

258 Triggered when the user sends an 'active' chat state (:xep:`0085`) 

259 

260 :param chat: See :meth:`.BaseSession.on_text` 

261 :param thread: 

262 """ 

263 raise NotImplementedError 

264 

265 active = deprecated("BaseSession.active", on_active) 

266 

267 async def on_inactive( 

268 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None 

269 ): 

270 """ 

271 Triggered when the user sends an 'inactive' chat state (:xep:`0085`) 

272 

273 :param chat: See :meth:`.BaseSession.on_text` 

274 :param thread: 

275 """ 

276 raise NotImplementedError 

277 

278 inactive = deprecated("BaseSession.inactive", on_inactive) 

279 

280 async def on_composing( 

281 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None 

282 ): 

283 """ 

284 Triggered when the user starts typing in a legacy chat (:xep:`0085`) 

285 

286 :param chat: See :meth:`.BaseSession.on_text` 

287 :param thread: 

288 """ 

289 raise NotImplementedError 

290 

291 composing = deprecated("BaseSession.composing", on_composing) 

292 

293 async def on_paused( 

294 self, chat: RecipientType, thread: Optional[LegacyThreadType] = None 

295 ): 

296 """ 

297 Triggered when the user pauses typing in a legacy chat (:xep:`0085`) 

298 

299 :param chat: See :meth:`.BaseSession.on_text` 

300 :param thread: 

301 """ 

302 raise NotImplementedError 

303 

304 paused = deprecated("BaseSession.paused", on_paused) 

305 

306 async def on_displayed( 

307 self, 

308 chat: RecipientType, 

309 legacy_msg_id: LegacyMessageType, 

310 thread: Optional[LegacyThreadType] = None, 

311 ): 

312 """ 

313 Triggered when the user reads a message in a legacy chat. (:xep:`0333`) 

314 

315 This is only possible if a valid ``legacy_msg_id`` was passed when 

316 transmitting a message from a legacy chat to the user, eg in 

317 :meth:`slidge.contact.LegacyContact.send_text` 

318 or 

319 :meth:`slidge.group.LegacyParticipant.send_text`. 

320 

321 :param chat: See :meth:`.BaseSession.on_text` 

322 :param legacy_msg_id: Identifier of the message/ 

323 :param thread: 

324 """ 

325 raise NotImplementedError 

326 

327 displayed = deprecated("BaseSession.displayed", on_displayed) 

328 

329 async def on_correct( 

330 self, 

331 chat: RecipientType, 

332 text: str, 

333 legacy_msg_id: LegacyMessageType, 

334 *, 

335 thread: Optional[LegacyThreadType] = None, 

336 link_previews: Iterable[LinkPreview] = (), 

337 mentions: Optional[list[Mention]] = None, 

338 ) -> Optional[LegacyMessageType]: 

339 """ 

340 Triggered when the user corrects a message using :xep:`0308` 

341 

342 This is only possible if a valid ``legacy_msg_id`` was returned by 

343 :meth:`.on_text`. 

344 

345 :param chat: See :meth:`.BaseSession.on_text` 

346 :param text: The new text 

347 :param legacy_msg_id: Identifier of the edited message 

348 :param thread: 

349 :param link_previews: A list of sender-generated link previews. 

350 At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_ 

351 supports it. 

352 :param mentions: (only for groups) A list of Contacts mentioned by their 

353 nicknames. 

354 """ 

355 raise NotImplementedError 

356 

357 correct = deprecated("BaseSession.correct", on_correct) 

358 

359 async def on_react( 

360 self, 

361 chat: RecipientType, 

362 legacy_msg_id: LegacyMessageType, 

363 emojis: list[str], 

364 thread: Optional[LegacyThreadType] = None, 

365 ): 

366 """ 

367 Triggered when the user sends message reactions (:xep:`0444`). 

368 

369 :param chat: See :meth:`.BaseSession.on_text` 

370 :param thread: 

371 :param legacy_msg_id: ID of the message the user reacts to 

372 :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``. 

373 An empty string means "no reaction", ie, remove all reactions if any were present before 

374 """ 

375 raise NotImplementedError 

376 

377 react = deprecated("BaseSession.react", on_react) 

378 

379 async def on_retract( 

380 self, 

381 chat: RecipientType, 

382 legacy_msg_id: LegacyMessageType, 

383 thread: Optional[LegacyThreadType] = None, 

384 ): 

385 """ 

386 Triggered when the user retracts (:xep:`0424`) a message. 

387 

388 :param chat: See :meth:`.BaseSession.on_text` 

389 :param thread: 

390 :param legacy_msg_id: Legacy ID of the retracted message 

391 """ 

392 raise NotImplementedError 

393 

394 retract = deprecated("BaseSession.retract", on_retract) 

395 

396 async def on_presence( 

397 self, 

398 resource: str, 

399 show: PseudoPresenceShow, 

400 status: str, 

401 resources: dict[str, ResourceDict], 

402 merged_resource: Optional[ResourceDict], 

403 ): 

404 """ 

405 Called when the gateway component receives a presence, ie, when 

406 one of the user's clients goes online of offline, or changes its 

407 status. 

408 

409 :param resource: The XMPP client identifier, arbitrary string. 

410 :param show: The presence ``<show>``, if available. If the resource is 

411 just 'available' without any ``<show>`` element, this is an empty 

412 str. 

413 :param status: A status message, like a deeply profound quote, eg, 

414 "Roses are red, violets are blue, [INSERT JOKE]". 

415 :param resources: A summary of all the resources for this user. 

416 :param merged_resource: A global presence for the user account, 

417 following rules described in :meth:`merge_resources` 

418 """ 

419 raise NotImplementedError 

420 

421 presence = deprecated("BaseSession.presence", on_presence) 

422 

423 async def on_search(self, form_values: dict[str, str]) -> Optional[SearchResult]: 

424 """ 

425 Triggered when the user uses Jabber Search (:xep:`0055`) on the component 

426 

427 Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS` 

428 

429 :param form_values: search query, defined for a specific plugin by overriding 

430 in :attr:`.BaseGateway.SEARCH_FIELDS` 

431 :return: 

432 """ 

433 raise NotImplementedError 

434 

435 search = deprecated("BaseSession.search", on_search) 

436 

437 async def on_avatar( 

438 self, 

439 bytes_: Optional[bytes], 

440 hash_: Optional[str], 

441 type_: Optional[str], 

442 width: Optional[int], 

443 height: Optional[int], 

444 ) -> None: 

445 """ 

446 Triggered when the user uses modifies their avatar via :xep:`0084`. 

447 

448 :param bytes_: The data of the avatar. According to the spec, this 

449 should always be a PNG, but some implementations do not respect 

450 that. If `None` it means the user has unpublished their avatar. 

451 :param hash_: The SHA1 hash of the avatar data. This is an identifier of 

452 the avatar. 

453 :param type_: The MIME type of the avatar. 

454 :param width: The width of the avatar image. 

455 :param height: The height of the avatar image. 

456 """ 

457 raise NotImplementedError 

458 

459 async def on_moderate( 

460 self, muc: LegacyMUC, legacy_msg_id: LegacyMessageType, reason: Optional[str] 

461 ): 

462 """ 

463 Triggered when the user attempts to retract a message that was sent in 

464 a MUC using :xep:`0425`. 

465 

466 If retraction is not possible, this should raise the appropriate 

467 XMPPError with a human-readable message. 

468 

469 NB: the legacy module is responsible for calling 

470 :method:`LegacyParticipant.moderate` when this is successful, because 

471 slidge will acknowledge the moderation IQ, but will not send the 

472 moderation message from the MUC automatically. 

473 

474 :param muc: The MUC in which the message was sent 

475 :param legacy_msg_id: The legacy ID of the message to be retracted 

476 :param reason: Optionally, a reason for the moderation, given by the 

477 user-moderator. 

478 """ 

479 raise NotImplementedError 

480 

481 async def on_create_group( 

482 self, name: str, contacts: list[LegacyContact] 

483 ) -> LegacyGroupIdType: 

484 """ 

485 Triggered when the user request the creation of a group via the 

486 dedicated :term:`Command`. 

487 

488 :param name: Name of the group 

489 :param contacts: list of contacts that should be members of the group 

490 """ 

491 raise NotImplementedError 

492 

493 async def on_invitation( 

494 self, contact: LegacyContact, muc: LegacyMUC, reason: Optional[str] 

495 ): 

496 """ 

497 Triggered when the user invites a :term:`Contact` to a legacy MUC via 

498 :xep:`0249`. 

499 

500 The default implementation calls :meth:`LegacyMUC.on_set_affiliation` 

501 with the 'member' affiliation. Override if you want to customize this 

502 behaviour. 

503 

504 :param contact: The invitee 

505 :param muc: The group 

506 :param reason: Optionally, a reason 

507 """ 

508 await muc.on_set_affiliation(contact, "member", reason, None) 

509 

510 async def on_leave_group(self, muc_legacy_id: LegacyGroupIdType): 

511 """ 

512 Triggered when the user leaves a group via the dedicated slidge command 

513 or the :xep:`0077` ``<remove />`` mechanism. 

514 

515 This should be interpreted as definitely leaving the group. 

516 

517 :param muc_legacy_id: The legacy ID of the group to leave 

518 """ 

519 raise NotImplementedError 

520 

521 def __reset_ready(self): 

522 self.ready = self.xmpp.loop.create_future() 

523 

524 @property 

525 def logged(self): 

526 return self._logged 

527 

528 @logged.setter 

529 def logged(self, v: bool): 

530 self._logged = v 

531 if self.ready.done(): 

532 if v: 

533 return 

534 self.__reset_ready() 

535 else: 

536 if v: 

537 self.ready.set_result(True) 

538 

539 def __repr__(self): 

540 return f"<Session of {self.user_jid}>" 

541 

542 def shutdown(self) -> asyncio.Task: 

543 for c in self.contacts: 

544 c.offline() 

545 for m in self.bookmarks: 

546 m.shutdown() 

547 return self.xmpp.loop.create_task(self.logout()) 

548 

549 @staticmethod 

550 def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str: 

551 """ 

552 Convert a legacy msg ID to a valid XMPP msg ID. 

553 Needed for read marks, retractions and message corrections. 

554 

555 The default implementation just converts the legacy ID to a :class:`str`, 

556 but this should be overridden in case some characters needs to be escaped, 

557 or to add some additional, 

558 :term:`legacy network <Legacy Network`>-specific logic. 

559 

560 :param legacy_msg_id: 

561 :return: A string that is usable as an XMPP stanza ID 

562 """ 

563 return str(legacy_msg_id) 

564 

565 legacy_msg_id_to_xmpp_msg_id = staticmethod( 

566 deprecated("BaseSession.legacy_msg_id_to_xmpp_msg_id", legacy_to_xmpp_msg_id) 

567 ) 

568 

569 @staticmethod 

570 def xmpp_to_legacy_msg_id(i: str) -> LegacyMessageType: 

571 """ 

572 Convert a legacy XMPP ID to a valid XMPP msg ID. 

573 Needed for read marks and message corrections. 

574 

575 The default implementation just converts the legacy ID to a :class:`str`, 

576 but this should be overridden in case some characters needs to be escaped, 

577 or to add some additional, 

578 :term:`legacy network <Legacy Network`>-specific logic. 

579 

580 The default implementation is an identity function. 

581 

582 :param i: The XMPP stanza ID 

583 :return: An ID that can be used to identify a message on the legacy network 

584 """ 

585 return cast(LegacyMessageType, i) 

586 

587 xmpp_msg_id_to_legacy_msg_id = staticmethod( 

588 deprecated("BaseSession.xmpp_msg_id_to_legacy_msg_id", xmpp_to_legacy_msg_id) 

589 ) 

590 

591 def raise_if_not_logged(self): 

592 if not self.logged: 

593 raise XMPPError( 

594 "internal-server-error", 

595 text="You are not logged to the legacy network", 

596 ) 

597 

598 @classmethod 

599 def _from_user_or_none(cls, user): 

600 if user is None: 

601 log.debug("user not found", stack_info=True) 

602 raise XMPPError(text="User not found", condition="subscription-required") 

603 

604 session = _sessions.get(user.jid.bare) 

605 if session is None: 

606 _sessions[user.jid.bare] = session = cls(user) 

607 return session 

608 

609 @classmethod 

610 def from_user(cls, user): 

611 return cls._from_user_or_none(user) 

612 

613 @classmethod 

614 def from_stanza(cls, s) -> "BaseSession": 

615 # """ 

616 # Get a user's :class:`.LegacySession` using the "from" field of a stanza 

617 # 

618 # Meant to be called from :class:`BaseGateway` only. 

619 # 

620 # :param s: 

621 # :return: 

622 # """ 

623 return cls.from_jid(s.get_from()) 

624 

625 @classmethod 

626 def from_jid(cls, jid: JID) -> "BaseSession": 

627 # """ 

628 # Get a user's :class:`.LegacySession` using its jid 

629 # 

630 # Meant to be called from :class:`BaseGateway` only. 

631 # 

632 # :param jid: 

633 # :return: 

634 # """ 

635 session = _sessions.get(jid.bare) 

636 if session is not None: 

637 return session 

638 user = cls.xmpp.store.users.get(jid) 

639 return cls._from_user_or_none(user) 

640 

641 @classmethod 

642 async def kill_by_jid(cls, jid: JID): 

643 # """ 

644 # Terminate a user session. 

645 # 

646 # Meant to be called from :class:`BaseGateway` only. 

647 # 

648 # :param jid: 

649 # :return: 

650 # """ 

651 log.debug("Killing session of %s", jid) 

652 for user_jid, session in _sessions.items(): 

653 if user_jid == jid.bare: 

654 break 

655 else: 

656 log.debug("Did not find a session for %s", jid) 

657 return 

658 for c in session.contacts: 

659 c.unsubscribe() 

660 user = cls.xmpp.store.users.get(jid) 

661 if user is None: 

662 log.warning("User not found during unregistration") 

663 return 

664 await cls.xmpp.unregister(user) 

665 cls.xmpp.store.users.delete(user.jid) 

666 del _sessions[user.jid.bare] 

667 del user 

668 del session 

669 

670 def __ack(self, msg: Message): 

671 if not self.xmpp.PROPER_RECEIPTS: 

672 self.xmpp.delivery_receipt.ack(msg) 

673 

674 def send_gateway_status( 

675 self, 

676 status: Optional[str] = None, 

677 show=Optional[PresenceShows], 

678 **kwargs, 

679 ): 

680 """ 

681 Send a presence from the gateway to the user. 

682 

683 Can be used to indicate the user session status, ie "SMS code required", "connected", … 

684 

685 :param status: A status message 

686 :param show: Presence stanza 'show' element. I suggest using "dnd" to show 

687 that the gateway is not fully functional 

688 """ 

689 self.__cached_presence = CachedPresence(status, show, kwargs) 

690 self.xmpp.send_presence( 

691 pto=self.user_jid.bare, pstatus=status, pshow=show, **kwargs 

692 ) 

693 

694 def send_cached_presence(self, to: JID): 

695 if not self.__cached_presence: 

696 self.xmpp.send_presence(pto=to, ptype="unavailable") 

697 return 

698 self.xmpp.send_presence( 

699 pto=to, 

700 pstatus=self.__cached_presence.status, 

701 pshow=self.__cached_presence.show, 

702 **self.__cached_presence.kwargs, 

703 ) 

704 

705 def send_gateway_message(self, text: str, **msg_kwargs): 

706 """ 

707 Send a message from the gateway component to the user. 

708 

709 Can be used to indicate the user session status, ie "SMS code required", "connected", … 

710 

711 :param text: A text 

712 """ 

713 self.xmpp.send_text(text, mto=self.user_jid, **msg_kwargs) 

714 

715 def send_gateway_invite( 

716 self, 

717 muc: LegacyMUC, 

718 reason: Optional[str] = None, 

719 password: Optional[str] = None, 

720 ): 

721 """ 

722 Send an invitation to join a MUC, emanating from the gateway component. 

723 

724 :param muc: 

725 :param reason: 

726 :param password: 

727 """ 

728 self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user_jid) 

729 

730 async def input(self, text: str, **msg_kwargs): 

731 """ 

732 Request user input via direct messages from the gateway component. 

733 

734 Wraps call to :meth:`.BaseSession.input` 

735 

736 :param text: The prompt to send to the user 

737 :param msg_kwargs: Extra attributes 

738 :return: 

739 """ 

740 return await self.xmpp.input(self.user_jid, text, **msg_kwargs) 

741 

742 async def send_qr(self, text: str): 

743 """ 

744 Sends a QR code generated from 'text' via HTTP Upload and send the URL to 

745 ``self.user`` 

746 

747 :param text: Text to encode as a QR code 

748 """ 

749 await self.xmpp.send_qr(text, mto=self.user_jid) 

750 

751 def re_login(self): 

752 # Logout then re-login 

753 # 

754 # No reason to override this 

755 self.xmpp.re_login(self) 

756 

757 async def get_contact_or_group_or_participant(self, jid: JID, create=True): 

758 if (contact := self.contacts.by_jid_only_if_exists(jid)) is not None: 

759 return contact 

760 if (muc := self.bookmarks.by_jid_only_if_exists(JID(jid.bare))) is not None: 

761 return await self.__get_muc_or_participant(muc, jid) 

762 else: 

763 muc = None 

764 

765 if not create: 

766 return None 

767 

768 try: 

769 return await self.contacts.by_jid(jid) 

770 except XMPPError: 

771 if muc is None: 

772 try: 

773 muc = await self.bookmarks.by_jid(jid) 

774 except XMPPError: 

775 return 

776 return await self.__get_muc_or_participant(muc, jid) 

777 

778 @staticmethod 

779 async def __get_muc_or_participant(muc: LegacyMUC, jid: JID): 

780 if nick := jid.resource: 

781 try: 

782 return await muc.get_participant( 

783 nick, raise_if_not_found=True, fill_first=True 

784 ) 

785 except XMPPError: 

786 return None 

787 return muc 

788 

789 async def wait_for_ready(self, timeout: Optional[Union[int, float]] = 10): 

790 # """ 

791 # Wait until session, contacts and bookmarks are ready 

792 # 

793 # (slidge internal use) 

794 # 

795 # :param timeout: 

796 # :return: 

797 # """ 

798 try: 

799 await asyncio.wait_for(asyncio.shield(self.ready), timeout) 

800 await asyncio.wait_for(asyncio.shield(self.contacts.ready), timeout) 

801 await asyncio.wait_for(asyncio.shield(self.bookmarks.ready), timeout) 

802 except asyncio.TimeoutError: 

803 raise XMPPError( 

804 "recipient-unavailable", 

805 "Legacy session is not fully initialized, retry later", 

806 ) 

807 

808 def legacy_module_data_update(self, data: dict): 

809 with self.xmpp.store.session(): 

810 user = self.user 

811 user.legacy_module_data.update(data) 

812 self.xmpp.store.users.update(user) 

813 

814 def legacy_module_data_set(self, data: dict): 

815 with self.xmpp.store.session(): 

816 user = self.user 

817 user.legacy_module_data = data 

818 self.xmpp.store.users.update(user) 

819 

820 def legacy_module_data_clear(self): 

821 with self.xmpp.store.session(): 

822 user = self.user 

823 user.legacy_module_data.clear() 

824 self.xmpp.store.users.update(user) 

825 

826 

827# keys = user.jid.bare 

828_sessions: dict[str, BaseSession] = {} 

829log = logging.getLogger(__name__)