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

252 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-04 08:17 +0000

1import asyncio 

2import logging 

3from typing import ( 

4 TYPE_CHECKING, 

5 Any, 

6 Generic, 

7 Iterable, 

8 NamedTuple, 

9 Optional, 

10 Type, 

11 Union, 

12 cast, 

13) 

14 

15import aiohttp 

16import sqlalchemy as sa 

17from slixmpp import JID, Message 

18from slixmpp.exceptions import XMPPError 

19from slixmpp.types import PresenceShows 

20 

21from ..command import SearchResult 

22from ..contact import LegacyContact, LegacyRoster 

23from ..db.models import Contact, GatewayUser 

24from ..group.bookmarks import LegacyBookmarks 

25from ..group.room import LegacyMUC 

26from ..util import ABCSubclassableOnceAtMost 

27from ..util.types import ( 

28 LegacyGroupIdType, 

29 LegacyMessageType, 

30 LegacyThreadType, 

31 LinkPreview, 

32 Mention, 

33 PseudoPresenceShow, 

34 RecipientType, 

35 ResourceDict, 

36 Sticker, 

37) 

38from ..util.util import deprecated, noop_coro 

39 

40if TYPE_CHECKING: 

41 from ..group.participant import LegacyParticipant 

42 from ..util.types import Sender 

43 from .gateway import BaseGateway 

44 

45 

46class CachedPresence(NamedTuple): 

47 status: Optional[str] 

48 show: Optional[str] 

49 kwargs: dict[str, Any] 

50 

51 

52class BaseSession( 

53 Generic[LegacyMessageType, RecipientType], metaclass=ABCSubclassableOnceAtMost 

54): 

55 """ 

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

57 

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

59 

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

61 or upon registration for new (validated) users. 

62 

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

64 """ 

65 

66 """ 

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

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

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

70 the official client of a legacy network. 

71 """ 

72 

73 xmpp: "BaseGateway" 

74 """ 

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

76 session-specific. 

77 """ 

78 

79 MESSAGE_IDS_ARE_THREAD_IDS = False 

80 """ 

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

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

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

84 threads). 

85 """ 

86 SPECIAL_MSG_ID_PREFIX: Optional[str] = None 

87 """ 

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

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

90 applied. 

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

92 """ 

93 

94 _roster_cls: Type[LegacyRoster] 

95 _bookmarks_cls: Type[LegacyBookmarks] 

96 

97 def __init__(self, user: GatewayUser) -> None: 

98 self.user = user 

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

100 

101 self.ignore_messages = set[str]() 

102 

103 self.contacts = self._roster_cls(self) 

104 self.is_logging_in = False 

105 self._logged = False 

106 self.__reset_ready() 

107 

108 self.bookmarks = self._bookmarks_cls(self) 

109 

110 self.thread_creation_lock = asyncio.Lock() 

111 

112 self.__cached_presence: Optional[CachedPresence] = None 

113 

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

115 

116 @property 

117 def user_jid(self) -> JID: 

118 return self.user.jid 

119 

120 @property 

121 def user_pk(self) -> int: 

122 return self.user.id 

123 

124 @property 

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

126 return self.xmpp.http 

127 

128 def __remove_task(self, fut) -> None: 

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

130 self.__tasks.remove(fut) 

131 

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

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

134 self.__tasks.add(task) 

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

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

137 return task 

138 

139 def cancel_all_tasks(self) -> None: 

140 for task in self.__tasks: 

141 task.cancel() 

142 

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

144 """ 

145 Logs in the gateway user to the legacy network. 

146 

147 Triggered when the gateway start and on user registration. 

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

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

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

151 

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

153 """ 

154 raise NotImplementedError 

155 

156 async def logout(self) -> None: 

157 """ 

158 Logs out the gateway user from the legacy network. 

159 

160 Called on gateway shutdown. 

161 """ 

162 raise NotImplementedError 

163 

164 async def on_text( 

165 self, 

166 chat: RecipientType, 

167 text: str, 

168 *, 

169 reply_to_msg_id: Optional[LegacyMessageType] = None, 

170 reply_to_fallback_text: Optional[str] = None, 

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

172 thread: Optional[LegacyThreadType] = None, 

173 link_previews: Iterable[LinkPreview] = (), 

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

175 ) -> Optional[LegacyMessageType]: 

176 """ 

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

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

179 

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

181 

182 :param text: Content of the message 

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

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

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

186 another message (:xep:`0461`) 

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

188 by XMPP clients 

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

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

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

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

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

194 supports it. 

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

196 nicknames. 

197 :param thread: 

198 

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

200 as read by the user 

201 """ 

202 raise NotImplementedError 

203 

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

205 

206 async def on_file( 

207 self, 

208 chat: RecipientType, 

209 url: str, 

210 *, 

211 http_response: aiohttp.ClientResponse, 

212 reply_to_msg_id: Optional[LegacyMessageType] = None, 

213 reply_to_fallback_text: Optional[str] = None, 

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

215 thread: Optional[LegacyThreadType] = None, 

216 ) -> Optional[LegacyMessageType]: 

217 """ 

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

219 

220 :param url: URL of the file 

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

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

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

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

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

226 :param thread: 

227 

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

229 as read by the user 

230 """ 

231 raise NotImplementedError 

232 

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

234 

235 async def on_sticker( 

236 self, 

237 chat: RecipientType, 

238 sticker: Sticker, 

239 *, 

240 reply_to_msg_id: Optional[LegacyMessageType] = None, 

241 reply_to_fallback_text: Optional[str] = None, 

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

243 thread: Optional[LegacyThreadType] = None, 

244 ) -> Optional[LegacyMessageType]: 

245 """ 

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

247 

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

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

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

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

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

253 :param thread: 

254 

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

256 as read by the user 

257 """ 

258 raise NotImplementedError 

259 

260 async def on_active( 

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

262 ): 

263 """ 

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

265 

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

267 :param thread: 

268 """ 

269 raise NotImplementedError 

270 

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

272 

273 async def on_inactive( 

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

275 ): 

276 """ 

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

278 

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

280 :param thread: 

281 """ 

282 raise NotImplementedError 

283 

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

285 

286 async def on_composing( 

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

288 ): 

289 """ 

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

291 

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

293 :param thread: 

294 """ 

295 raise NotImplementedError 

296 

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

298 

299 async def on_paused( 

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

301 ): 

302 """ 

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

304 

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

306 :param thread: 

307 """ 

308 raise NotImplementedError 

309 

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

311 

312 async def on_gone( 

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

314 ): 

315 """ 

316 Triggered when the user is "gone" in a legacy chat (:xep:`0085`) 

317 

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

319 :param thread: 

320 """ 

321 raise NotImplementedError 

322 

323 async def on_displayed( 

324 self, 

325 chat: RecipientType, 

326 legacy_msg_id: LegacyMessageType, 

327 thread: Optional[LegacyThreadType] = None, 

328 ): 

329 """ 

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

331 

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

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

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

335 or 

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

337 

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

339 :param legacy_msg_id: Identifier of the message/ 

340 :param thread: 

341 """ 

342 raise NotImplementedError 

343 

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

345 

346 async def on_correct( 

347 self, 

348 chat: RecipientType, 

349 text: str, 

350 legacy_msg_id: LegacyMessageType, 

351 *, 

352 thread: Optional[LegacyThreadType] = None, 

353 link_previews: Iterable[LinkPreview] = (), 

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

355 ) -> Optional[LegacyMessageType]: 

356 """ 

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

358 

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

360 :meth:`.on_text`. 

361 

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

363 :param text: The new text 

364 :param legacy_msg_id: Identifier of the edited message 

365 :param thread: 

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

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

368 supports it. 

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

370 nicknames. 

371 """ 

372 raise NotImplementedError 

373 

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

375 

376 async def on_react( 

377 self, 

378 chat: RecipientType, 

379 legacy_msg_id: LegacyMessageType, 

380 emojis: list[str], 

381 thread: Optional[LegacyThreadType] = None, 

382 ): 

383 """ 

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

385 

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

387 :param thread: 

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

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

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

391 """ 

392 raise NotImplementedError 

393 

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

395 

396 async def on_retract( 

397 self, 

398 chat: RecipientType, 

399 legacy_msg_id: LegacyMessageType, 

400 thread: Optional[LegacyThreadType] = None, 

401 ): 

402 """ 

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

404 

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

406 :param thread: 

407 :param legacy_msg_id: Legacy ID of the retracted message 

408 """ 

409 raise NotImplementedError 

410 

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

412 

413 async def on_presence( 

414 self, 

415 resource: str, 

416 show: PseudoPresenceShow, 

417 status: str, 

418 resources: dict[str, ResourceDict], 

419 merged_resource: Optional[ResourceDict], 

420 ): 

421 """ 

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

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

424 status. 

425 

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

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

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

429 str. 

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

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

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

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

434 following rules described in :meth:`merge_resources` 

435 """ 

436 raise NotImplementedError 

437 

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

439 

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

441 """ 

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

443 

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

445 

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

447 in :attr:`.BaseGateway.SEARCH_FIELDS` 

448 :return: 

449 """ 

450 raise NotImplementedError 

451 

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

453 

454 async def on_avatar( 

455 self, 

456 bytes_: Optional[bytes], 

457 hash_: Optional[str], 

458 type_: Optional[str], 

459 width: Optional[int], 

460 height: Optional[int], 

461 ) -> None: 

462 """ 

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

464 

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

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

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

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

469 the avatar. 

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

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

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

473 """ 

474 raise NotImplementedError 

475 

476 async def on_moderate( 

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

478 ): 

479 """ 

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

481 a MUC using :xep:`0425`. 

482 

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

484 XMPPError with a human-readable message. 

485 

486 NB: the legacy module is responsible for calling 

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

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

489 moderation message from the MUC automatically. 

490 

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

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

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

494 user-moderator. 

495 """ 

496 raise NotImplementedError 

497 

498 async def on_create_group( 

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

500 ) -> LegacyGroupIdType: 

501 """ 

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

503 dedicated :term:`Command`. 

504 

505 :param name: Name of the group 

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

507 """ 

508 raise NotImplementedError 

509 

510 async def on_invitation( 

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

512 ) -> None: 

513 """ 

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

515 :xep:`0249`. 

516 

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

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

519 behaviour. 

520 

521 :param contact: The invitee 

522 :param muc: The group 

523 :param reason: Optionally, a reason 

524 """ 

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

526 

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

528 """ 

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

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

531 

532 This should be interpreted as definitely leaving the group. 

533 

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

535 """ 

536 raise NotImplementedError 

537 

538 def __reset_ready(self) -> None: 

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

540 

541 @property 

542 def logged(self): 

543 return self._logged 

544 

545 @logged.setter 

546 def logged(self, v: bool) -> None: 

547 self.is_logging_in = False 

548 self._logged = v 

549 if self.ready.done(): 

550 if v: 

551 return 

552 self.__reset_ready() 

553 self.shutdown(logout=False) 

554 else: 

555 if v: 

556 self.ready.set_result(True) 

557 

558 def __repr__(self) -> str: 

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

560 

561 def shutdown(self, logout: bool = True) -> asyncio.Task: 

562 for m in self.bookmarks: 

563 m.shutdown() 

564 with self.xmpp.store.session() as orm: 

565 for jid in orm.execute( 

566 sa.select(Contact.jid).filter_by(user=self.user, is_friend=True) 

567 ).scalars(): 

568 pres = self.xmpp.make_presence( 

569 pfrom=jid, 

570 pto=self.user_jid, 

571 ptype="unavailable", 

572 pstatus="Gateway has shut down.", 

573 ) 

574 pres.send() 

575 if logout: 

576 return self.xmpp.loop.create_task(self.__logout()) 

577 else: 

578 return self.xmpp.loop.create_task(noop_coro()) 

579 

580 async def __logout(self) -> None: 

581 try: 

582 await self.logout() 

583 except NotImplementedError: 

584 pass 

585 

586 @staticmethod 

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

588 """ 

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

590 Needed for read marks, retractions and message corrections. 

591 

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

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

594 or to add some additional, 

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

596 

597 :param legacy_msg_id: 

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

599 """ 

600 return str(legacy_msg_id) 

601 

602 legacy_msg_id_to_xmpp_msg_id = staticmethod( 

603 deprecated("BaseSession.legacy_msg_id_to_xmpp_msg_id", legacy_to_xmpp_msg_id) 

604 ) 

605 

606 @staticmethod 

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

608 """ 

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

610 Needed for read marks and message corrections. 

611 

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

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

614 or to add some additional, 

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

616 

617 The default implementation is an identity function. 

618 

619 :param i: The XMPP stanza ID 

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

621 """ 

622 return cast(LegacyMessageType, i) 

623 

624 xmpp_msg_id_to_legacy_msg_id = staticmethod( 

625 deprecated("BaseSession.xmpp_msg_id_to_legacy_msg_id", xmpp_to_legacy_msg_id) 

626 ) 

627 

628 def raise_if_not_logged(self): 

629 if not self.logged: 

630 raise XMPPError( 

631 "internal-server-error", 

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

633 ) 

634 

635 @classmethod 

636 def _from_user_or_none(cls, user): 

637 if user is None: 

638 log.debug("user not found") 

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

640 

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

642 if session is None: 

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

644 return session 

645 

646 @classmethod 

647 def from_user(cls, user): 

648 return cls._from_user_or_none(user) 

649 

650 @classmethod 

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

652 # """ 

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

654 # 

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

656 # 

657 # :param s: 

658 # :return: 

659 # """ 

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

661 

662 @classmethod 

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

664 # """ 

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

666 # 

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

668 # 

669 # :param jid: 

670 # :return: 

671 # """ 

672 session = _sessions.get(jid.bare) 

673 if session is not None: 

674 return session 

675 with cls.xmpp.store.session() as orm: 

676 user = orm.query(GatewayUser).filter_by(jid=jid.bare).one_or_none() 

677 return cls._from_user_or_none(user) 

678 

679 @classmethod 

680 async def kill_by_jid(cls, jid: JID) -> None: 

681 # """ 

682 # Terminate a user session. 

683 # 

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

685 # 

686 # :param jid: 

687 # :return: 

688 # """ 

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

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

691 if user_jid == jid.bare: 

692 break 

693 else: 

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

695 return 

696 for c in session.contacts: 

697 c.unsubscribe() 

698 for m in session.bookmarks: 

699 m.shutdown() 

700 

701 try: 

702 session = _sessions.pop(jid.bare) 

703 except KeyError: 

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

705 return 

706 

707 with cls.xmpp.store.session() as orm: 

708 await cls.xmpp.unregister(session.user) 

709 orm.delete(session.user) 

710 orm.commit() 

711 

712 def __ack(self, msg: Message) -> None: 

713 if not self.xmpp.PROPER_RECEIPTS: 

714 self.xmpp.delivery_receipt.ack(msg) 

715 

716 def send_gateway_status( 

717 self, 

718 status: Optional[str] = None, 

719 show=Optional[PresenceShows], 

720 **kwargs, 

721 ) -> None: 

722 """ 

723 Send a presence from the gateway to the user. 

724 

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

726 

727 :param status: A status message 

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

729 that the gateway is not fully functional 

730 """ 

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

732 self.xmpp.send_presence( 

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

734 ) 

735 

736 def send_cached_presence(self, to: JID) -> None: 

737 if not self.__cached_presence: 

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

739 return 

740 self.xmpp.send_presence( 

741 pto=to, 

742 pstatus=self.__cached_presence.status, 

743 pshow=self.__cached_presence.show, 

744 **self.__cached_presence.kwargs, 

745 ) 

746 

747 def send_gateway_message(self, text: str, **msg_kwargs) -> None: 

748 """ 

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

750 

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

752 

753 :param text: A text 

754 """ 

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

756 

757 def send_gateway_invite( 

758 self, 

759 muc: LegacyMUC, 

760 reason: Optional[str] = None, 

761 password: Optional[str] = None, 

762 ) -> None: 

763 """ 

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

765 

766 :param muc: 

767 :param reason: 

768 :param password: 

769 """ 

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

771 

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

773 """ 

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

775 

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

777 

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

779 :param msg_kwargs: Extra attributes 

780 :return: 

781 """ 

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

783 

784 async def send_qr(self, text: str) -> None: 

785 """ 

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

787 ``self.user`` 

788 

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

790 """ 

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

792 

793 def re_login(self) -> None: 

794 # Logout then re-login 

795 # 

796 # No reason to override this 

797 self.xmpp.re_login(self) 

798 

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

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

801 return contact 

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

803 return await self.__get_muc_or_participant(muc, jid) 

804 else: 

805 muc = None 

806 

807 if not create: 

808 return None 

809 

810 try: 

811 return await self.contacts.by_jid(jid) 

812 except XMPPError: 

813 if muc is None: 

814 try: 

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

816 except XMPPError: 

817 return 

818 return await self.__get_muc_or_participant(muc, jid) 

819 

820 @staticmethod 

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

822 if nick := jid.resource: 

823 try: 

824 return await muc.get_participant( 

825 nick, raise_if_not_found=True, fill_first=True 

826 ) 

827 except XMPPError: 

828 return None 

829 return muc 

830 

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

832 # """ 

833 # Wait until session, contacts and bookmarks are ready 

834 # 

835 # (slidge internal use) 

836 # 

837 # :param timeout: 

838 # :return: 

839 # """ 

840 try: 

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

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

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

844 except asyncio.TimeoutError: 

845 raise XMPPError( 

846 "recipient-unavailable", 

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

848 ) 

849 

850 def legacy_module_data_update(self, data: dict) -> None: 

851 user = self.user 

852 user.legacy_module_data.update(data) 

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

854 

855 def legacy_module_data_set(self, data: dict) -> None: 

856 user = self.user 

857 user.legacy_module_data = data 

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

859 

860 def legacy_module_data_clear(self) -> None: 

861 user = self.user 

862 user.legacy_module_data.clear() 

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

864 

865 

866# keys = user.jid.bare 

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

868log = logging.getLogger(__name__)