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.
Files changed (96) hide show
  1. slidge/__init__.py +61 -0
  2. slidge/__main__.py +192 -0
  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 +3 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +209 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +892 -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 +757 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +525 -0
  41. slidge/core/session.py +752 -0
  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 +440 -0
  46. slidge/group/room.py +1095 -0
  47. slidge/migration.py +18 -0
  48. slidge/py.typed +0 -0
  49. slidge/slixfix/__init__.py +68 -0
  50. slidge/slixfix/link_preview/__init__.py +10 -0
  51. slidge/slixfix/link_preview/link_preview.py +17 -0
  52. slidge/slixfix/link_preview/stanza.py +99 -0
  53. slidge/slixfix/roster.py +60 -0
  54. slidge/slixfix/xep_0077/__init__.py +10 -0
  55. slidge/slixfix/xep_0077/register.py +289 -0
  56. slidge/slixfix/xep_0077/stanza.py +104 -0
  57. slidge/slixfix/xep_0100/__init__.py +5 -0
  58. slidge/slixfix/xep_0100/gateway.py +121 -0
  59. slidge/slixfix/xep_0100/stanza.py +9 -0
  60. slidge/slixfix/xep_0153/__init__.py +10 -0
  61. slidge/slixfix/xep_0153/stanza.py +25 -0
  62. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  63. slidge/slixfix/xep_0264/__init__.py +5 -0
  64. slidge/slixfix/xep_0264/stanza.py +36 -0
  65. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  66. slidge/slixfix/xep_0292/__init__.py +5 -0
  67. slidge/slixfix/xep_0292/vcard4.py +100 -0
  68. slidge/slixfix/xep_0313/__init__.py +12 -0
  69. slidge/slixfix/xep_0313/mam.py +262 -0
  70. slidge/slixfix/xep_0313/stanza.py +359 -0
  71. slidge/slixfix/xep_0317/__init__.py +5 -0
  72. slidge/slixfix/xep_0317/hats.py +17 -0
  73. slidge/slixfix/xep_0317/stanza.py +28 -0
  74. slidge/slixfix/xep_0356_old/__init__.py +7 -0
  75. slidge/slixfix/xep_0356_old/privilege.py +167 -0
  76. slidge/slixfix/xep_0356_old/stanza.py +44 -0
  77. slidge/slixfix/xep_0424/__init__.py +9 -0
  78. slidge/slixfix/xep_0424/retraction.py +77 -0
  79. slidge/slixfix/xep_0424/stanza.py +28 -0
  80. slidge/slixfix/xep_0490/__init__.py +8 -0
  81. slidge/slixfix/xep_0490/mds.py +47 -0
  82. slidge/slixfix/xep_0490/stanza.py +17 -0
  83. slidge/util/__init__.py +15 -0
  84. slidge/util/archive_msg.py +61 -0
  85. slidge/util/conf.py +206 -0
  86. slidge/util/db.py +229 -0
  87. slidge/util/schema.sql +126 -0
  88. slidge/util/sql.py +508 -0
  89. slidge/util/test.py +295 -0
  90. slidge/util/types.py +180 -0
  91. slidge/util/util.py +295 -0
  92. slidge-0.1.0.dist-info/LICENSE +661 -0
  93. slidge-0.1.0.dist-info/METADATA +109 -0
  94. slidge-0.1.0.dist-info/RECORD +96 -0
  95. slidge-0.1.0.dist-info/WHEEL +4 -0
  96. 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__)