slidge 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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__)