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