slidge 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slidge/__init__.py +61 -0
- slidge/__main__.py +192 -0
- 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 +3 -0
- slidge/core/cache.py +183 -0
- slidge/core/config.py +209 -0
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +892 -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 +757 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +19 -0
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +31 -0
- slidge/core/mixins/disco.py +130 -0
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +398 -0
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +217 -0
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +525 -0
- slidge/core/session.py +752 -0
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +440 -0
- slidge/group/room.py +1095 -0
- slidge/migration.py +18 -0
- slidge/py.typed +0 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/slixfix/link_preview/__init__.py +10 -0
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/slixfix/xep_0077/__init__.py +10 -0
- slidge/slixfix/xep_0077/register.py +289 -0
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/slixfix/xep_0100/__init__.py +5 -0
- slidge/slixfix/xep_0100/gateway.py +121 -0
- slidge/slixfix/xep_0100/stanza.py +9 -0
- 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/slixfix/xep_0356_old/__init__.py +7 -0
- slidge/slixfix/xep_0356_old/privilege.py +167 -0
- slidge/slixfix/xep_0356_old/stanza.py +44 -0
- 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 +15 -0
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +206 -0
- slidge/util/db.py +229 -0
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +295 -0
- slidge/util/types.py +180 -0
- slidge/util/util.py +295 -0
- slidge-0.1.0.dist-info/LICENSE +661 -0
- slidge-0.1.0.dist-info/METADATA +109 -0
- slidge-0.1.0.dist-info/RECORD +96 -0
- slidge-0.1.0.dist-info/WHEEL +4 -0
- slidge-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,757 @@
|
|
1
|
+
import logging
|
2
|
+
from copy import copy
|
3
|
+
from typing import TYPE_CHECKING, Awaitable, Callable, Optional, Union
|
4
|
+
|
5
|
+
from slixmpp import JID, CoroutineCallback, Iq, Message, Presence, StanzaPath
|
6
|
+
from slixmpp.exceptions import XMPPError
|
7
|
+
from slixmpp.plugins.xep_0004 import Form
|
8
|
+
from slixmpp.plugins.xep_0084.stanza import Info
|
9
|
+
|
10
|
+
from ... import LegacyContact
|
11
|
+
from ...group.room import LegacyMUC
|
12
|
+
from ...util.sql import db
|
13
|
+
from ...util.types import LinkPreview, Recipient, RecipientType
|
14
|
+
from ...util.util import (
|
15
|
+
dict_to_named_tuple,
|
16
|
+
merge_resources,
|
17
|
+
remove_emoji_variation_selector_16,
|
18
|
+
)
|
19
|
+
from .. import config
|
20
|
+
from ..session import BaseSession
|
21
|
+
|
22
|
+
if TYPE_CHECKING:
|
23
|
+
from .base import BaseGateway
|
24
|
+
|
25
|
+
HandlerType = Callable[[Union[Presence, Message]], Awaitable[None]]
|
26
|
+
|
27
|
+
|
28
|
+
class Ignore(BaseException):
|
29
|
+
pass
|
30
|
+
|
31
|
+
|
32
|
+
class SessionDispatcher:
|
33
|
+
def __init__(self, xmpp: "BaseGateway"):
|
34
|
+
self.xmpp = xmpp
|
35
|
+
self.http = xmpp.http
|
36
|
+
|
37
|
+
xmpp.register_handler(
|
38
|
+
CoroutineCallback(
|
39
|
+
"MUCModerate",
|
40
|
+
StanzaPath("iq/apply_to/moderate"),
|
41
|
+
_exceptions_to_xmpp_errors(self.on_user_moderation), # type:ignore
|
42
|
+
)
|
43
|
+
)
|
44
|
+
xmpp.register_handler(
|
45
|
+
CoroutineCallback(
|
46
|
+
"MUCSetAffiliation",
|
47
|
+
StanzaPath("iq@type=set/mucadmin_query"),
|
48
|
+
_exceptions_to_xmpp_errors(self.on_user_set_affiliation), # type:ignore
|
49
|
+
)
|
50
|
+
)
|
51
|
+
xmpp.register_handler(
|
52
|
+
CoroutineCallback(
|
53
|
+
"muc#admin",
|
54
|
+
StanzaPath("iq@type=get/mucowner_query"),
|
55
|
+
_exceptions_to_xmpp_errors(self.on_muc_owner_query), # type: ignore
|
56
|
+
)
|
57
|
+
)
|
58
|
+
xmpp.register_handler(
|
59
|
+
CoroutineCallback(
|
60
|
+
"muc#admin",
|
61
|
+
StanzaPath("iq@type=set/mucowner_query"),
|
62
|
+
_exceptions_to_xmpp_errors(self.on_muc_owner_set), # type: ignore
|
63
|
+
)
|
64
|
+
)
|
65
|
+
|
66
|
+
for event in (
|
67
|
+
"legacy_message",
|
68
|
+
"marker_displayed",
|
69
|
+
"presence",
|
70
|
+
"chatstate_active",
|
71
|
+
"chatstate_inactive",
|
72
|
+
"chatstate_composing",
|
73
|
+
"chatstate_paused",
|
74
|
+
"message_correction",
|
75
|
+
"reactions",
|
76
|
+
"message_retract",
|
77
|
+
"groupchat_join",
|
78
|
+
"groupchat_message",
|
79
|
+
"groupchat_direct_invite",
|
80
|
+
"groupchat_subject",
|
81
|
+
"avatar_metadata_publish",
|
82
|
+
"message_displayed_synchronization_publish",
|
83
|
+
):
|
84
|
+
xmpp.add_event_handler(
|
85
|
+
event, _exceptions_to_xmpp_errors(getattr(self, "on_" + event))
|
86
|
+
)
|
87
|
+
|
88
|
+
async def __get_session(
|
89
|
+
self, stanza: Union[Message, Presence, Iq], timeout: Optional[int] = 10
|
90
|
+
) -> BaseSession:
|
91
|
+
xmpp = self.xmpp
|
92
|
+
if stanza.get_from().server == xmpp.boundjid.bare:
|
93
|
+
log.debug("Ignoring echo")
|
94
|
+
raise Ignore
|
95
|
+
if (
|
96
|
+
isinstance(stanza, Message)
|
97
|
+
and stanza.get_type() == "chat"
|
98
|
+
and stanza.get_to() == xmpp.boundjid.bare
|
99
|
+
):
|
100
|
+
log.debug("Ignoring message to component")
|
101
|
+
raise Ignore
|
102
|
+
session = xmpp.get_session_from_stanza(stanza)
|
103
|
+
await session.wait_for_ready(timeout)
|
104
|
+
if isinstance(stanza, Message) and _ignore(session, stanza):
|
105
|
+
raise Ignore
|
106
|
+
return session
|
107
|
+
|
108
|
+
def __ack(self, msg: Message):
|
109
|
+
if not self.xmpp.PROPER_RECEIPTS:
|
110
|
+
self.xmpp.delivery_receipt.ack(msg)
|
111
|
+
|
112
|
+
async def __get_session_entity_thread(
|
113
|
+
self, msg: Message
|
114
|
+
) -> tuple["BaseSession", Recipient, Union[int, str]]:
|
115
|
+
session = await self.__get_session(msg)
|
116
|
+
e: Recipient = await _get_entity(session, msg)
|
117
|
+
legacy_thread = await _xmpp_to_legacy_thread(session, msg, e)
|
118
|
+
return session, e, legacy_thread
|
119
|
+
|
120
|
+
async def on_legacy_message(self, msg: Message):
|
121
|
+
"""
|
122
|
+
Meant to be called from :class:`BaseGateway` only.
|
123
|
+
|
124
|
+
:param msg:
|
125
|
+
:return:
|
126
|
+
"""
|
127
|
+
# we MUST not use `if m["replace"]["id"]` because it adds the tag if not
|
128
|
+
# present. this is a problem for MUC echoed messages
|
129
|
+
if msg.get_plugin("replace", check=True) is not None:
|
130
|
+
# ignore last message correction (handled by a specific method)
|
131
|
+
return
|
132
|
+
if msg.get_plugin("apply_to", check=True) is not None:
|
133
|
+
# ignore message retraction (handled by a specific method)
|
134
|
+
return
|
135
|
+
if msg.get_plugin("reactions", check=True) is not None:
|
136
|
+
# ignore message reaction fallback.
|
137
|
+
# the reaction itself is handled by self.react_from_msg().
|
138
|
+
return
|
139
|
+
if msg.get_plugin("retract", check=True) is not None:
|
140
|
+
# ignore message retraction fallback.
|
141
|
+
# the retraction itself is handled by self.on_retract
|
142
|
+
return
|
143
|
+
|
144
|
+
session, entity, thread = await self.__get_session_entity_thread(msg)
|
145
|
+
|
146
|
+
e: Recipient = await _get_entity(session, msg)
|
147
|
+
log.debug("Entity %r", e)
|
148
|
+
|
149
|
+
if msg.get_plugin("oob", check=True) is not None:
|
150
|
+
url = msg["oob"]["url"]
|
151
|
+
else:
|
152
|
+
url = None
|
153
|
+
|
154
|
+
text = msg["body"]
|
155
|
+
|
156
|
+
reply_to = None
|
157
|
+
reply_fallback = None
|
158
|
+
if msg.get_plugin("reply", check=True):
|
159
|
+
try:
|
160
|
+
reply_to_msg_xmpp_id = _xmpp_msg_id_to_legacy(
|
161
|
+
session, msg["reply"]["id"]
|
162
|
+
)
|
163
|
+
except XMPPError:
|
164
|
+
session.log.debug(
|
165
|
+
"Could not determine reply-to legacy msg ID, sending quote instead."
|
166
|
+
)
|
167
|
+
text = msg["body"]
|
168
|
+
reply_fallback = None
|
169
|
+
reply_to_msg_xmpp_id = None
|
170
|
+
else:
|
171
|
+
reply_to_jid = JID(msg["reply"]["to"])
|
172
|
+
if msg["type"] == "chat":
|
173
|
+
if reply_to_jid.bare != session.user.jid.bare:
|
174
|
+
try:
|
175
|
+
reply_to = await session.contacts.by_jid(reply_to_jid)
|
176
|
+
except XMPPError:
|
177
|
+
pass
|
178
|
+
elif msg["type"] == "groupchat":
|
179
|
+
nick = reply_to_jid.resource
|
180
|
+
try:
|
181
|
+
muc = await session.bookmarks.by_jid(reply_to_jid)
|
182
|
+
except XMPPError:
|
183
|
+
pass
|
184
|
+
else:
|
185
|
+
if nick != muc.user_nick:
|
186
|
+
reply_to = await muc.get_participant(
|
187
|
+
reply_to_jid.resource, store=False
|
188
|
+
)
|
189
|
+
if msg.get_plugin("fallback", check=True) and (
|
190
|
+
isinstance(e, LegacyMUC) or e.REPLIES
|
191
|
+
):
|
192
|
+
text = msg["fallback"].get_stripped_body(
|
193
|
+
self.xmpp["xep_0461"].namespace
|
194
|
+
)
|
195
|
+
try:
|
196
|
+
reply_fallback = msg["reply"].get_fallback_body()
|
197
|
+
except AttributeError:
|
198
|
+
pass
|
199
|
+
else:
|
200
|
+
reply_to_msg_xmpp_id = None
|
201
|
+
reply_to = None
|
202
|
+
|
203
|
+
if msg.get_plugin("link_previews", check=True):
|
204
|
+
pass
|
205
|
+
|
206
|
+
kwargs = dict(
|
207
|
+
reply_to_msg_id=reply_to_msg_xmpp_id,
|
208
|
+
reply_to_fallback_text=reply_fallback,
|
209
|
+
reply_to=reply_to,
|
210
|
+
thread=thread,
|
211
|
+
)
|
212
|
+
|
213
|
+
if not url and isinstance(e, LegacyMUC):
|
214
|
+
kwargs["mentions"] = await e.parse_mentions(text)
|
215
|
+
|
216
|
+
if previews := msg["link_previews"]:
|
217
|
+
kwargs["link_previews"] = [
|
218
|
+
dict_to_named_tuple(p, LinkPreview) for p in previews
|
219
|
+
]
|
220
|
+
|
221
|
+
if url:
|
222
|
+
async with self.http.get(url) as response:
|
223
|
+
if response.status >= 400:
|
224
|
+
session.log.warning(
|
225
|
+
(
|
226
|
+
"OOB url cannot be downloaded: %s, sending the URL as text"
|
227
|
+
" instead."
|
228
|
+
),
|
229
|
+
response,
|
230
|
+
)
|
231
|
+
legacy_msg_id = await session.on_text(e, url, **kwargs)
|
232
|
+
else:
|
233
|
+
legacy_msg_id = await session.on_file(
|
234
|
+
e, url, http_response=response, **kwargs
|
235
|
+
)
|
236
|
+
elif text:
|
237
|
+
legacy_msg_id = await session.on_text(e, text, **kwargs)
|
238
|
+
else:
|
239
|
+
log.debug("Ignoring %s", msg.get_id())
|
240
|
+
return
|
241
|
+
|
242
|
+
if isinstance(e, LegacyMUC):
|
243
|
+
await e.echo(msg, legacy_msg_id)
|
244
|
+
if legacy_msg_id is not None:
|
245
|
+
session.muc_sent_msg_ids[legacy_msg_id] = msg.get_id()
|
246
|
+
else:
|
247
|
+
self.__ack(msg)
|
248
|
+
if legacy_msg_id is not None:
|
249
|
+
session.sent[legacy_msg_id] = msg.get_id()
|
250
|
+
if session.MESSAGE_IDS_ARE_THREAD_IDS and (t := msg["thread"]):
|
251
|
+
session.threads[t] = legacy_msg_id
|
252
|
+
|
253
|
+
async def on_groupchat_message(self, msg: Message):
|
254
|
+
await self.on_legacy_message(msg)
|
255
|
+
|
256
|
+
async def on_message_correction(self, msg: Message):
|
257
|
+
session, entity, thread = await self.__get_session_entity_thread(msg)
|
258
|
+
xmpp_id = msg["replace"]["id"]
|
259
|
+
if isinstance(entity, LegacyMUC):
|
260
|
+
legacy_id = session.muc_sent_msg_ids.inverse.get(xmpp_id)
|
261
|
+
else:
|
262
|
+
legacy_id = _xmpp_msg_id_to_legacy(session, xmpp_id)
|
263
|
+
|
264
|
+
if isinstance(entity, LegacyMUC):
|
265
|
+
mentions = await entity.parse_mentions(msg["body"])
|
266
|
+
else:
|
267
|
+
mentions = None
|
268
|
+
|
269
|
+
if previews := msg["link_previews"]:
|
270
|
+
link_previews = [dict_to_named_tuple(p, LinkPreview) for p in previews]
|
271
|
+
else:
|
272
|
+
link_previews = []
|
273
|
+
|
274
|
+
if legacy_id is None:
|
275
|
+
log.debug("Did not find legacy ID to correct")
|
276
|
+
new_legacy_msg_id = await session.on_text(
|
277
|
+
entity,
|
278
|
+
"Correction:" + msg["body"],
|
279
|
+
thread=thread,
|
280
|
+
mentions=mentions,
|
281
|
+
link_previews=link_previews,
|
282
|
+
)
|
283
|
+
elif (
|
284
|
+
not msg["body"].strip()
|
285
|
+
and config.CORRECTION_EMPTY_BODY_AS_RETRACTION
|
286
|
+
and entity.RETRACTION
|
287
|
+
):
|
288
|
+
await session.on_retract(entity, legacy_id, thread=thread)
|
289
|
+
new_legacy_msg_id = None
|
290
|
+
elif entity.CORRECTION:
|
291
|
+
new_legacy_msg_id = await session.on_correct(
|
292
|
+
entity,
|
293
|
+
msg["body"],
|
294
|
+
legacy_id,
|
295
|
+
thread=thread,
|
296
|
+
mentions=mentions,
|
297
|
+
link_previews=link_previews,
|
298
|
+
)
|
299
|
+
else:
|
300
|
+
session.send_gateway_message(
|
301
|
+
"Last message correction is not supported by this legacy service. "
|
302
|
+
"Slidge will send your correction as new message."
|
303
|
+
)
|
304
|
+
if (
|
305
|
+
config.LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND
|
306
|
+
and entity.RETRACTION
|
307
|
+
and legacy_id is not None
|
308
|
+
):
|
309
|
+
if legacy_id is not None:
|
310
|
+
session.send_gateway_message(
|
311
|
+
"Slidge will attempt to retract the original message you wanted"
|
312
|
+
" to edit."
|
313
|
+
)
|
314
|
+
await session.on_retract(entity, legacy_id, thread=thread)
|
315
|
+
|
316
|
+
new_legacy_msg_id = await session.on_text(
|
317
|
+
entity,
|
318
|
+
"Correction: " + msg["body"],
|
319
|
+
thread=thread,
|
320
|
+
mentions=mentions,
|
321
|
+
link_previews=link_previews,
|
322
|
+
)
|
323
|
+
|
324
|
+
if isinstance(entity, LegacyMUC):
|
325
|
+
if new_legacy_msg_id is not None:
|
326
|
+
session.muc_sent_msg_ids[new_legacy_msg_id] = msg.get_id()
|
327
|
+
await entity.echo(msg, new_legacy_msg_id)
|
328
|
+
else:
|
329
|
+
self.__ack(msg)
|
330
|
+
if new_legacy_msg_id is not None:
|
331
|
+
session.sent[new_legacy_msg_id] = msg.get_id()
|
332
|
+
|
333
|
+
async def on_message_retract(self, msg: Message):
|
334
|
+
session, entity, thread = await self.__get_session_entity_thread(msg)
|
335
|
+
if not entity.RETRACTION:
|
336
|
+
raise XMPPError(
|
337
|
+
"bad-request",
|
338
|
+
"This legacy service does not support message retraction.",
|
339
|
+
)
|
340
|
+
xmpp_id: str = msg["retract"]["id"]
|
341
|
+
legacy_id = _xmpp_msg_id_to_legacy(session, xmpp_id)
|
342
|
+
if legacy_id:
|
343
|
+
await session.on_retract(entity, legacy_id, thread=thread)
|
344
|
+
if isinstance(entity, LegacyMUC):
|
345
|
+
await entity.echo(msg, None)
|
346
|
+
else:
|
347
|
+
log.debug("Ignored retraction from user")
|
348
|
+
self.__ack(msg)
|
349
|
+
|
350
|
+
async def on_marker_displayed(self, msg: Message):
|
351
|
+
session = await self.__get_session(msg)
|
352
|
+
|
353
|
+
e: Recipient = await _get_entity(session, msg)
|
354
|
+
legacy_thread = await _xmpp_to_legacy_thread(session, msg, e)
|
355
|
+
displayed_msg_id = msg["displayed"]["id"]
|
356
|
+
if not isinstance(e, LegacyMUC) and self.xmpp.MARK_ALL_MESSAGES:
|
357
|
+
to_mark = e.get_msg_xmpp_id_up_to(displayed_msg_id) # type: ignore
|
358
|
+
if to_mark is None:
|
359
|
+
session.log.debug("Can't mark all messages up to %s", displayed_msg_id)
|
360
|
+
to_mark = [displayed_msg_id]
|
361
|
+
else:
|
362
|
+
to_mark = [displayed_msg_id]
|
363
|
+
for xmpp_id in to_mark:
|
364
|
+
await session.on_displayed(
|
365
|
+
e, _xmpp_msg_id_to_legacy(session, xmpp_id), legacy_thread
|
366
|
+
)
|
367
|
+
if isinstance(e, LegacyMUC):
|
368
|
+
await e.echo(msg, None)
|
369
|
+
|
370
|
+
async def on_chatstate_active(self, msg: Message):
|
371
|
+
if msg["body"]:
|
372
|
+
# if there is a body, it's handled in self.on_legacy_message()
|
373
|
+
return
|
374
|
+
session, entity, thread = await self.__get_session_entity_thread(msg)
|
375
|
+
await session.on_active(entity, thread)
|
376
|
+
|
377
|
+
async def on_chatstate_inactive(self, msg: Message):
|
378
|
+
session, entity, thread = await self.__get_session_entity_thread(msg)
|
379
|
+
await session.on_inactive(entity, thread)
|
380
|
+
|
381
|
+
async def on_chatstate_composing(self, msg: Message):
|
382
|
+
session, entity, thread = await self.__get_session_entity_thread(msg)
|
383
|
+
await session.on_composing(entity, thread)
|
384
|
+
|
385
|
+
async def on_chatstate_paused(self, msg: Message):
|
386
|
+
session, entity, thread = await self.__get_session_entity_thread(msg)
|
387
|
+
await session.on_paused(entity, thread)
|
388
|
+
|
389
|
+
async def on_reactions(self, msg: Message):
|
390
|
+
session, entity, thread = await self.__get_session_entity_thread(msg)
|
391
|
+
react_to: str = msg["reactions"]["id"]
|
392
|
+
|
393
|
+
special_msg = session.SPECIAL_MSG_ID_PREFIX and react_to.startswith(
|
394
|
+
session.SPECIAL_MSG_ID_PREFIX
|
395
|
+
)
|
396
|
+
|
397
|
+
if special_msg:
|
398
|
+
legacy_id = react_to
|
399
|
+
else:
|
400
|
+
legacy_id = _xmpp_msg_id_to_legacy(session, react_to)
|
401
|
+
|
402
|
+
if not legacy_id:
|
403
|
+
log.debug("Ignored reaction from user")
|
404
|
+
raise XMPPError(
|
405
|
+
"internal-server-error",
|
406
|
+
"Could not convert the XMPP msg ID to a legacy ID",
|
407
|
+
)
|
408
|
+
|
409
|
+
emojis = [
|
410
|
+
remove_emoji_variation_selector_16(r["value"]) for r in msg["reactions"]
|
411
|
+
]
|
412
|
+
error_msg = None
|
413
|
+
entity = entity
|
414
|
+
|
415
|
+
if not special_msg:
|
416
|
+
if entity.REACTIONS_SINGLE_EMOJI and len(emojis) > 1:
|
417
|
+
error_msg = "Maximum 1 emoji/message"
|
418
|
+
|
419
|
+
if not error_msg and (subset := await entity.available_emojis(legacy_id)):
|
420
|
+
if not set(emojis).issubset(subset):
|
421
|
+
error_msg = f"You can only react with the following emojis: {''.join(subset)}"
|
422
|
+
|
423
|
+
if error_msg:
|
424
|
+
session.send_gateway_message(error_msg)
|
425
|
+
if not isinstance(entity, LegacyMUC):
|
426
|
+
# no need to carbon for groups, we just don't echo the stanza
|
427
|
+
entity.react(legacy_id, carbon=True) # type: ignore
|
428
|
+
await session.on_react(entity, legacy_id, [], thread=thread)
|
429
|
+
raise XMPPError("not-acceptable", text=error_msg)
|
430
|
+
|
431
|
+
await session.on_react(entity, legacy_id, emojis, thread=thread)
|
432
|
+
if isinstance(entity, LegacyMUC):
|
433
|
+
await entity.echo(msg, None)
|
434
|
+
else:
|
435
|
+
self.__ack(msg)
|
436
|
+
|
437
|
+
multi = db.attachment_get_associated_xmpp_ids(react_to)
|
438
|
+
if not multi:
|
439
|
+
return
|
440
|
+
|
441
|
+
if isinstance(entity, LegacyMUC):
|
442
|
+
for xmpp_id in multi:
|
443
|
+
mc = copy(msg)
|
444
|
+
mc["reactions"]["id"] = xmpp_id
|
445
|
+
await entity.echo(mc)
|
446
|
+
elif isinstance(entity, LegacyContact):
|
447
|
+
for xmpp_id in multi:
|
448
|
+
entity.react(legacy_id, emojis, xmpp_id=xmpp_id, carbon=True)
|
449
|
+
|
450
|
+
async def on_presence(self, p: Presence):
|
451
|
+
session = await self.__get_session(p)
|
452
|
+
|
453
|
+
if p.get_to() != self.xmpp.boundjid.bare:
|
454
|
+
return
|
455
|
+
# NB: get_type() returns either a proper presence type or
|
456
|
+
# a presence show if available. Weird, weird, weird slix.
|
457
|
+
if (ptype := p.get_type()) not in _USEFUL_PRESENCES:
|
458
|
+
return
|
459
|
+
resources = self.xmpp.roster[self.xmpp.boundjid.bare][p.get_from()].resources
|
460
|
+
session.log.debug("Received a presence from %s", p.get_from())
|
461
|
+
await session.on_presence(
|
462
|
+
p.get_from().resource,
|
463
|
+
ptype, # type: ignore
|
464
|
+
p["status"],
|
465
|
+
resources,
|
466
|
+
merge_resources(resources),
|
467
|
+
)
|
468
|
+
|
469
|
+
async def on_groupchat_join(self, p: Presence):
|
470
|
+
if not self.xmpp.GROUPS:
|
471
|
+
raise XMPPError(
|
472
|
+
"feature-not-implemented",
|
473
|
+
"This gateway does not implement multi-user chats.",
|
474
|
+
)
|
475
|
+
session = await self.__get_session(p)
|
476
|
+
session.raise_if_not_logged()
|
477
|
+
muc = await session.bookmarks.by_jid(p.get_to())
|
478
|
+
await muc.join(p)
|
479
|
+
|
480
|
+
async def on_message_displayed_synchronization_publish(self, msg: Message):
|
481
|
+
session = await self.__get_session(msg, timeout=None)
|
482
|
+
|
483
|
+
chat_jid = msg["pubsub_event"]["items"]["item"]["id"]
|
484
|
+
chat = await session.get_contact_or_group_or_participant(JID(chat_jid))
|
485
|
+
if not isinstance(chat, LegacyMUC):
|
486
|
+
session.log.debug("Ignoring non-groupchat MDS event")
|
487
|
+
return
|
488
|
+
|
489
|
+
stanza_id = msg["pubsub_event"]["items"]["item"]["displayed"]["stanza_id"]["id"]
|
490
|
+
await session.on_displayed(chat, _xmpp_msg_id_to_legacy(session, stanza_id))
|
491
|
+
|
492
|
+
async def on_avatar_metadata_publish(self, m: Message):
|
493
|
+
if not config.SYNC_AVATAR:
|
494
|
+
return
|
495
|
+
|
496
|
+
session = await self.__get_session(m, timeout=None)
|
497
|
+
info = m["pubsub_event"]["items"]["item"]["avatar_metadata"]["info"]
|
498
|
+
|
499
|
+
await self.on_avatar_metadata_info(session, info)
|
500
|
+
|
501
|
+
async def on_avatar_metadata_info(self, session: BaseSession, info: Info):
|
502
|
+
session.log.debug("Avatar metadata info: %s", info)
|
503
|
+
hash_ = info["id"]
|
504
|
+
|
505
|
+
if session.avatar_hash == hash_:
|
506
|
+
return
|
507
|
+
session.avatar_hash = hash_
|
508
|
+
|
509
|
+
if hash_:
|
510
|
+
iq = await self.xmpp.plugin["xep_0084"].retrieve_avatar(
|
511
|
+
session.user.jid, hash_, ifrom=self.xmpp.boundjid.bare
|
512
|
+
)
|
513
|
+
bytes_ = iq["pubsub"]["items"]["item"]["avatar_data"]["value"]
|
514
|
+
type_ = info["type"]
|
515
|
+
height = info["height"]
|
516
|
+
width = info["width"]
|
517
|
+
else:
|
518
|
+
bytes_ = type_ = height = width = hash_ = None
|
519
|
+
try:
|
520
|
+
await session.on_avatar(bytes_, hash_, type_, width, height)
|
521
|
+
except NotImplementedError:
|
522
|
+
pass
|
523
|
+
except Exception as e:
|
524
|
+
# If something goes wrong here, replying an error stanza will to the
|
525
|
+
# avatar update will likely not show in most clients, so let's send
|
526
|
+
# a normal message from the component to the user.
|
527
|
+
session.send_gateway_message(
|
528
|
+
f"Something went wrong trying to set your avatar: {e!r}"
|
529
|
+
)
|
530
|
+
|
531
|
+
async def on_user_moderation(self, iq: Iq):
|
532
|
+
session = await self.__get_session(iq)
|
533
|
+
session.raise_if_not_logged()
|
534
|
+
|
535
|
+
muc = await session.bookmarks.by_jid(iq.get_to())
|
536
|
+
|
537
|
+
apply_to = iq["apply_to"]
|
538
|
+
xmpp_id = apply_to["id"]
|
539
|
+
if not xmpp_id:
|
540
|
+
raise XMPPError("bad-request", "Missing moderated message ID")
|
541
|
+
|
542
|
+
moderate = apply_to["moderate"]
|
543
|
+
if not moderate["retract"]:
|
544
|
+
raise XMPPError(
|
545
|
+
"feature-not-implemented",
|
546
|
+
"Slidge only implements moderation/retraction",
|
547
|
+
)
|
548
|
+
|
549
|
+
legacy_id = _xmpp_msg_id_to_legacy(session, xmpp_id)
|
550
|
+
await session.on_moderate(muc, legacy_id, moderate["reason"] or None)
|
551
|
+
iq.reply(clear=True).send()
|
552
|
+
|
553
|
+
async def on_user_set_affiliation(self, iq: Iq):
|
554
|
+
session = await self.__get_session(iq)
|
555
|
+
session.raise_if_not_logged()
|
556
|
+
|
557
|
+
item = iq["mucadmin_query"]["item"]
|
558
|
+
contact = await session.contacts.by_jid(JID(item["jid"]))
|
559
|
+
|
560
|
+
muc = await session.bookmarks.by_jid(iq.get_to())
|
561
|
+
|
562
|
+
await muc.on_set_affiliation(
|
563
|
+
contact, item["affiliation"], item["reason"] or None, item["nick"] or None
|
564
|
+
)
|
565
|
+
iq.reply(clear=True).send()
|
566
|
+
|
567
|
+
async def on_groupchat_direct_invite(self, msg: Message):
|
568
|
+
session = await self.__get_session(msg)
|
569
|
+
session.raise_if_not_logged()
|
570
|
+
|
571
|
+
invite = msg["groupchat_invite"]
|
572
|
+
jid = JID(invite["jid"])
|
573
|
+
|
574
|
+
if jid.domain != self.xmpp.boundjid.bare:
|
575
|
+
raise XMPPError(
|
576
|
+
"bad-request",
|
577
|
+
"Legacy contacts can only be invited to legacy groups, not standard XMPP MUCs.",
|
578
|
+
)
|
579
|
+
|
580
|
+
if invite["password"]:
|
581
|
+
raise XMPPError(
|
582
|
+
"bad-request", "Password-protected groups are not supported"
|
583
|
+
)
|
584
|
+
|
585
|
+
contact = await session.contacts.by_jid(msg.get_to())
|
586
|
+
muc = await session.bookmarks.by_jid(jid)
|
587
|
+
|
588
|
+
await session.on_invitation(contact, muc, invite["reason"] or None)
|
589
|
+
|
590
|
+
async def on_muc_owner_query(self, iq: Iq):
|
591
|
+
session = await self.__get_session(iq)
|
592
|
+
session.raise_if_not_logged()
|
593
|
+
|
594
|
+
muc = await session.bookmarks.by_jid(iq.get_to())
|
595
|
+
|
596
|
+
reply = iq.reply()
|
597
|
+
|
598
|
+
form = Form(title="Slidge room configuration")
|
599
|
+
form["instructions"] = (
|
600
|
+
"Complete this form to modify the configuration of your room."
|
601
|
+
)
|
602
|
+
form.add_field(
|
603
|
+
var="FORM_TYPE",
|
604
|
+
type="hidden",
|
605
|
+
value="http://jabber.org/protocol/muc#roomconfig",
|
606
|
+
)
|
607
|
+
form.add_field(
|
608
|
+
var="muc#roomconfig_roomname",
|
609
|
+
label="Natural-Language Room Name",
|
610
|
+
type="text-single",
|
611
|
+
value=muc.name,
|
612
|
+
)
|
613
|
+
if muc.HAS_DESCRIPTION:
|
614
|
+
form.add_field(
|
615
|
+
var="muc#roomconfig_roomdesc",
|
616
|
+
label="Short Description of Room",
|
617
|
+
type="text-single",
|
618
|
+
value=muc.description,
|
619
|
+
)
|
620
|
+
|
621
|
+
muc_owner = iq["mucowner_query"]
|
622
|
+
muc_owner.append(form)
|
623
|
+
reply.append(muc_owner)
|
624
|
+
reply.send()
|
625
|
+
|
626
|
+
async def on_muc_owner_set(self, iq: Iq):
|
627
|
+
session = await self.__get_session(iq)
|
628
|
+
session.raise_if_not_logged()
|
629
|
+
muc = await session.bookmarks.by_jid(iq.get_to())
|
630
|
+
query = iq["mucowner_query"]
|
631
|
+
|
632
|
+
if form := query.get_plugin("form", check=True):
|
633
|
+
values = form.get_values()
|
634
|
+
await muc.on_set_config(
|
635
|
+
name=values.get("muc#roomconfig_roomname"),
|
636
|
+
description=(
|
637
|
+
values.get("muc#roomconfig_roomdesc")
|
638
|
+
if muc.HAS_DESCRIPTION
|
639
|
+
else None
|
640
|
+
),
|
641
|
+
)
|
642
|
+
form["type"] = "result"
|
643
|
+
clear = False
|
644
|
+
elif destroy := query.get_plugin("destroy", check=True):
|
645
|
+
reason = destroy["reason"] or None
|
646
|
+
await muc.on_destroy_request(reason)
|
647
|
+
user_participant = await muc.get_user_participant()
|
648
|
+
user_participant._affiliation = "none"
|
649
|
+
user_participant._role = "none"
|
650
|
+
presence = user_participant._make_presence(ptype="unavailable", force=True)
|
651
|
+
presence["muc"].enable("destroy")
|
652
|
+
if reason is not None:
|
653
|
+
presence["muc"]["destroy"]["reason"] = reason
|
654
|
+
user_participant._send(presence)
|
655
|
+
session.bookmarks.remove(muc)
|
656
|
+
clear = True
|
657
|
+
else:
|
658
|
+
raise XMPPError("bad-request")
|
659
|
+
|
660
|
+
iq.reply(clear=clear).send()
|
661
|
+
|
662
|
+
async def on_groupchat_subject(self, msg: Message):
|
663
|
+
session = await self.__get_session(msg)
|
664
|
+
session.raise_if_not_logged()
|
665
|
+
muc = await session.bookmarks.by_jid(msg.get_to())
|
666
|
+
if not muc.HAS_SUBJECT:
|
667
|
+
raise XMPPError(
|
668
|
+
"bad-request",
|
669
|
+
"There are no room subject in here. "
|
670
|
+
"Use the room configuration to update its name or description",
|
671
|
+
)
|
672
|
+
await muc.on_set_subject(msg["subject"])
|
673
|
+
|
674
|
+
|
675
|
+
def _xmpp_msg_id_to_legacy(session: "BaseSession", xmpp_id: str):
|
676
|
+
sent = session.sent.inverse.get(xmpp_id)
|
677
|
+
if sent:
|
678
|
+
return sent
|
679
|
+
|
680
|
+
multi = db.attachment_get_legacy_id_for_xmpp_id(xmpp_id)
|
681
|
+
if multi:
|
682
|
+
return multi
|
683
|
+
|
684
|
+
try:
|
685
|
+
return session.xmpp_to_legacy_msg_id(xmpp_id)
|
686
|
+
except XMPPError:
|
687
|
+
raise
|
688
|
+
except Exception as e:
|
689
|
+
log.debug("Couldn't convert xmpp msg ID to legacy ID.", exc_info=e)
|
690
|
+
raise XMPPError(
|
691
|
+
"internal-server-error", "Couldn't convert xmpp msg ID to legacy ID."
|
692
|
+
)
|
693
|
+
|
694
|
+
|
695
|
+
def _ignore(session: "BaseSession", msg: Message):
|
696
|
+
if (i := msg.get_id()) not in session.ignore_messages:
|
697
|
+
return False
|
698
|
+
session.log.debug("Ignored sent carbon: %s", i)
|
699
|
+
session.ignore_messages.remove(i)
|
700
|
+
return True
|
701
|
+
|
702
|
+
|
703
|
+
async def _xmpp_to_legacy_thread(
|
704
|
+
session: "BaseSession", msg: Message, recipient: RecipientType
|
705
|
+
):
|
706
|
+
xmpp_thread = msg["thread"]
|
707
|
+
if not xmpp_thread:
|
708
|
+
return
|
709
|
+
|
710
|
+
if session.MESSAGE_IDS_ARE_THREAD_IDS:
|
711
|
+
return session.threads.get(xmpp_thread)
|
712
|
+
|
713
|
+
async with session.thread_creation_lock:
|
714
|
+
legacy_thread = session.threads.get(xmpp_thread)
|
715
|
+
if legacy_thread is None:
|
716
|
+
legacy_thread = await recipient.create_thread(xmpp_thread)
|
717
|
+
session.threads[xmpp_thread] = legacy_thread
|
718
|
+
return legacy_thread
|
719
|
+
|
720
|
+
|
721
|
+
async def _get_entity(session: "BaseSession", m: Message) -> RecipientType:
|
722
|
+
session.raise_if_not_logged()
|
723
|
+
if m.get_type() == "groupchat":
|
724
|
+
muc = await session.bookmarks.by_jid(m.get_to())
|
725
|
+
r = m.get_from().resource
|
726
|
+
if r not in muc.user_resources:
|
727
|
+
session.xmpp.loop.create_task(muc.kick_resource(r))
|
728
|
+
raise XMPPError("not-acceptable", "You are not connected to this chat")
|
729
|
+
return muc
|
730
|
+
else:
|
731
|
+
return await session.contacts.by_jid(m.get_to())
|
732
|
+
|
733
|
+
|
734
|
+
def _exceptions_to_xmpp_errors(cb: HandlerType) -> HandlerType:
|
735
|
+
async def wrapped(stanza: Union[Presence, Message]):
|
736
|
+
try:
|
737
|
+
await cb(stanza)
|
738
|
+
except Ignore:
|
739
|
+
pass
|
740
|
+
except XMPPError:
|
741
|
+
raise
|
742
|
+
except NotImplementedError:
|
743
|
+
log.debug("NotImplementedError raised in %s", cb)
|
744
|
+
raise XMPPError(
|
745
|
+
"feature-not-implemented", "Not implemented by the legacy module"
|
746
|
+
)
|
747
|
+
except Exception as e:
|
748
|
+
log.error("Failed to handle incoming stanza: %s", stanza, exc_info=e)
|
749
|
+
raise XMPPError("internal-server-error", str(e))
|
750
|
+
|
751
|
+
return wrapped
|
752
|
+
|
753
|
+
|
754
|
+
_USEFUL_PRESENCES = {"available", "unavailable", "away", "chat", "dnd", "xa"}
|
755
|
+
|
756
|
+
|
757
|
+
log = logging.getLogger(__name__)
|