slidge 0.2.0a9__py3-none-any.whl → 0.2.0a10__py3-none-any.whl

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