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
slidge/core/session.py CHANGED
@@ -1,75 +1,66 @@
1
- import functools
1
+ import asyncio
2
2
  import logging
3
- from typing import Generic, Optional, Type, Union, cast
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ Generic,
7
+ Iterable,
8
+ NamedTuple,
9
+ Optional,
10
+ Union,
11
+ cast,
12
+ )
4
13
 
5
- from slixmpp import JID, Message, Presence
14
+ import aiohttp
15
+ from slixmpp import JID, Message
6
16
  from slixmpp.exceptions import XMPPError
17
+ from slixmpp.types import PresenceShows
7
18
 
8
- from ..util import ABCSubclassableOnceAtMost, BiDict
19
+ from ..command import SearchResult
20
+ from ..contact import LegacyContact, LegacyRoster
21
+ from ..group.bookmarks import LegacyBookmarks
22
+ from ..group.room import LegacyMUC
23
+ from ..util import ABCSubclassableOnceAtMost
9
24
  from ..util.db import GatewayUser, user_store
25
+ from ..util.sql import SQLBiDict
10
26
  from ..util.types import (
11
- BookmarksType,
12
- Chat,
13
- GatewayType,
14
- LegacyContactType,
27
+ LegacyGroupIdType,
15
28
  LegacyMessageType,
16
- LegacyMUCType,
17
- LegacyParticipantType,
18
- LegacyRosterType,
19
- PresenceShow,
20
- SessionType,
29
+ LegacyThreadType,
30
+ LinkPreview,
31
+ Mention,
32
+ PseudoPresenceShow,
33
+ RecipientType,
34
+ ResourceDict,
21
35
  )
22
- from ..util.util import SearchResult
23
- from .contact import LegacyRoster
24
- from .muc.bookmarks import LegacyBookmarks
25
- from .muc.room import LegacyMUC
26
-
27
-
28
- def ignore_sent_carbons(func):
29
- @functools.wraps(func)
30
- async def wrapped(self: SessionType, msg: Message):
31
- if (i := msg.get_id()) in self.ignore_messages:
32
- self.log.debug("Ignored sent carbon: %s", i)
33
- self.ignore_messages.remove(i)
34
- else:
35
- return await func(self, msg)
36
+ from ..util.util import deprecated
36
37
 
37
- return wrapped
38
+ if TYPE_CHECKING:
39
+ from ..group.participant import LegacyParticipant
40
+ from ..util.types import Sender
41
+ from .gateway import BaseGateway
38
42
 
39
43
 
40
- def ignore_message_to_component(func):
41
- @functools.wraps(func)
42
- async def wrapped(self: SessionType, msg: Message):
43
- if msg.get_to() != self.xmpp.boundjid.bare:
44
- return await func(self, msg)
45
- else:
46
- log.debug("Ignoring message to component: %s %s", self, msg)
47
-
48
- return wrapped
44
+ class CachedPresence(NamedTuple):
45
+ status: Optional[str]
46
+ show: Optional[str]
47
+ kwargs: dict[str, Any]
49
48
 
50
49
 
51
50
  class BaseSession(
52
- Generic[
53
- GatewayType,
54
- LegacyMessageType,
55
- LegacyRosterType,
56
- LegacyContactType,
57
- BookmarksType,
58
- LegacyMUCType,
59
- LegacyParticipantType,
60
- ],
61
- metaclass=ABCSubclassableOnceAtMost,
51
+ Generic[LegacyMessageType, RecipientType], metaclass=ABCSubclassableOnceAtMost
62
52
  ):
63
53
  """
54
+ The session of a registered :term:`User`.
55
+
64
56
  Represents a gateway user logged in to the legacy network and performing actions.
65
57
 
66
- Will be instantiated automatically when a user sends an online presence to the gateway
67
- component, as per :xep:`0100`.
58
+ Will be instantiated automatically on slidge startup for each registered user,
59
+ or upon registration for new (validated) users.
68
60
 
69
- Must be subclassed for a functional slidge plugin.
61
+ Must be subclassed for a functional :term:`Legacy Module`.
70
62
  """
71
63
 
72
- sent: BiDict[LegacyMessageType, str]
73
64
  """
74
65
  Since we cannot set the XMPP ID of messages sent by XMPP clients, we need to keep a mapping
75
66
  between XMPP IDs and legacy message IDs if we want to further refer to a message that was sent
@@ -77,623 +68,700 @@ class BaseSession(
77
68
  the official client of a legacy network.
78
69
  """
79
70
 
80
- xmpp: "GatewayType"
71
+ xmpp: "BaseGateway"
81
72
  """
82
73
  The gateway instance singleton. Use it for low-level XMPP calls or custom methods that are not
83
74
  session-specific.
84
75
  """
85
76
 
86
- def __init__(self, user: GatewayUser):
87
- self._roster_cls: Type[
88
- LegacyRosterType
89
- ] = LegacyRoster.get_self_or_unique_subclass()
77
+ http: aiohttp.ClientSession
90
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
+ def __init__(self, user: GatewayUser):
91
95
  self.log = logging.getLogger(user.bare_jid)
92
96
 
93
97
  self.user = user
94
- self.sent = BiDict[LegacyMessageType, str]() # TODO: set a max size for this
98
+ self.sent = SQLBiDict[LegacyMessageType, str](
99
+ "session_message_sent", "legacy_id", "xmpp_id", self.user
100
+ )
95
101
  # message ids (*not* stanza-ids), needed for last msg correction
96
- self.muc_sent_msg_ids = BiDict[LegacyMessageType, str]()
102
+ self.muc_sent_msg_ids = SQLBiDict[LegacyMessageType, str](
103
+ "session_message_sent_muc", "legacy_id", "xmpp_id", self.user
104
+ )
97
105
 
98
106
  self.ignore_messages = set[str]()
99
107
 
100
- self.contacts: LegacyRosterType = self._roster_cls(self)
101
- self.never_logged = True
108
+ self.contacts: LegacyRoster = LegacyRoster.get_self_or_unique_subclass()(self)
109
+ self._logged = False
110
+ self.__reset_ready()
102
111
 
103
- self.bookmarks: BookmarksType = LegacyBookmarks.get_self_or_unique_subclass()(
112
+ self.bookmarks: LegacyBookmarks = LegacyBookmarks.get_self_or_unique_subclass()(
104
113
  self
105
114
  )
106
115
 
107
- def shutdown(self):
108
- for c in self.contacts:
109
- c.offline()
110
- for m in self.bookmarks:
111
- m.shutdown()
112
- self.xmpp.loop.create_task(self.logout())
113
-
114
- @staticmethod
115
- def legacy_msg_id_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
116
- """
117
- Convert a legacy msg ID to a valid XMPP msg ID.
118
- Needed for read marks and message corrections.
116
+ self.http = self.xmpp.http
119
117
 
120
- The default implementation just converts the legacy ID to a :class:`str`,
121
- but this should be overridden in case some characters needs to be escaped,
122
- or to add some additional, legacy network-specific logic.
118
+ self.threads = SQLBiDict[str, LegacyThreadType]( # type:ignore
119
+ "session_thread_sent_muc", "legacy_id", "xmpp_id", self.user
120
+ )
121
+ self.thread_creation_lock = asyncio.Lock()
123
122
 
124
- :param legacy_msg_id:
125
- :return: Should return a string that is usable as an XMPP stanza ID
126
- """
127
- return str(legacy_msg_id)
123
+ self.__cached_presence: Optional[CachedPresence] = None
128
124
 
129
- @staticmethod
130
- def xmpp_msg_id_to_legacy_msg_id(i: str) -> LegacyMessageType:
131
- """
132
- Convert a legacy XMPP ID to a valid XMPP msg ID.
133
- Needed for read marks and message corrections.
125
+ self.avatar_hash: Optional[str] = None
134
126
 
135
- The default implementation just converts the legacy ID to a :class:`str`,
136
- but this should be overridden in case some characters needs to be escaped,
137
- or to add some additional, legacy network-specific logic.
127
+ self.__tasks = set[asyncio.Task]()
138
128
 
139
- The default implementation is an identity function
129
+ def __remove_task(self, fut):
130
+ self.log.debug("Removing fut %s", fut)
131
+ self.__tasks.remove(fut)
140
132
 
141
- :param i: The XMPP stanza ID
142
- :return: An ID that can be used to identify a message on the legacy network
143
- """
144
- return cast(LegacyMessageType, i)
133
+ def create_task(self, coro) -> None:
134
+ task = self.xmpp.loop.create_task(coro)
135
+ self.__tasks.add(task)
136
+ self.log.debug("Creating task %s", task)
137
+ task.add_done_callback(lambda _: self.__remove_task(task))
145
138
 
146
- @classmethod
147
- def _from_user_or_none(cls, user):
148
- if user is None:
149
- raise XMPPError(
150
- text="User not found", condition="subscription-required", etype="auth"
151
- )
139
+ def cancel_all_tasks(self):
140
+ for task in self.__tasks:
141
+ task.cancel()
152
142
 
153
- session = _sessions.get(user)
154
- if session is None:
155
- _sessions[user] = session = cls(user)
156
- return session
143
+ async def login(self) -> Optional[str]:
144
+ """
145
+ Logs in the gateway user to the legacy network.
157
146
 
158
- @classmethod
159
- def from_user(cls, user):
160
- return cls._from_user_or_none(user)
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.
161
151
 
162
- @classmethod
163
- def from_stanza(cls: Type[SessionType], s) -> SessionType:
152
+ :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'"
164
153
  """
165
- Get a user's :class:`.LegacySession` using the "from" field of a stanza
154
+ raise NotImplementedError
166
155
 
167
- Meant to be called from :class:`BaseGateway` only.
156
+ async def logout(self):
157
+ """
158
+ Logs out the gateway user from the legacy network.
168
159
 
169
- :param s:
170
- :return:
160
+ Called on gateway shutdown.
171
161
  """
172
- return cls._from_user_or_none(user_store.get_by_stanza(s))
162
+ raise NotImplementedError
173
163
 
174
- @classmethod
175
- def from_jid(cls: Type[SessionType], jid: JID) -> SessionType:
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
176
  """
177
- Get a user's :class:`.LegacySession` using its jid
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``
178
179
 
179
- Meant to be called from :class:`BaseGateway` only.
180
+ Override this and implement sending a message to the legacy network in this method.
180
181
 
181
- :param jid:
182
- :return:
183
- """
184
- return cls._from_user_or_none(user_store.get_by_jid(jid))
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:
185
198
 
186
- @classmethod
187
- async def kill_by_jid(cls, jid: JID):
199
+ :return: An ID of some sort that can be used later to ack and mark the message
200
+ as read by the user
188
201
  """
189
- Terminate a user session.
202
+ raise NotImplementedError
190
203
 
191
- Meant to be called from :class:`BaseGateway` only.
204
+ send_text = deprecated("BaseSession.send_text", on_text)
192
205
 
193
- :param jid:
194
- :return:
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]:
195
217
  """
196
- log.debug("Killing session of %s", jid)
197
- for user, session in _sessions.items():
198
- if user.jid == jid.bare:
199
- break
200
- else:
201
- log.debug("Did not find a session for %s", jid)
202
- return
203
- for c in session.contacts:
204
- c.unsubscribe()
205
- await session.logout()
206
- await cls.xmpp.unregister(user)
207
- del _sessions[user]
208
- del user
209
- del session
218
+ Triggered when the user sends a file using HTTP Upload (:xep:`0363`)
210
219
 
211
- @ignore_message_to_component
212
- @ignore_sent_carbons
213
- async def send_from_msg(self, m: Message):
214
- """
215
- Meant to be called from :class:`BaseGateway` only.
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:
216
227
 
217
- :param m:
218
- :return:
228
+ :return: An ID of some sort that can be used later to ack and mark the message
229
+ as read by the user
219
230
  """
220
- # we MUST not use `if m["replace"]["id"]` because it adds the tag if not
221
- # present. this is a problem for MUC echoed messages
222
- if m.xml.find("{urn:xmpp:message-correct:0}replace") is not None:
223
- # ignore last message correction (handled by a specific method)
224
- return
225
- if m.xml.find("{urn:xmpp:fasten:0}apply-to") is not None:
226
- # ignore message retraction (handled by a specific method)
227
- return
228
-
229
- e = await self.__get_entity(m)
230
- self.log.debug("Entity %r", e)
231
+ raise NotImplementedError
231
232
 
232
- if m.xml.find("{jabber:x:oob}x") is not None:
233
- url = m["oob"]["url"]
234
- else:
235
- url = None
236
-
237
- text = m["body"]
238
- if m.xml.find("{urn:xmpp:fallback:0}fallback") is not None and (
239
- isinstance(e, LegacyMUC) or e.REPLIES # type: ignore
240
- ):
241
- text = m["feature_fallback"].get_stripped_body()
242
- reply_fallback = m["feature_fallback"].get_fallback_body()
243
- else:
244
- reply_fallback = None
245
-
246
- # Testing with `is None` is mandatory since a reply element have no
247
- # 'data' but only attributes, so the ElementTree is "false-ish".
248
- # Grrrrr this took me some time to figure out.
249
- reply_to = None
250
- if m.xml.find("{urn:xmpp:reply:0}reply") is not None:
251
- reply_to_msg_xmpp_id = self.__xmpp_msg_id_to_legacy(m["reply"]["id"])
252
- reply_to_jid = JID(m["reply"]["to"])
253
- if m["type"] == "chat":
254
- if reply_to_jid.bare != self.user.jid.bare:
255
- try:
256
- reply_to = await self.contacts.by_jid(reply_to_jid)
257
- except XMPPError:
258
- pass
259
- elif m["type"] == "groupchat":
260
- nick = reply_to_jid.resource
261
- try:
262
- muc = await self.bookmarks.by_jid(reply_to_jid)
263
- except XMPPError:
264
- pass
265
- else:
266
- if nick != muc.user_nick:
267
- reply_to = await muc.get_participant(reply_to_jid.resource)
268
- else:
269
- reply_to_msg_xmpp_id = None
270
- reply_to = None
233
+ send_file = deprecated("BaseSession.send_file", on_file)
271
234
 
272
- kwargs = dict(
273
- reply_to_msg_id=reply_to_msg_xmpp_id,
274
- reply_to_fallback_text=reply_fallback,
275
- reply_to=reply_to,
276
- )
235
+ async def on_active(
236
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
237
+ ):
238
+ """
239
+ Triggered when the user sends an 'active' chat state (:xep:`0085`)
277
240
 
278
- if url:
279
- legacy_msg_id = await self.send_file(url, e, **kwargs)
280
- elif text:
281
- legacy_msg_id = await self.send_text(text, e, **kwargs)
282
- else:
283
- log.debug("Ignoring %s", m)
284
- return
241
+ :param chat: See :meth:`.BaseSession.on_text`
242
+ :param thread:
243
+ """
244
+ raise NotImplementedError
285
245
 
286
- if isinstance(e, LegacyMUC):
287
- await e.echo(m, legacy_msg_id)
288
- if legacy_msg_id is not None:
289
- self.muc_sent_msg_ids[legacy_msg_id] = m.get_id()
290
- else:
291
- if legacy_msg_id is not None:
292
- self.sent[legacy_msg_id] = m.get_id()
293
-
294
- async def __get_entity(self, m: Message) -> Union[LegacyContactType, LegacyMUCType]:
295
- if m.get_type() == "groupchat":
296
- muc = await self.bookmarks.by_jid(m.get_to())
297
- if m.get_from().resource not in muc.user_resources:
298
- raise XMPPError("not-acceptable", "You are not connected to this chat")
299
- return muc
300
- else:
301
- return await self.contacts.by_jid(m.get_to())
246
+ active = deprecated("BaseSession.active", on_active)
302
247
 
303
- @ignore_message_to_component
304
- async def active_from_msg(self, m: Message):
248
+ async def on_inactive(
249
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
250
+ ):
305
251
  """
306
- Meant to be called from :class:`BaseGateway` only.
252
+ Triggered when the user sends an 'inactive' chat state (:xep:`0085`)
307
253
 
308
- :param m:
309
- :return:
254
+ :param chat: See :meth:`.BaseSession.on_text`
255
+ :param thread:
310
256
  """
311
- await self.active(await self.__get_entity(m))
257
+ raise NotImplementedError
312
258
 
313
- @ignore_message_to_component
314
- async def inactive_from_msg(self, m: Message):
315
- """
316
- Meant to be called from :class:`BaseGateway` only.
259
+ inactive = deprecated("BaseSession.inactive", on_inactive)
317
260
 
318
- :param m:
319
- :return:
261
+ async def on_composing(
262
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
263
+ ):
320
264
  """
321
- await self.inactive(await self.__get_entity(m))
265
+ Triggered when the user starts typing in a legacy chat (:xep:`0085`)
322
266
 
323
- @ignore_message_to_component
324
- async def composing_from_msg(self, m: Message):
267
+ :param chat: See :meth:`.BaseSession.on_text`
268
+ :param thread:
325
269
  """
326
- Meant to be called from :class:`BaseGateway` only.
270
+ raise NotImplementedError
327
271
 
328
- :param m:
329
- :return:
330
- """
331
- await self.composing(await self.__get_entity(m))
272
+ composing = deprecated("BaseSession.composing", on_composing)
332
273
 
333
- @ignore_message_to_component
334
- async def paused_from_msg(self, m: Message):
274
+ async def on_paused(
275
+ self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
276
+ ):
335
277
  """
336
- Meant to be called from :class:`BaseGateway` only.
278
+ Triggered when the user pauses typing in a legacy chat (:xep:`0085`)
337
279
 
338
- :param m:
339
- :return:
280
+ :param chat: See :meth:`.BaseSession.on_text`
281
+ :param thread:
340
282
  """
341
- await self.paused(await self.__get_entity(m))
283
+ raise NotImplementedError
342
284
 
343
- def __xmpp_msg_id_to_legacy(self, xmpp_id: str):
344
- sent = self.sent.inverse.get(xmpp_id)
345
- if sent:
346
- return sent
285
+ paused = deprecated("BaseSession.paused", on_paused)
347
286
 
348
- try:
349
- return self.xmpp_msg_id_to_legacy_msg_id(xmpp_id)
350
- except Exception as e:
351
- log.debug(
352
- "Couldn't convert xmpp msg ID to legacy ID, ignoring: %r, %s",
353
- e,
354
- e.args,
355
- )
287
+ async def on_displayed(
288
+ self,
289
+ chat: RecipientType,
290
+ legacy_msg_id: LegacyMessageType,
291
+ thread: Optional[LegacyThreadType] = None,
292
+ ):
293
+ """
294
+ Triggered when the user reads a message in a legacy chat. (:xep:`0333`)
356
295
 
357
- @ignore_message_to_component
358
- @ignore_sent_carbons
359
- async def displayed_from_msg(self, m: Message):
296
+ This is only possible if a valid ``legacy_msg_id`` was passed when
297
+ transmitting a message from a legacy chat to the user, eg in
298
+ :meth:`slidge.contact.LegacyContact.send_text`
299
+ or
300
+ :meth:`slidge.group.LegacyParticipant.send_text`.
301
+
302
+ :param chat: See :meth:`.BaseSession.on_text`
303
+ :param legacy_msg_id: Identifier of the message/
304
+ :param thread:
360
305
  """
361
- Meant to be called from :class:`BaseGateway` only.
306
+ raise NotImplementedError
362
307
 
363
- :param m:
364
- :return:
308
+ displayed = deprecated("BaseSession.displayed", on_displayed)
309
+
310
+ async def on_correct(
311
+ self,
312
+ chat: RecipientType,
313
+ text: str,
314
+ legacy_msg_id: LegacyMessageType,
315
+ *,
316
+ thread: Optional[LegacyThreadType] = None,
317
+ link_previews: Iterable[LinkPreview] = (),
318
+ mentions: Optional[list[Mention]] = None,
319
+ ) -> Optional[LegacyMessageType]:
365
320
  """
366
- e = await self.__get_entity(m)
367
- displayed_msg_id = m["displayed"]["id"]
368
- if not isinstance(e, LegacyMUC) and self.xmpp.MARK_ALL_MESSAGES:
369
- to_mark = e.get_msg_xmpp_id_up_to(displayed_msg_id) # type: ignore
370
- if to_mark is None:
371
- log.debug("Can't mark all messages up to %s", displayed_msg_id)
372
- to_mark = [displayed_msg_id]
373
- else:
374
- to_mark = [displayed_msg_id]
375
- for xmpp_id in to_mark:
376
- if legacy := self.__xmpp_msg_id_to_legacy(xmpp_id):
377
- await self.displayed(legacy, e)
378
- if isinstance(e, LegacyMUC):
379
- await e.echo(m, None)
380
- else:
381
- log.debug("Ignored displayed marker for msg: %r", xmpp_id)
382
-
383
- @ignore_message_to_component
384
- @ignore_sent_carbons
385
- async def correct_from_msg(self, m: Message):
386
- e = await self.__get_entity(m)
387
- xmpp_id = m["replace"]["id"]
388
- if isinstance(e, LegacyMUC):
389
- legacy_id = self.muc_sent_msg_ids.inverse.get(xmpp_id)
390
- else:
391
- legacy_id = self.__xmpp_msg_id_to_legacy(xmpp_id)
321
+ Triggered when the user corrects a message using :xep:`0308`
392
322
 
393
- if legacy_id is None:
394
- log.debug("Did not find legacy ID to correct")
395
- new_legacy_msg_id = await self.send_text(m["body"], e)
396
- else:
397
- new_legacy_msg_id = await self.correct(m["body"], legacy_id, e)
398
- if isinstance(e, LegacyMUC):
399
- if new_legacy_msg_id is not None:
400
- self.muc_sent_msg_ids[new_legacy_msg_id] = m.get_id()
401
- await e.echo(m, new_legacy_msg_id)
402
- else:
403
- if new_legacy_msg_id is not None:
404
- self.sent[new_legacy_msg_id] = m.get_id()
405
-
406
- @ignore_message_to_component
407
- @ignore_sent_carbons
408
- async def react_from_msg(self, m: Message):
409
- e = await self.__get_entity(m)
410
- react_to: str = m["reactions"]["id"]
411
- legacy_id = self.__xmpp_msg_id_to_legacy(react_to)
412
-
413
- if not legacy_id:
414
- log.debug("Ignored reaction from user")
415
- raise XMPPError("internal-server-error")
416
-
417
- emojis = [
418
- remove_emoji_variation_selector_16(r["value"]) for r in m["reactions"]
419
- ]
420
- error_msg = None
421
-
422
- if e.REACTIONS_SINGLE_EMOJI and len(emojis) > 1:
423
- error_msg = "Maximum 1 emoji/message"
424
-
425
- if not error_msg and (subset := await e.available_emojis(legacy_id)):
426
- log.debug("%s %s %s", set(emojis), subset, set(emojis).issubset(subset))
427
- if not set(emojis).issubset(subset):
428
- error_msg = (
429
- f"You can only react with the following emojis: {''.join(subset)}"
430
- )
323
+ This is only possible if a valid ``legacy_msg_id`` was returned by
324
+ :meth:`.on_text`.
431
325
 
432
- if error_msg:
433
- self.send_gateway_message(error_msg)
434
- if not isinstance(e, LegacyMUC):
435
- # no need to carbon for groups, we just don't echo the stanza
436
- e.react(legacy_id, carbon=True) # type: ignore
437
- await self.react(legacy_id, [], e)
438
- raise XMPPError("not-acceptable", text=error_msg)
439
-
440
- await self.react(legacy_id, emojis, e)
441
- if isinstance(e, LegacyMUC):
442
- await e.echo(m, None)
443
-
444
- @ignore_message_to_component
445
- @ignore_sent_carbons
446
- async def retract_from_msg(self, m: Message):
447
- e = await self.__get_entity(m)
448
- xmpp_id: str = m["apply_to"]["id"]
449
- legacy_id = self.__xmpp_msg_id_to_legacy(xmpp_id)
450
- if legacy_id:
451
- await self.retract(legacy_id, e)
452
- if isinstance(e, LegacyMUC):
453
- await e.echo(m, None)
454
- else:
455
- log.debug("Ignored retraction from user")
326
+ :param chat: See :meth:`.BaseSession.on_text`
327
+ :param text: The new text
328
+ :param legacy_msg_id: Identifier of the edited message
329
+ :param thread:
330
+ :param link_previews: A list of sender-generated link previews.
331
+ At the time of writing, only `Cheogram <https://wiki.soprani.ca/CheogramApp/LinkPreviews>`_
332
+ supports it.
333
+ :param mentions: (only for groups) A list of Contacts mentioned by their
334
+ nicknames.
335
+ """
336
+ raise NotImplementedError
456
337
 
457
- async def join_groupchat(self, p: Presence):
458
- if not self.xmpp.GROUPS:
459
- raise XMPPError(
460
- "not-implemented", "This gateway does not implement multi-user chats."
461
- )
462
- muc = await self.bookmarks.by_jid(p.get_to())
463
- log.debug("BOOKMARKS: %r", self.bookmarks.__class__)
464
- log.debug("JOIN MUC: %r -- %r -- %r", muc, muc.join, muc.__class__)
465
- await muc.join(p)
338
+ correct = deprecated("BaseSession.correct", on_correct)
466
339
 
467
- def send_gateway_status(
340
+ async def on_react(
468
341
  self,
469
- status: Optional[str] = None,
470
- show=Optional[PresenceShow],
471
- **kwargs,
342
+ chat: RecipientType,
343
+ legacy_msg_id: LegacyMessageType,
344
+ emojis: list[str],
345
+ thread: Optional[LegacyThreadType] = None,
472
346
  ):
473
347
  """
474
- Send a presence from the gateway to the user.
348
+ Triggered when the user sends message reactions (:xep:`0444`).
475
349
 
476
- Can be used to indicate the user session status, ie "SMS code required", "connected", …
350
+ :param chat: See :meth:`.BaseSession.on_text`
351
+ :param thread:
352
+ :param legacy_msg_id: ID of the message the user reacts to
353
+ :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``.
354
+ An empty string means "no reaction", ie, remove all reactions if any were present before
355
+ """
356
+ raise NotImplementedError
477
357
 
478
- :param status: A status message
479
- :param show: Presence stanza 'show' element. I suggest using "dnd" to show
480
- that the gateway is not fully functional
358
+ react = deprecated("BaseSession.react", on_react)
359
+
360
+ async def on_retract(
361
+ self,
362
+ chat: RecipientType,
363
+ legacy_msg_id: LegacyMessageType,
364
+ thread: Optional[LegacyThreadType] = None,
365
+ ):
481
366
  """
482
- self.xmpp.send_presence(
483
- pto=self.user.bare_jid, pstatus=status, pshow=show, **kwargs
484
- )
367
+ Triggered when the user retracts (:xep:`0424`) a message.
485
368
 
486
- def send_gateway_message(self, text, **msg_kwargs):
369
+ :param chat: See :meth:`.BaseSession.on_text`
370
+ :param thread:
371
+ :param legacy_msg_id: Legacy ID of the retracted message
487
372
  """
488
- Send a message from the gateway component to the user.
373
+ raise NotImplementedError
489
374
 
490
- Can be used to indicate the user session status, ie "SMS code required", "connected", …
375
+ retract = deprecated("BaseSession.retract", on_retract)
491
376
 
492
- :param text: A text
377
+ async def on_presence(
378
+ self,
379
+ resource: str,
380
+ show: PseudoPresenceShow,
381
+ status: str,
382
+ resources: dict[str, ResourceDict],
383
+ merged_resource: Optional[ResourceDict],
384
+ ):
493
385
  """
494
- self.xmpp.send_message(
495
- mto=self.user.jid, mbody=text, mfrom=self.xmpp.boundjid, **msg_kwargs
496
- )
386
+ Called when the gateway component receives a presence, ie, when
387
+ one of the user's clients goes online of offline, or changes its
388
+ status.
497
389
 
498
- async def input(self, text: str, **msg_kwargs):
390
+ :param resource: The XMPP client identifier, arbitrary string.
391
+ :param show: The presence ``<show>``, if available. If the resource is
392
+ just 'available' without any ``<show>`` element, this is an empty
393
+ str.
394
+ :param status: A status message, like a deeply profound quote, eg,
395
+ "Roses are red, violets are blue, [INSERT JOKE]".
396
+ :param resources: A summary of all the resources for this user.
397
+ :param merged_resource: A global presence for the user account,
398
+ following rules described in :meth:`merge_resources`
499
399
  """
500
- Request user input via direct messages.
400
+ raise NotImplementedError
501
401
 
502
- Wraps call to :meth:`.BaseSession.input`
402
+ presence = deprecated("BaseSession.presence", on_presence)
503
403
 
504
- :param text: The prompt to send to the user
505
- :param msg_kwargs: Extra attributes
506
- :return:
404
+ async def on_search(self, form_values: dict[str, str]) -> Optional[SearchResult]:
507
405
  """
508
- return await self.xmpp.input(self.user.jid, text, **msg_kwargs)
406
+ Triggered when the user uses Jabber Search (:xep:`0055`) on the component
509
407
 
510
- async def send_qr(self, text: str):
408
+ Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS`
409
+
410
+ :param form_values: search query, defined for a specific plugin by overriding
411
+ in :attr:`.BaseGateway.SEARCH_FIELDS`
412
+ :return:
511
413
  """
512
- Sends a QR code generated from 'text' via HTTP Upload and send the URL to
513
- ``self.user``
414
+ raise NotImplementedError
514
415
 
515
- :param text: Text to encode as a QR code
416
+ search = deprecated("BaseSession.search", on_search)
417
+
418
+ async def on_avatar(
419
+ self,
420
+ bytes_: Optional[bytes],
421
+ hash_: Optional[str],
422
+ type_: Optional[str],
423
+ width: Optional[int],
424
+ height: Optional[int],
425
+ ) -> None:
426
+ """
427
+ Triggered when the user uses modifies their avatar via :xep:`0084`.
428
+
429
+ :param bytes_: The data of the avatar. According to the spec, this
430
+ should always be a PNG, but some implementations do not respect
431
+ that. If `None` it means the user has unpublished their avatar.
432
+ :param hash_: The SHA1 hash of the avatar data. This is an identifier of
433
+ the avatar.
434
+ :param type_: The MIME type of the avatar.
435
+ :param width: The width of the avatar image.
436
+ :param height: The height of the avatar image.
516
437
  """
517
- await self.xmpp.send_qr(text, mto=self.user.jid)
438
+ raise NotImplementedError
518
439
 
519
- async def login(self) -> Optional[str]:
440
+ async def on_moderate(
441
+ self, muc: LegacyMUC, legacy_msg_id: LegacyMessageType, reason: Optional[str]
442
+ ):
520
443
  """
521
- Login the gateway user to the legacy network.
444
+ Triggered when the user attempts to retract a message that was sent in
445
+ a MUC using :xep:`0425`.
522
446
 
523
- Triggered when the gateway start and on user registration.
524
- It is recommended that this function returns once the user is logged in,
525
- so if you need to await forever (for instance to listen to incoming events),
526
- it's a good idea to wrap your listener in an asyncio.Task.
447
+ If retraction is not possible, this should raise the appropriate
448
+ XMPPError with a human-readable message.
527
449
 
528
- :return: Optionally, a text to use as the gateway status, e.g., "Connected as 'dude@legacy.network'"
450
+ NB: the legacy module is responsible for calling
451
+ :method:`LegacyParticipant.moderate` when this is successful, because
452
+ slidge will acknowledge the moderation IQ, but will not send the
453
+ moderation message from the MUC automatically.
454
+
455
+ :param muc: The MUC in which the message was sent
456
+ :param legacy_msg_id: The legacy ID of the message to be retracted
457
+ :param reason: Optionally, a reason for the moderation, given by the
458
+ user-moderator.
529
459
  """
530
460
  raise NotImplementedError
531
461
 
532
- async def logout(self):
462
+ async def on_create_group(
463
+ self, name: str, contacts: list[LegacyContact]
464
+ ) -> LegacyGroupIdType:
533
465
  """
534
- Logout the gateway user from the legacy network.
466
+ Triggered when the user request the creation of a group via the
467
+ dedicated :term:`Command`.
535
468
 
536
- Called on user unregistration and gateway shutdown.
469
+ :param name: Name of the group
470
+ :param contacts: list of contacts that should be members of the group
537
471
  """
538
472
  raise NotImplementedError
539
473
 
540
- def re_login(self):
474
+ async def on_invitation(
475
+ self, contact: LegacyContact, muc: LegacyMUC, reason: Optional[str]
476
+ ):
541
477
  """
542
- Logout then re-login
478
+ Triggered when the user invites a :term:`Contact` to a legacy MUC via
479
+ :xep:`0249`.
543
480
 
544
- No reason to override this
545
- """
546
- self.xmpp.re_login(self)
481
+ The default implementation calls :meth:`LegacyMUC.on_set_affiliation`
482
+ with the 'member' affiliation. Override if you want to customize this
483
+ behaviour.
547
484
 
548
- async def send_text(
549
- self,
550
- text: str,
551
- chat: Chat,
552
- *,
553
- reply_to_msg_id: Optional[LegacyMessageType] = None,
554
- reply_to_fallback_text: Optional[str] = None,
555
- reply_to: Optional[Union["LegacyContactType", "LegacyParticipantType"]] = None,
556
- ) -> Optional[LegacyMessageType]:
485
+ :param contact: The invitee
486
+ :param muc: The group
487
+ :param reason: Optionally, a reason
557
488
  """
558
- Triggered when the user sends a text message from XMPP to a bridged entity, e.g.
559
- to ``translated_user_name@slidge.example.com``, or ``translated_group_name@slidge.example.com``
489
+ await muc.on_set_affiliation(contact, "member", reason, None)
560
490
 
561
- Override this and implement sending a message to the legacy network in this method.
491
+ def __reset_ready(self):
492
+ self.ready = self.xmpp.loop.create_future()
562
493
 
563
- :param text: Content of the message
564
- :param chat: Recipient of the message. :class:`.LegacyContact` instance for 1:1 chat,
565
- :class:`.MUC` instance for groups.
566
- :param reply_to_msg_id: A legacy message ID if the message references (quotes)
567
- another message (:xep:`0461`)
568
- :param reply_to_fallback_text: Content of the quoted text. Not necessarily set
569
- by XMPP clients
570
- :param reply_to: Author of the quoted message. :class:`LegacyContact` instance for
571
- 1:1 chat, :class:`LegacyParticipant` instance for groups.
494
+ @property
495
+ def logged(self):
496
+ return self._logged
572
497
 
573
- :return: An ID of some sort that can be used later to ack and mark the message
574
- as read by the user
575
- """
576
- raise NotImplementedError
498
+ @logged.setter
499
+ def logged(self, v: bool):
500
+ self._logged = v
501
+ if self.ready.done():
502
+ if v:
503
+ return
504
+ self.__reset_ready()
505
+ else:
506
+ if v:
507
+ self.ready.set_result(True)
577
508
 
578
- async def send_file(
579
- self,
580
- url: str,
581
- chat: Chat,
582
- *,
583
- reply_to_msg_id: Optional[LegacyMessageType] = None,
584
- reply_to_fallback_text: Optional[str] = None,
585
- reply_to: Optional[Union[LegacyContactType, "LegacyParticipantType"]] = None,
586
- ) -> Optional[LegacyMessageType]:
587
- """
588
- Triggered when the user has sends a file using HTTP Upload (:xep:`0363`)
509
+ def __repr__(self):
510
+ return f"<Session of {self.user}>"
589
511
 
590
- :param url: URL of the file
591
- :param chat: See :meth:`.BaseSession.send_text`
592
- :param reply_to_msg_id: See :meth:`.BaseSession.send_text`
593
- :param reply_to_fallback_text: See :meth:`.BaseSession.send_text`
594
- :param reply_to: See :meth:`.BaseSession.send_text`
512
+ def shutdown(self) -> asyncio.Task:
513
+ for c in self.contacts:
514
+ c.offline()
515
+ for m in self.bookmarks:
516
+ m.shutdown()
517
+ return self.xmpp.loop.create_task(self.logout())
595
518
 
596
- :return: An ID of some sort that can be used later to ack and mark the message
597
- as read by the user
519
+ @staticmethod
520
+ def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
598
521
  """
599
- raise NotImplementedError
522
+ Convert a legacy msg ID to a valid XMPP msg ID.
523
+ Needed for read marks, retractions and message corrections.
600
524
 
601
- async def active(self, c: Chat):
602
- """
603
- Triggered when the user sends an 'active' chat state to the legacy network (:xep:`0085`)
525
+ The default implementation just converts the legacy ID to a :class:`str`,
526
+ but this should be overridden in case some characters needs to be escaped,
527
+ or to add some additional,
528
+ :term:`legacy network <Legacy Network`>-specific logic.
604
529
 
605
- :param c: Recipient of the active chat state
530
+ :param legacy_msg_id:
531
+ :return: A string that is usable as an XMPP stanza ID
606
532
  """
607
- raise NotImplementedError
533
+ return str(legacy_msg_id)
608
534
 
609
- async def inactive(self, c: Chat):
610
- """
611
- Triggered when the user sends an 'inactive' chat state to the legacy network (:xep:`0085`)
535
+ legacy_msg_id_to_xmpp_msg_id = staticmethod(
536
+ deprecated("BaseSession.legacy_msg_id_to_xmpp_msg_id", legacy_to_xmpp_msg_id)
537
+ )
612
538
 
613
- :param c:
539
+ @staticmethod
540
+ def xmpp_to_legacy_msg_id(i: str) -> LegacyMessageType:
614
541
  """
615
- raise NotImplementedError
542
+ Convert a legacy XMPP ID to a valid XMPP msg ID.
543
+ Needed for read marks and message corrections.
616
544
 
617
- async def composing(self, c: Chat):
618
- """
619
- Triggered when the user starts typing in the window of a legacy contact (:xep:`0085`)
545
+ The default implementation just converts the legacy ID to a :class:`str`,
546
+ but this should be overridden in case some characters needs to be escaped,
547
+ or to add some additional,
548
+ :term:`legacy network <Legacy Network`>-specific logic.
620
549
 
621
- :param c:
622
- """
623
- raise NotImplementedError
550
+ The default implementation is an identity function.
624
551
 
625
- async def paused(self, c: Chat):
552
+ :param i: The XMPP stanza ID
553
+ :return: An ID that can be used to identify a message on the legacy network
626
554
  """
627
- Triggered when the user pauses typing in the window of a legacy contact (:xep:`0085`)
555
+ return cast(LegacyMessageType, i)
628
556
 
629
- :param c:
557
+ xmpp_msg_id_to_legacy_msg_id = staticmethod(
558
+ deprecated("BaseSession.xmpp_msg_id_to_legacy_msg_id", xmpp_to_legacy_msg_id)
559
+ )
560
+
561
+ def raise_if_not_logged(self):
562
+ if not self.logged:
563
+ raise XMPPError(
564
+ "internal-server-error",
565
+ text="You are not logged to the legacy network",
566
+ )
567
+
568
+ @classmethod
569
+ def _from_user_or_none(cls, user):
570
+ if user is None:
571
+ log.debug("user not found", stack_info=True)
572
+ raise XMPPError(text="User not found", condition="subscription-required")
573
+
574
+ session = _sessions.get(user)
575
+ if session is None:
576
+ _sessions[user] = session = cls(user)
577
+ return session
578
+
579
+ @classmethod
580
+ def from_user(cls, user):
581
+ return cls._from_user_or_none(user)
582
+
583
+ @classmethod
584
+ def from_stanza(cls, s) -> "BaseSession":
585
+ # """
586
+ # Get a user's :class:`.LegacySession` using the "from" field of a stanza
587
+ #
588
+ # Meant to be called from :class:`BaseGateway` only.
589
+ #
590
+ # :param s:
591
+ # :return:
592
+ # """
593
+ return cls._from_user_or_none(user_store.get_by_stanza(s))
594
+
595
+ @classmethod
596
+ def from_jid(cls, jid: JID) -> "BaseSession":
597
+ # """
598
+ # Get a user's :class:`.LegacySession` using its jid
599
+ #
600
+ # Meant to be called from :class:`BaseGateway` only.
601
+ #
602
+ # :param jid:
603
+ # :return:
604
+ # """
605
+ return cls._from_user_or_none(user_store.get_by_jid(jid))
606
+
607
+ @classmethod
608
+ async def kill_by_jid(cls, jid: JID):
609
+ # """
610
+ # Terminate a user session.
611
+ #
612
+ # Meant to be called from :class:`BaseGateway` only.
613
+ #
614
+ # :param jid:
615
+ # :return:
616
+ # """
617
+ log.debug("Killing session of %s", jid)
618
+ for user, session in _sessions.items():
619
+ if user.jid == jid.bare:
620
+ break
621
+ else:
622
+ log.debug("Did not find a session for %s", jid)
623
+ return
624
+ for c in session.contacts:
625
+ c.unsubscribe()
626
+ await cls.xmpp.unregister(user)
627
+ del _sessions[user]
628
+ del user
629
+ del session
630
+
631
+ def __ack(self, msg: Message):
632
+ if not self.xmpp.PROPER_RECEIPTS:
633
+ self.xmpp.delivery_receipt.ack(msg)
634
+
635
+ def send_gateway_status(
636
+ self,
637
+ status: Optional[str] = None,
638
+ show=Optional[PresenceShows],
639
+ **kwargs,
640
+ ):
630
641
  """
631
- raise NotImplementedError
642
+ Send a presence from the gateway to the user.
632
643
 
633
- async def displayed(self, legacy_msg_id: LegacyMessageType, c: Chat):
644
+ Can be used to indicate the user session status, ie "SMS code required", "connected",
645
+
646
+ :param status: A status message
647
+ :param show: Presence stanza 'show' element. I suggest using "dnd" to show
648
+ that the gateway is not fully functional
634
649
  """
635
- Triggered when the user reads a message sent by a legacy contact. (:xep:`0333`)
650
+ self.__cached_presence = CachedPresence(status, show, kwargs)
651
+ self.xmpp.send_presence(
652
+ pto=self.user.bare_jid, pstatus=status, pshow=show, **kwargs
653
+ )
636
654
 
637
- This is only possible if a valid ``legacy_msg_id`` was passed when transmitting a message
638
- from a contact to the user in :meth:`.LegacyContact.sent_text` or :meth:`slidge.LegacyContact.send_file`.
655
+ def send_cached_presence(self, to: JID):
656
+ if not self.__cached_presence:
657
+ self.xmpp.send_presence(pto=to, ptype="unavailable")
658
+ return
659
+ self.xmpp.send_presence(
660
+ pto=to,
661
+ pstatus=self.__cached_presence.status,
662
+ pshow=self.__cached_presence.show,
663
+ **self.__cached_presence.kwargs,
664
+ )
639
665
 
640
- :param legacy_msg_id: Identifier of the message, passed to :meth:`slidge.LegacyContact.send_text`
641
- or :meth:`slidge.LegacyContact.send_file`
642
- :param c:
666
+ def send_gateway_message(self, text: str, **msg_kwargs):
643
667
  """
644
- raise NotImplementedError
668
+ Send a message from the gateway component to the user.
645
669
 
646
- async def correct(
647
- self, text: str, legacy_msg_id: LegacyMessageType, c: Chat
648
- ) -> Optional[LegacyMessageType]:
670
+ Can be used to indicate the user session status, ie "SMS code required", "connected", …
671
+
672
+ :param text: A text
649
673
  """
650
- Triggered when the user corrected a message using :xep:`0308`
674
+ self.xmpp.send_text(text, mto=self.user.jid, **msg_kwargs)
651
675
 
652
- This is only possible if a valid ``legacy_msg_id`` was passed when transmitting a message
653
- from a contact to the user in :meth:`.LegacyContact.send_text` or :meth:`slidge.LegacyContact.send_file`.
676
+ def send_gateway_invite(
677
+ self,
678
+ muc: LegacyMUC,
679
+ reason: Optional[str] = None,
680
+ password: Optional[str] = None,
681
+ ):
682
+ """
683
+ Send an invitation to join a MUC, emanating from the gateway component.
654
684
 
655
- :param text:
656
- :param legacy_msg_id:
657
- :param c:
685
+ :param muc:
686
+ :param reason:
687
+ :param password:
658
688
  """
659
- raise NotImplementedError
689
+ self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user.jid)
660
690
 
661
- async def search(self, form_values: dict[str, str]) -> Optional["SearchResult"]:
691
+ async def input(self, text: str, **msg_kwargs):
662
692
  """
663
- Triggered when the user uses Jabber Search (:xep:`0055`) on the component
693
+ Request user input via direct messages from the gateway component.
664
694
 
665
- Form values is a dict in which keys are defined in :attr:`.BaseGateway.SEARCH_FIELDS`
695
+ Wraps call to :meth:`.BaseSession.input`
666
696
 
667
- :param form_values: search query, defined for a specific plugin by overriding
668
- in :attr:`.BaseGateway.SEARCH_FIELDS`
697
+ :param text: The prompt to send to the user
698
+ :param msg_kwargs: Extra attributes
669
699
  :return:
670
700
  """
671
- raise NotImplementedError
701
+ return await self.xmpp.input(self.user.jid, text, **msg_kwargs)
672
702
 
673
- async def react(self, legacy_msg_id: LegacyMessageType, emojis: list[str], c: Chat):
703
+ async def send_qr(self, text: str):
674
704
  """
675
- Triggered when the user sends message reactions (:xep:`0444`).
705
+ Sends a QR code generated from 'text' via HTTP Upload and send the URL to
706
+ ``self.user``
676
707
 
677
- :param legacy_msg_id: ID of the message the user reacts to
678
- :param emojis: Unicode characters representing reactions to the message ``legacy_msg_id``.
679
- An empty string means "no reaction", ie, remove all reactions if any were present before
680
- :param c: Contact or MUC the reaction refers to
708
+ :param text: Text to encode as a QR code
681
709
  """
682
- raise NotImplementedError
710
+ await self.xmpp.send_qr(text, mto=self.user.jid)
683
711
 
684
- async def retract(self, legacy_msg_id: LegacyMessageType, c: Chat):
685
- """
686
- Triggered when the user retracts (:xep:`0424`) a message.
712
+ def re_login(self):
713
+ # Logout then re-login
714
+ #
715
+ # No reason to override this
716
+ self.xmpp.re_login(self)
687
717
 
688
- :param legacy_msg_id: Legacy ID of the retracted message
689
- :param c: The contact this retraction refers to
690
- """
691
- raise NotImplementedError
718
+ async def get_contact_or_group_or_participant(self, jid: JID):
719
+ if jid.bare in (contacts := self.contacts.known_contacts(only_friends=False)):
720
+ return contacts[jid.bare]
721
+ if jid.bare in (mucs := self.bookmarks._mucs_by_bare_jid):
722
+ return await self.__get_muc_or_participant(mucs[jid.bare], jid)
723
+ else:
724
+ muc = None
692
725
 
726
+ try:
727
+ return await self.contacts.by_jid(jid)
728
+ except XMPPError:
729
+ if muc is None:
730
+ try:
731
+ muc = await self.bookmarks.by_jid(jid)
732
+ except XMPPError:
733
+ return
734
+ return await self.__get_muc_or_participant(muc, jid)
693
735
 
694
- def remove_emoji_variation_selector_16(emoji: str):
695
- # this is required for compatibility with dino, and maybe other future clients?
696
- return bytes(emoji, encoding="utf-8").replace(b"\xef\xb8\x8f", b"").decode()
736
+ @staticmethod
737
+ async def __get_muc_or_participant(muc: LegacyMUC, jid: JID):
738
+ if nick := jid.resource:
739
+ try:
740
+ return await muc.get_participant(
741
+ nick, raise_if_not_found=True, fill_first=True
742
+ )
743
+ except XMPPError:
744
+ return None
745
+ return muc
746
+
747
+ async def wait_for_ready(self, timeout: Optional[Union[int, float]] = 10):
748
+ # """
749
+ # Wait until session, contacts and bookmarks are ready
750
+ #
751
+ # (slidge internal use)
752
+ #
753
+ # :param timeout:
754
+ # :return:
755
+ # """
756
+ try:
757
+ await asyncio.wait_for(asyncio.shield(self.ready), timeout)
758
+ await asyncio.wait_for(asyncio.shield(self.contacts.ready), timeout)
759
+ await asyncio.wait_for(asyncio.shield(self.bookmarks.ready), timeout)
760
+ except asyncio.TimeoutError:
761
+ raise XMPPError(
762
+ "recipient-unavailable",
763
+ "Legacy session is not fully initialized, retry later",
764
+ )
697
765
 
698
766
 
699
767
  _sessions: dict[GatewayUser, BaseSession] = {}