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.
- slidge/__init__.py +54 -31
- slidge/__main__.py +51 -5
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +2 -0
- slidge/core/cache.py +121 -39
- slidge/core/config.py +116 -11
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +895 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +795 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +9 -1
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +6 -19
- slidge/core/mixins/disco.py +66 -15
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +254 -252
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +128 -31
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +275 -116
- slidge/core/session.py +586 -518
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +458 -0
- slidge/group/room.py +1103 -0
- slidge/migration.py +18 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/{util → slixfix}/xep_0077/register.py +1 -2
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +4 -6
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +25 -4
- slidge/util/db.py +23 -69
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +136 -86
- slidge/util/types.py +155 -14
- slidge/util/util.py +225 -51
- slidge-0.1.2.dist-info/METADATA +111 -0
- slidge-0.1.2.dist-info/RECORD +96 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
- slidge/core/adhoc.py +0 -492
- slidge/core/chat_command.py +0 -197
- slidge/core/contact.py +0 -441
- slidge/core/disco.py +0 -59
- slidge/core/gateway.py +0 -899
- slidge/core/muc/__init__.py +0 -3
- slidge/core/muc/bookmarks.py +0 -74
- slidge/core/muc/participant.py +0 -152
- slidge/core/muc/room.py +0 -348
- slidge/plugins/discord/__init__.py +0 -121
- slidge/plugins/discord/client.py +0 -121
- slidge/plugins/discord/session.py +0 -172
- slidge/plugins/dummy.py +0 -334
- slidge/plugins/facebook.py +0 -591
- slidge/plugins/hackernews.py +0 -209
- slidge/plugins/mattermost/__init__.py +0 -1
- slidge/plugins/mattermost/api.py +0 -288
- slidge/plugins/mattermost/gateway.py +0 -417
- slidge/plugins/mattermost/websocket.py +0 -248
- slidge/plugins/signal/__init__.py +0 -4
- slidge/plugins/signal/config.py +0 -4
- slidge/plugins/signal/contact.py +0 -104
- slidge/plugins/signal/gateway.py +0 -379
- slidge/plugins/signal/group.py +0 -76
- slidge/plugins/signal/session.py +0 -515
- slidge/plugins/signal/txt.py +0 -13
- slidge/plugins/signal/util.py +0 -32
- slidge/plugins/skype.py +0 -310
- slidge/plugins/steam.py +0 -400
- slidge/plugins/telegram/__init__.py +0 -6
- slidge/plugins/telegram/client.py +0 -325
- slidge/plugins/telegram/config.py +0 -21
- slidge/plugins/telegram/contact.py +0 -154
- slidge/plugins/telegram/gateway.py +0 -182
- slidge/plugins/telegram/group.py +0 -184
- slidge/plugins/telegram/session.py +0 -275
- slidge/plugins/telegram/util.py +0 -153
- slidge/plugins/whatsapp/__init__.py +0 -6
- slidge/plugins/whatsapp/config.py +0 -17
- slidge/plugins/whatsapp/contact.py +0 -33
- slidge/plugins/whatsapp/event.go +0 -455
- slidge/plugins/whatsapp/gateway.go +0 -156
- slidge/plugins/whatsapp/gateway.py +0 -69
- slidge/plugins/whatsapp/go.mod +0 -17
- slidge/plugins/whatsapp/go.sum +0 -22
- slidge/plugins/whatsapp/session.go +0 -371
- slidge/plugins/whatsapp/session.py +0 -370
- slidge/util/xep_0030/__init__.py +0 -13
- slidge/util/xep_0030/disco.py +0 -811
- slidge/util/xep_0030/stanza/__init__.py +0 -7
- slidge/util/xep_0030/stanza/info.py +0 -270
- slidge/util/xep_0030/stanza/items.py +0 -147
- slidge/util/xep_0030/static.py +0 -467
- slidge/util/xep_0050/adhoc.py +0 -631
- slidge/util/xep_0050/stanza.py +0 -180
- slidge/util/xep_0077/stanza.py +0 -71
- slidge/util/xep_0292/__init__.py +0 -1
- slidge/util/xep_0292/stanza.py +0 -167
- slidge/util/xep_0292/vcard4.py +0 -74
- slidge/util/xep_0356/__init__.py +0 -7
- slidge/util/xep_0356/permissions.py +0 -35
- slidge/util/xep_0356/privilege.py +0 -160
- slidge/util/xep_0356/stanza.py +0 -44
- slidge/util/xep_0461/__init__.py +0 -6
- slidge/util/xep_0461/reply.py +0 -48
- slidge/util/xep_0461/stanza.py +0 -80
- slidge-0.1.0rc1.dist-info/METADATA +0 -171
- slidge-0.1.0rc1.dist-info/RECORD +0 -99
- /slidge/{plugins/__init__.py → py.typed} +0 -0
- /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
- /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
- /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
- {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
- {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
|
1
|
+
import asyncio
|
2
2
|
import logging
|
3
|
-
from typing import
|
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
|
-
|
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 ..
|
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
|
-
|
12
|
-
Chat,
|
13
|
-
GatewayType,
|
14
|
-
LegacyContactType,
|
27
|
+
LegacyGroupIdType,
|
15
28
|
LegacyMessageType,
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
29
|
+
LegacyThreadType,
|
30
|
+
LinkPreview,
|
31
|
+
Mention,
|
32
|
+
PseudoPresenceShow,
|
33
|
+
RecipientType,
|
34
|
+
ResourceDict,
|
21
35
|
)
|
22
|
-
from ..util.util import
|
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
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
67
|
-
|
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
|
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: "
|
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
|
-
|
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 =
|
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 =
|
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:
|
101
|
-
self.
|
108
|
+
self.contacts: LegacyRoster = LegacyRoster.get_self_or_unique_subclass()(self)
|
109
|
+
self._logged = False
|
110
|
+
self.__reset_ready()
|
102
111
|
|
103
|
-
self.bookmarks:
|
112
|
+
self.bookmarks: LegacyBookmarks = LegacyBookmarks.get_self_or_unique_subclass()(
|
104
113
|
self
|
105
114
|
)
|
106
115
|
|
107
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
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
|
-
:
|
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
|
-
|
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
|
-
|
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
|
-
|
129
|
+
def __remove_task(self, fut):
|
130
|
+
self.log.debug("Removing fut %s", fut)
|
131
|
+
self.__tasks.remove(fut)
|
140
132
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
return session
|
143
|
+
async def login(self) -> Optional[str]:
|
144
|
+
"""
|
145
|
+
Logs in the gateway user to the legacy network.
|
157
146
|
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
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
|
-
|
154
|
+
raise NotImplementedError
|
166
155
|
|
167
|
-
|
156
|
+
async def logout(self):
|
157
|
+
"""
|
158
|
+
Logs out the gateway user from the legacy network.
|
168
159
|
|
169
|
-
|
170
|
-
:return:
|
160
|
+
Called on gateway shutdown.
|
171
161
|
"""
|
172
|
-
|
162
|
+
raise NotImplementedError
|
173
163
|
|
174
|
-
|
175
|
-
|
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
|
-
|
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
|
-
|
180
|
+
Override this and implement sending a message to the legacy network in this method.
|
180
181
|
|
181
|
-
:param
|
182
|
-
:
|
183
|
-
|
184
|
-
|
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
|
-
|
187
|
-
|
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
|
-
|
202
|
+
raise NotImplementedError
|
190
203
|
|
191
|
-
|
204
|
+
send_text = deprecated("BaseSession.send_text", on_text)
|
192
205
|
|
193
|
-
|
194
|
-
|
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
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
:
|
218
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
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
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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
|
-
|
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
|
-
|
304
|
-
|
248
|
+
async def on_inactive(
|
249
|
+
self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
|
250
|
+
):
|
305
251
|
"""
|
306
|
-
|
252
|
+
Triggered when the user sends an 'inactive' chat state (:xep:`0085`)
|
307
253
|
|
308
|
-
:param
|
309
|
-
:
|
254
|
+
:param chat: See :meth:`.BaseSession.on_text`
|
255
|
+
:param thread:
|
310
256
|
"""
|
311
|
-
|
257
|
+
raise NotImplementedError
|
312
258
|
|
313
|
-
|
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
|
-
|
319
|
-
:
|
261
|
+
async def on_composing(
|
262
|
+
self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
|
263
|
+
):
|
320
264
|
"""
|
321
|
-
|
265
|
+
Triggered when the user starts typing in a legacy chat (:xep:`0085`)
|
322
266
|
|
323
|
-
|
324
|
-
|
267
|
+
:param chat: See :meth:`.BaseSession.on_text`
|
268
|
+
:param thread:
|
325
269
|
"""
|
326
|
-
|
270
|
+
raise NotImplementedError
|
327
271
|
|
328
|
-
|
329
|
-
:return:
|
330
|
-
"""
|
331
|
-
await self.composing(await self.__get_entity(m))
|
272
|
+
composing = deprecated("BaseSession.composing", on_composing)
|
332
273
|
|
333
|
-
|
334
|
-
|
274
|
+
async def on_paused(
|
275
|
+
self, chat: RecipientType, thread: Optional[LegacyThreadType] = None
|
276
|
+
):
|
335
277
|
"""
|
336
|
-
|
278
|
+
Triggered when the user pauses typing in a legacy chat (:xep:`0085`)
|
337
279
|
|
338
|
-
:param
|
339
|
-
:
|
280
|
+
:param chat: See :meth:`.BaseSession.on_text`
|
281
|
+
:param thread:
|
340
282
|
"""
|
341
|
-
|
283
|
+
raise NotImplementedError
|
342
284
|
|
343
|
-
|
344
|
-
sent = self.sent.inverse.get(xmpp_id)
|
345
|
-
if sent:
|
346
|
-
return sent
|
285
|
+
paused = deprecated("BaseSession.paused", on_paused)
|
347
286
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
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
|
-
|
358
|
-
|
359
|
-
|
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
|
-
|
306
|
+
raise NotImplementedError
|
362
307
|
|
363
|
-
|
364
|
-
|
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
|
-
|
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
|
394
|
-
|
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
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
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
|
-
|
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
|
340
|
+
async def on_react(
|
468
341
|
self,
|
469
|
-
|
470
|
-
|
471
|
-
|
342
|
+
chat: RecipientType,
|
343
|
+
legacy_msg_id: LegacyMessageType,
|
344
|
+
emojis: list[str],
|
345
|
+
thread: Optional[LegacyThreadType] = None,
|
472
346
|
):
|
473
347
|
"""
|
474
|
-
|
348
|
+
Triggered when the user sends message reactions (:xep:`0444`).
|
475
349
|
|
476
|
-
|
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
|
-
|
479
|
-
|
480
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
373
|
+
raise NotImplementedError
|
489
374
|
|
490
|
-
|
375
|
+
retract = deprecated("BaseSession.retract", on_retract)
|
491
376
|
|
492
|
-
|
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
|
-
|
495
|
-
|
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
|
-
|
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
|
-
|
400
|
+
raise NotImplementedError
|
501
401
|
|
502
|
-
|
402
|
+
presence = deprecated("BaseSession.presence", on_presence)
|
503
403
|
|
504
|
-
|
505
|
-
:param msg_kwargs: Extra attributes
|
506
|
-
:return:
|
404
|
+
async def on_search(self, form_values: dict[str, str]) -> Optional[SearchResult]:
|
507
405
|
"""
|
508
|
-
|
406
|
+
Triggered when the user uses Jabber Search (:xep:`0055`) on the component
|
509
407
|
|
510
|
-
|
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
|
-
|
513
|
-
``self.user``
|
414
|
+
raise NotImplementedError
|
514
415
|
|
515
|
-
|
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
|
-
|
438
|
+
raise NotImplementedError
|
518
439
|
|
519
|
-
async def
|
440
|
+
async def on_moderate(
|
441
|
+
self, muc: LegacyMUC, legacy_msg_id: LegacyMessageType, reason: Optional[str]
|
442
|
+
):
|
520
443
|
"""
|
521
|
-
|
444
|
+
Triggered when the user attempts to retract a message that was sent in
|
445
|
+
a MUC using :xep:`0425`.
|
522
446
|
|
523
|
-
|
524
|
-
|
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
|
-
:
|
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
|
462
|
+
async def on_create_group(
|
463
|
+
self, name: str, contacts: list[LegacyContact]
|
464
|
+
) -> LegacyGroupIdType:
|
533
465
|
"""
|
534
|
-
|
466
|
+
Triggered when the user request the creation of a group via the
|
467
|
+
dedicated :term:`Command`.
|
535
468
|
|
536
|
-
|
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
|
474
|
+
async def on_invitation(
|
475
|
+
self, contact: LegacyContact, muc: LegacyMUC, reason: Optional[str]
|
476
|
+
):
|
541
477
|
"""
|
542
|
-
|
478
|
+
Triggered when the user invites a :term:`Contact` to a legacy MUC via
|
479
|
+
:xep:`0249`.
|
543
480
|
|
544
|
-
|
545
|
-
|
546
|
-
|
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
|
-
|
549
|
-
|
550
|
-
|
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
|
-
|
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
|
-
|
491
|
+
def __reset_ready(self):
|
492
|
+
self.ready = self.xmpp.loop.create_future()
|
562
493
|
|
563
|
-
|
564
|
-
|
565
|
-
|
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
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
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
|
-
|
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
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
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
|
-
|
597
|
-
|
519
|
+
@staticmethod
|
520
|
+
def legacy_to_xmpp_msg_id(legacy_msg_id: LegacyMessageType) -> str:
|
598
521
|
"""
|
599
|
-
|
522
|
+
Convert a legacy msg ID to a valid XMPP msg ID.
|
523
|
+
Needed for read marks, retractions and message corrections.
|
600
524
|
|
601
|
-
|
602
|
-
|
603
|
-
|
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
|
530
|
+
:param legacy_msg_id:
|
531
|
+
:return: A string that is usable as an XMPP stanza ID
|
606
532
|
"""
|
607
|
-
|
533
|
+
return str(legacy_msg_id)
|
608
534
|
|
609
|
-
|
610
|
-
""
|
611
|
-
|
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
|
-
|
539
|
+
@staticmethod
|
540
|
+
def xmpp_to_legacy_msg_id(i: str) -> LegacyMessageType:
|
614
541
|
"""
|
615
|
-
|
542
|
+
Convert a legacy XMPP ID to a valid XMPP msg ID.
|
543
|
+
Needed for read marks and message corrections.
|
616
544
|
|
617
|
-
|
618
|
-
|
619
|
-
|
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
|
-
|
622
|
-
"""
|
623
|
-
raise NotImplementedError
|
550
|
+
The default implementation is an identity function.
|
624
551
|
|
625
|
-
|
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
|
-
|
555
|
+
return cast(LegacyMessageType, i)
|
628
556
|
|
629
|
-
|
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
|
-
|
642
|
+
Send a presence from the gateway to the user.
|
632
643
|
|
633
|
-
|
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
|
-
|
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
|
-
|
638
|
-
|
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
|
-
|
641
|
-
or :meth:`slidge.LegacyContact.send_file`
|
642
|
-
:param c:
|
666
|
+
def send_gateway_message(self, text: str, **msg_kwargs):
|
643
667
|
"""
|
644
|
-
|
668
|
+
Send a message from the gateway component to the user.
|
645
669
|
|
646
|
-
|
647
|
-
|
648
|
-
|
670
|
+
Can be used to indicate the user session status, ie "SMS code required", "connected", …
|
671
|
+
|
672
|
+
:param text: A text
|
649
673
|
"""
|
650
|
-
|
674
|
+
self.xmpp.send_text(text, mto=self.user.jid, **msg_kwargs)
|
651
675
|
|
652
|
-
|
653
|
-
|
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
|
656
|
-
:param
|
657
|
-
:param
|
685
|
+
:param muc:
|
686
|
+
:param reason:
|
687
|
+
:param password:
|
658
688
|
"""
|
659
|
-
|
689
|
+
self.xmpp.invite_to(muc, reason=reason, password=password, mto=self.user.jid)
|
660
690
|
|
661
|
-
async def
|
691
|
+
async def input(self, text: str, **msg_kwargs):
|
662
692
|
"""
|
663
|
-
|
693
|
+
Request user input via direct messages from the gateway component.
|
664
694
|
|
665
|
-
|
695
|
+
Wraps call to :meth:`.BaseSession.input`
|
666
696
|
|
667
|
-
:param
|
668
|
-
|
697
|
+
:param text: The prompt to send to the user
|
698
|
+
:param msg_kwargs: Extra attributes
|
669
699
|
:return:
|
670
700
|
"""
|
671
|
-
|
701
|
+
return await self.xmpp.input(self.user.jid, text, **msg_kwargs)
|
672
702
|
|
673
|
-
async def
|
703
|
+
async def send_qr(self, text: str):
|
674
704
|
"""
|
675
|
-
|
705
|
+
Sends a QR code generated from 'text' via HTTP Upload and send the URL to
|
706
|
+
``self.user``
|
676
707
|
|
677
|
-
:param
|
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
|
-
|
710
|
+
await self.xmpp.send_qr(text, mto=self.user.jid)
|
683
711
|
|
684
|
-
|
685
|
-
|
686
|
-
|
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
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
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
|
-
|
695
|
-
|
696
|
-
|
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] = {}
|