slidge 0.2.0a8__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 (46) 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/contact.py +3 -2
  5. slidge/contact/roster.py +7 -0
  6. slidge/core/dispatcher/__init__.py +3 -0
  7. slidge/core/{gateway → dispatcher}/caps.py +6 -4
  8. slidge/core/{gateway → dispatcher}/disco.py +11 -17
  9. slidge/core/dispatcher/message/__init__.py +10 -0
  10. slidge/core/dispatcher/message/chat_state.py +40 -0
  11. slidge/core/dispatcher/message/marker.py +67 -0
  12. slidge/core/dispatcher/message/message.py +397 -0
  13. slidge/core/dispatcher/muc/__init__.py +12 -0
  14. slidge/core/dispatcher/muc/admin.py +98 -0
  15. slidge/core/{gateway → dispatcher/muc}/mam.py +26 -15
  16. slidge/core/dispatcher/muc/misc.py +118 -0
  17. slidge/core/dispatcher/muc/owner.py +96 -0
  18. slidge/core/{gateway → dispatcher/muc}/ping.py +10 -15
  19. slidge/core/dispatcher/presence.py +177 -0
  20. slidge/core/{gateway → dispatcher}/registration.py +23 -2
  21. slidge/core/{gateway → dispatcher}/search.py +9 -14
  22. slidge/core/dispatcher/session_dispatcher.py +84 -0
  23. slidge/core/dispatcher/util.py +174 -0
  24. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +26 -12
  25. slidge/core/{gateway/base.py → gateway.py} +42 -137
  26. slidge/core/mixins/attachment.py +7 -2
  27. slidge/core/mixins/base.py +2 -2
  28. slidge/core/mixins/message.py +10 -4
  29. slidge/core/pubsub.py +2 -1
  30. slidge/core/session.py +28 -2
  31. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  32. slidge/db/models.py +13 -0
  33. slidge/db/store.py +128 -2
  34. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  35. slidge/util/test.py +5 -1
  36. slidge/util/types.py +6 -0
  37. slidge/util/util.py +5 -2
  38. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/METADATA +2 -1
  39. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/RECORD +42 -33
  40. slidge/core/gateway/__init__.py +0 -3
  41. slidge/core/gateway/muc_admin.py +0 -35
  42. slidge/core/gateway/presence.py +0 -95
  43. slidge/core/gateway/session_dispatcher.py +0 -895
  44. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/LICENSE +0 -0
  45. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/WHEEL +0 -0
  46. {slidge-0.2.0a8.dist-info → slidge-0.2.0a10.dist-info}/entry_points.txt +0 -0
slidge/__version__.py CHANGED
@@ -2,4 +2,4 @@ from slidge.util.util import get_version # noqa: F401
2
2
 
3
3
  # this is modified before publish, but if someone cloned from the repo,
4
4
  # it can help
5
- __version__ = "0.2.0alpha8"
5
+ __version__ = "0.2.0alpha10"
slidge/command/adhoc.py CHANGED
@@ -13,7 +13,7 @@ from . import Command, CommandResponseType, Confirmation, Form, TableResult
13
13
  from .base import FormField
14
14
 
15
15
  if TYPE_CHECKING:
16
- from ..core.gateway.base import BaseGateway
16
+ from ..core.gateway import BaseGateway
17
17
  from ..core.session import BaseSession
18
18
 
19
19
 
slidge/command/base.py CHANGED
@@ -6,9 +6,9 @@ from typing import (
6
6
  Any,
7
7
  Awaitable,
8
8
  Callable,
9
- Collection,
10
9
  Iterable,
11
10
  Optional,
11
+ Sequence,
12
12
  Type,
13
13
  TypedDict,
14
14
  Union,
@@ -54,11 +54,11 @@ class TableResult:
54
54
  Structured data as the result of a command
55
55
  """
56
56
 
57
- fields: Collection["FormField"]
57
+ fields: Sequence["FormField"]
58
58
  """
59
59
  The 'columns names' of the table.
60
60
  """
61
- items: Collection[dict[str, Union[str, JID]]]
61
+ items: Sequence[dict[str, Union[str, JID]]]
62
62
  """
63
63
  The rows of the table. Each row is a dict where keys are the fields ``var``
64
64
  attribute.
@@ -149,7 +149,7 @@ class Form:
149
149
 
150
150
  title: str
151
151
  instructions: str
152
- fields: Collection["FormField"]
152
+ fields: Sequence["FormField"]
153
153
  handler: FormHandlerType
154
154
  handler_args: Iterable[Any] = field(default_factory=list)
155
155
  handler_kwargs: dict[str, Any] = field(default_factory=dict)
slidge/contact/contact.py CHANGED
@@ -282,8 +282,9 @@ class LegacyContact(
282
282
  self._privileged_send(stanza)
283
283
  return stanza # type:ignore
284
284
 
285
- if not self._updating_info and isinstance(stanza, Presence):
286
- self.__propagate_to_participants(stanza)
285
+ if isinstance(stanza, Presence):
286
+ if not self._updating_info:
287
+ self.__propagate_to_participants(stanza)
287
288
  if (
288
289
  not self.is_friend
289
290
  and stanza["type"] not in self._NON_FRIEND_PRESENCES_FILTER
slidge/contact/roster.py CHANGED
@@ -92,6 +92,13 @@ class LegacyRoster(
92
92
  stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
93
93
  return await self.__update_contact(stored, legacy_id, username)
94
94
 
95
+ def by_jid_only_if_exists(self, contact_jid: JID) -> LegacyContactType | None:
96
+ with self.__store.session():
97
+ stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
98
+ if stored is not None and stored.updated:
99
+ return self._contact_cls.from_store(self.session, stored)
100
+ return None
101
+
95
102
  async def by_legacy_id(
96
103
  self, legacy_id: LegacyUserIdType, *args, **kwargs
97
104
  ) -> LegacyContactType:
@@ -0,0 +1,3 @@
1
+ from .session_dispatcher import SessionDispatcher
2
+
3
+ __all__ = ("SessionDispatcher",)
@@ -5,17 +5,19 @@ from slixmpp import Presence
5
5
  from slixmpp.exceptions import XMPPError
6
6
  from slixmpp.xmlstream import StanzaBase
7
7
 
8
+ from .util import DispatcherMixin
9
+
8
10
  if TYPE_CHECKING:
9
- from .base import BaseGateway
11
+ from slidge.core.gateway import BaseGateway
10
12
 
11
13
 
12
- class Caps:
14
+ class CapsMixin(DispatcherMixin):
13
15
  def __init__(self, xmpp: "BaseGateway"):
14
- self.xmpp = xmpp
16
+ super().__init__(xmpp)
15
17
  xmpp.del_filter("out", xmpp.plugin["xep_0115"]._filter_add_caps)
16
18
  xmpp.add_filter("out", self._filter_add_caps) # type:ignore
17
19
 
18
- async def _filter_add_caps(self, stanza: StanzaBase):
20
+ async def _filter_add_caps(self, stanza: StanzaBase) -> StanzaBase:
19
21
  # we rolled our own "add caps on presences" filter because
20
22
  # there is too much magic happening in slixmpp
21
23
  # anyway, we probably want to roll our own "dynamic disco"/caps
@@ -5,13 +5,15 @@ from slixmpp.exceptions import XMPPError
5
5
  from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
6
6
  from slixmpp.types import OptJid
7
7
 
8
+ from .util import DispatcherMixin
9
+
8
10
  if TYPE_CHECKING:
9
- from .base import BaseGateway
11
+ from slidge.core.gateway import BaseGateway
10
12
 
11
13
 
12
- class Disco:
14
+ class DiscoMixin(DispatcherMixin):
13
15
  def __init__(self, xmpp: "BaseGateway"):
14
- self.xmpp = xmpp
16
+ super().__init__(xmpp)
15
17
 
16
18
  xmpp.plugin["xep_0030"].set_node_handler(
17
19
  "get_info",
@@ -36,15 +38,11 @@ class Disco:
36
38
  if ifrom is None:
37
39
  raise XMPPError("subscription-required")
38
40
 
39
- user = self.xmpp.store.users.get(ifrom)
40
- if user is None:
41
- raise XMPPError("registration-required")
42
- session = self.xmpp.get_session_from_user(user)
43
- await session.wait_for_ready()
41
+ assert jid is not None
42
+ session = await self._get_session_from_jid(jid=ifrom)
44
43
 
45
44
  log.debug("Looking for entity: %s", jid)
46
45
 
47
- assert jid is not None
48
46
  entity = await session.get_contact_or_group_or_participant(jid)
49
47
 
50
48
  if entity is None:
@@ -61,16 +59,12 @@ class Disco:
61
59
  if jid != self.xmpp.boundjid.bare:
62
60
  return DiscoItems()
63
61
 
64
- user = self.xmpp.store.users.get(ifrom)
65
- if user is None:
66
- raise XMPPError("registration-required")
67
-
68
- session = self.xmpp.get_session_from_user(user)
69
- await session.wait_for_ready()
62
+ assert ifrom is not None
63
+ session = await self._get_session_from_jid(ifrom)
70
64
 
71
65
  d = DiscoItems()
72
- for muc in sorted(session.bookmarks, key=lambda m: m.name):
73
- d.add_item(muc.jid, name=muc.name)
66
+ for room in self.xmpp.store.rooms.get_all_jid_and_names(session.user_pk):
67
+ d.add_item(room.jid, name=room.name)
74
68
 
75
69
  return d
76
70
 
@@ -0,0 +1,10 @@
1
+ from .chat_state import ChatStateMixin
2
+ from .marker import MarkerMixin
3
+ from .message import MessageContentMixin
4
+
5
+
6
+ class MessageMixin(ChatStateMixin, MarkerMixin, MessageContentMixin):
7
+ pass
8
+
9
+
10
+ __all__ = ("MessageMixin",)
@@ -0,0 +1,40 @@
1
+ from slixmpp import Message
2
+ from slixmpp.xmlstream import StanzaBase
3
+
4
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
5
+
6
+
7
+ class ChatStateMixin(DispatcherMixin):
8
+ def __init__(self, xmpp) -> None:
9
+ super().__init__(xmpp)
10
+ xmpp.add_event_handler("chatstate_active", self.on_chatstate_active)
11
+ xmpp.add_event_handler("chatstate_inactive", self.on_chatstate_inactive)
12
+ xmpp.add_event_handler("chatstate_composing", self.on_chatstate_composing)
13
+ xmpp.add_event_handler("chatstate_paused", self.on_chatstate_paused)
14
+
15
+ @exceptions_to_xmpp_errors
16
+ async def on_chatstate_active(self, msg: StanzaBase) -> None:
17
+ assert isinstance(msg, Message)
18
+ if msg["body"]:
19
+ # if there is a body, it's handled in on_legacy_message()
20
+ return
21
+ session, entity, thread = await self._get_session_entity_thread(msg)
22
+ await session.on_active(entity, thread)
23
+
24
+ @exceptions_to_xmpp_errors
25
+ async def on_chatstate_inactive(self, msg: StanzaBase) -> None:
26
+ assert isinstance(msg, Message)
27
+ session, entity, thread = await self._get_session_entity_thread(msg)
28
+ await session.on_inactive(entity, thread)
29
+
30
+ @exceptions_to_xmpp_errors
31
+ async def on_chatstate_composing(self, msg: StanzaBase) -> None:
32
+ assert isinstance(msg, Message)
33
+ session, entity, thread = await self._get_session_entity_thread(msg)
34
+ await session.on_composing(entity, thread)
35
+
36
+ @exceptions_to_xmpp_errors
37
+ async def on_chatstate_paused(self, msg: StanzaBase) -> None:
38
+ assert isinstance(msg, Message)
39
+ session, entity, thread = await self._get_session_entity_thread(msg)
40
+ await session.on_paused(entity, thread)
@@ -0,0 +1,67 @@
1
+ from slixmpp import JID, Message
2
+ from slixmpp.xmlstream import StanzaBase
3
+
4
+ from ....group.room import LegacyMUC
5
+ from ....util.types import Recipient
6
+ from ..util import (
7
+ DispatcherMixin,
8
+ _get_entity,
9
+ _xmpp_to_legacy_thread,
10
+ exceptions_to_xmpp_errors,
11
+ )
12
+
13
+
14
+ class MarkerMixin(DispatcherMixin):
15
+ def __init__(self, xmpp) -> None:
16
+ super().__init__(xmpp)
17
+ xmpp.add_event_handler("marker_displayed", self.on_marker_displayed)
18
+ xmpp.add_event_handler(
19
+ "message_displayed_synchronization_publish",
20
+ self.on_message_displayed_synchronization_publish,
21
+ )
22
+
23
+ @exceptions_to_xmpp_errors
24
+ async def on_marker_displayed(self, msg: StanzaBase) -> None:
25
+ assert isinstance(msg, Message)
26
+ session = await self._get_session(msg)
27
+
28
+ e: Recipient = await _get_entity(session, msg)
29
+ legacy_thread = await _xmpp_to_legacy_thread(session, msg, e)
30
+ displayed_msg_id = msg["displayed"]["id"]
31
+ if not isinstance(e, LegacyMUC) and self.xmpp.MARK_ALL_MESSAGES:
32
+ to_mark = e.get_msg_xmpp_id_up_to(displayed_msg_id) # type: ignore
33
+ if to_mark is None:
34
+ session.log.debug("Can't mark all messages up to %s", displayed_msg_id)
35
+ to_mark = [displayed_msg_id]
36
+ else:
37
+ to_mark = [displayed_msg_id]
38
+ for xmpp_id in to_mark:
39
+ await session.on_displayed(
40
+ e, self._xmpp_msg_id_to_legacy(session, xmpp_id), legacy_thread
41
+ )
42
+ if isinstance(e, LegacyMUC):
43
+ await e.echo(msg, None)
44
+
45
+ @exceptions_to_xmpp_errors
46
+ async def on_message_displayed_synchronization_publish(
47
+ self, msg: StanzaBase
48
+ ) -> None:
49
+ assert isinstance(msg, Message)
50
+ chat_jid = JID(msg["pubsub_event"]["items"]["item"]["id"])
51
+ if chat_jid.server != self.xmpp.boundjid.bare:
52
+ return
53
+
54
+ session = await self._get_session(msg, timeout=None)
55
+
56
+ if chat_jid == self.xmpp.boundjid.bare:
57
+ return
58
+
59
+ chat = await session.get_contact_or_group_or_participant(chat_jid)
60
+ if not isinstance(chat, LegacyMUC):
61
+ session.log.debug("Ignoring non-groupchat MDS event")
62
+ return
63
+
64
+ stanza_id = msg["pubsub_event"]["items"]["item"]["displayed"]["stanza_id"]["id"]
65
+ await session.on_displayed(
66
+ chat, self._xmpp_msg_id_to_legacy(session, stanza_id)
67
+ )
@@ -0,0 +1,397 @@
1
+ import logging
2
+ from copy import copy
3
+ from xml.etree import ElementTree
4
+
5
+ from slixmpp import JID, Message
6
+ from slixmpp.exceptions import XMPPError
7
+
8
+ from ....contact.contact import LegacyContact
9
+ from ....group.participant import LegacyParticipant
10
+ from ....group.room import LegacyMUC
11
+ from ....util.types import LinkPreview, Recipient
12
+ from ....util.util import dict_to_named_tuple, remove_emoji_variation_selector_16
13
+ from ... import config
14
+ from ...session import BaseSession
15
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
16
+
17
+
18
+ class MessageContentMixin(DispatcherMixin):
19
+ def __init__(self, xmpp):
20
+ super().__init__(xmpp)
21
+ xmpp.add_event_handler("legacy_message", self.on_legacy_message)
22
+ xmpp.add_event_handler("message_correction", self.on_message_correction)
23
+ xmpp.add_event_handler("message_retract", self.on_message_retract)
24
+ xmpp.add_event_handler("groupchat_message", self.on_groupchat_message)
25
+ xmpp.add_event_handler("reactions", self.on_reactions)
26
+
27
+ async def on_groupchat_message(self, msg: Message) -> None:
28
+ await self.on_legacy_message(msg)
29
+
30
+ @exceptions_to_xmpp_errors
31
+ async def on_legacy_message(self, msg: Message):
32
+ """
33
+ Meant to be called from :class:`BaseGateway` only.
34
+
35
+ :param msg:
36
+ :return:
37
+ """
38
+ # we MUST not use `if m["replace"]["id"]` because it adds the tag if not
39
+ # present. this is a problem for MUC echoed messages
40
+ if msg.get_plugin("replace", check=True) is not None:
41
+ # ignore last message correction (handled by a specific method)
42
+ return
43
+ if msg.get_plugin("apply_to", check=True) is not None:
44
+ # ignore message retraction (handled by a specific method)
45
+ return
46
+ if msg.get_plugin("reactions", check=True) is not None:
47
+ # ignore message reaction fallback.
48
+ # the reaction itself is handled by self.react_from_msg().
49
+ return
50
+ if msg.get_plugin("retract", check=True) is not None:
51
+ # ignore message retraction fallback.
52
+ # the retraction itself is handled by self.on_retract
53
+ return
54
+ cid = None
55
+ if msg.get_plugin("html", check=True) is not None:
56
+ body = ElementTree.fromstring("<body>" + msg["html"].get_body() + "</body>")
57
+ p = body.findall("p")
58
+ if p is not None and len(p) == 1:
59
+ if p[0].text is None or not p[0].text.strip():
60
+ images = p[0].findall("img")
61
+ if len(images) == 1:
62
+ # no text, single img ⇒ this is a sticker
63
+ # other cases should be interpreted as "custom emojis" in text
64
+ src = images[0].get("src")
65
+ if src is not None and src.startswith("cid:"):
66
+ cid = src.removeprefix("cid:")
67
+
68
+ session, entity, thread = await self._get_session_entity_thread(msg)
69
+
70
+ if msg.get_plugin("oob", check=True) is not None:
71
+ url = msg["oob"]["url"]
72
+ else:
73
+ url = None
74
+
75
+ if msg.get_plugin("reply", check=True):
76
+ text, reply_to_msg_id, reply_to, reply_fallback = await self.__get_reply(
77
+ msg, session, entity
78
+ )
79
+ else:
80
+ text = msg["body"]
81
+ reply_to_msg_id = None
82
+ reply_to = None
83
+ reply_fallback = None
84
+
85
+ if msg.get_plugin("link_previews", check=True):
86
+ link_previews = [
87
+ dict_to_named_tuple(p, LinkPreview) for p in msg["link_previews"]
88
+ ]
89
+ else:
90
+ link_previews = []
91
+
92
+ if url:
93
+ legacy_msg_id = await self.__send_url(
94
+ url,
95
+ session,
96
+ entity,
97
+ reply_to_msg_id=reply_to_msg_id,
98
+ reply_to_fallback_text=reply_fallback,
99
+ reply_to=reply_to,
100
+ thread=thread,
101
+ )
102
+ elif cid:
103
+ legacy_msg_id = await self.__send_bob(
104
+ msg.get_from(),
105
+ cid,
106
+ session,
107
+ entity,
108
+ reply_to_msg_id=reply_to_msg_id,
109
+ reply_to_fallback_text=reply_fallback,
110
+ reply_to=reply_to,
111
+ thread=thread,
112
+ )
113
+ elif text:
114
+ if isinstance(entity, LegacyMUC):
115
+ mentions = {"mentions": await entity.parse_mentions(text)}
116
+ else:
117
+ mentions = {}
118
+ legacy_msg_id = await session.on_text(
119
+ entity,
120
+ text,
121
+ reply_to_msg_id=reply_to_msg_id,
122
+ reply_to_fallback_text=reply_fallback,
123
+ reply_to=reply_to,
124
+ thread=thread,
125
+ link_previews=link_previews,
126
+ **mentions,
127
+ )
128
+ else:
129
+ log.debug("Ignoring %s", msg.get_id())
130
+ return
131
+
132
+ if isinstance(entity, LegacyMUC):
133
+ await entity.echo(msg, legacy_msg_id)
134
+ if legacy_msg_id is not None:
135
+ self.xmpp.store.sent.set_group_message(
136
+ session.user_pk, str(legacy_msg_id), msg.get_id()
137
+ )
138
+ else:
139
+ self.__ack(msg)
140
+ if legacy_msg_id is not None:
141
+ self.xmpp.store.sent.set_message(
142
+ session.user_pk, str(legacy_msg_id), msg.get_id()
143
+ )
144
+ if session.MESSAGE_IDS_ARE_THREAD_IDS and (t := msg["thread"]):
145
+ self.xmpp.store.sent.set_thread(
146
+ session.user_pk, t, str(legacy_msg_id)
147
+ )
148
+
149
+ @exceptions_to_xmpp_errors
150
+ async def on_message_correction(self, msg: Message):
151
+ if msg.get_plugin("retract", check=True) is not None:
152
+ # ignore message retraction fallback (fallback=last msg correction)
153
+ return
154
+ session, entity, thread = await self._get_session_entity_thread(msg)
155
+ xmpp_id = msg["replace"]["id"]
156
+ if isinstance(entity, LegacyMUC):
157
+ legacy_id_str = self.xmpp.store.sent.get_group_legacy_id(
158
+ session.user_pk, xmpp_id
159
+ )
160
+ if legacy_id_str is None:
161
+ legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
162
+ else:
163
+ legacy_id = self.xmpp.LEGACY_MSG_ID_TYPE(legacy_id_str)
164
+ else:
165
+ legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
166
+
167
+ if isinstance(entity, LegacyMUC):
168
+ mentions = await entity.parse_mentions(msg["body"])
169
+ else:
170
+ mentions = None
171
+
172
+ if previews := msg["link_previews"]:
173
+ link_previews = [dict_to_named_tuple(p, LinkPreview) for p in previews]
174
+ else:
175
+ link_previews = []
176
+
177
+ if legacy_id is None:
178
+ log.debug("Did not find legacy ID to correct")
179
+ new_legacy_msg_id = await session.on_text(
180
+ entity,
181
+ "Correction:" + msg["body"],
182
+ thread=thread,
183
+ mentions=mentions,
184
+ link_previews=link_previews,
185
+ )
186
+ elif (
187
+ not msg["body"].strip()
188
+ and config.CORRECTION_EMPTY_BODY_AS_RETRACTION
189
+ and entity.RETRACTION
190
+ ):
191
+ await session.on_retract(entity, legacy_id, thread=thread)
192
+ new_legacy_msg_id = None
193
+ elif entity.CORRECTION:
194
+ new_legacy_msg_id = await session.on_correct(
195
+ entity,
196
+ msg["body"],
197
+ legacy_id,
198
+ thread=thread,
199
+ mentions=mentions,
200
+ link_previews=link_previews,
201
+ )
202
+ else:
203
+ session.send_gateway_message(
204
+ "Last message correction is not supported by this legacy service. "
205
+ "Slidge will send your correction as new message."
206
+ )
207
+ if (
208
+ config.LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND
209
+ and entity.RETRACTION
210
+ and legacy_id is not None
211
+ ):
212
+ if legacy_id is not None:
213
+ session.send_gateway_message(
214
+ "Slidge will attempt to retract the original message you wanted"
215
+ " to edit."
216
+ )
217
+ await session.on_retract(entity, legacy_id, thread=thread)
218
+
219
+ new_legacy_msg_id = await session.on_text(
220
+ entity,
221
+ "Correction: " + msg["body"],
222
+ thread=thread,
223
+ mentions=mentions,
224
+ link_previews=link_previews,
225
+ )
226
+
227
+ if isinstance(entity, LegacyMUC):
228
+ if new_legacy_msg_id is not None:
229
+ self.xmpp.store.sent.set_group_message(
230
+ session.user_pk, new_legacy_msg_id, msg.get_id()
231
+ )
232
+ await entity.echo(msg, new_legacy_msg_id)
233
+ else:
234
+ self.__ack(msg)
235
+ if new_legacy_msg_id is not None:
236
+ self.xmpp.store.sent.set_message(
237
+ session.user_pk, new_legacy_msg_id, msg.get_id()
238
+ )
239
+
240
+ @exceptions_to_xmpp_errors
241
+ async def on_message_retract(self, msg: Message):
242
+ session, entity, thread = await self._get_session_entity_thread(msg)
243
+ if not entity.RETRACTION:
244
+ raise XMPPError(
245
+ "bad-request",
246
+ "This legacy service does not support message retraction.",
247
+ )
248
+ xmpp_id: str = msg["retract"]["id"]
249
+ legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
250
+ if legacy_id:
251
+ await session.on_retract(entity, legacy_id, thread=thread)
252
+ if isinstance(entity, LegacyMUC):
253
+ await entity.echo(msg, None)
254
+ else:
255
+ log.debug("Ignored retraction from user")
256
+ self.__ack(msg)
257
+
258
+ @exceptions_to_xmpp_errors
259
+ async def on_reactions(self, msg: Message):
260
+ session, entity, thread = await self._get_session_entity_thread(msg)
261
+ react_to: str = msg["reactions"]["id"]
262
+
263
+ special_msg = session.SPECIAL_MSG_ID_PREFIX and react_to.startswith(
264
+ session.SPECIAL_MSG_ID_PREFIX
265
+ )
266
+
267
+ if special_msg:
268
+ legacy_id = react_to
269
+ else:
270
+ legacy_id = self._xmpp_msg_id_to_legacy(session, react_to)
271
+
272
+ if not legacy_id:
273
+ log.debug("Ignored reaction from user")
274
+ raise XMPPError(
275
+ "internal-server-error",
276
+ "Could not convert the XMPP msg ID to a legacy ID",
277
+ )
278
+
279
+ emojis = [
280
+ remove_emoji_variation_selector_16(r["value"]) for r in msg["reactions"]
281
+ ]
282
+ error_msg = None
283
+ entity = entity
284
+
285
+ if not special_msg:
286
+ if entity.REACTIONS_SINGLE_EMOJI and len(emojis) > 1:
287
+ error_msg = "Maximum 1 emoji/message"
288
+
289
+ if not error_msg and (subset := await entity.available_emojis(legacy_id)):
290
+ if not set(emojis).issubset(subset):
291
+ error_msg = f"You can only react with the following emojis: {''.join(subset)}"
292
+
293
+ if error_msg:
294
+ session.send_gateway_message(error_msg)
295
+ if not isinstance(entity, LegacyMUC):
296
+ # no need to carbon for groups, we just don't echo the stanza
297
+ entity.react(legacy_id, carbon=True) # type: ignore
298
+ await session.on_react(entity, legacy_id, [], thread=thread)
299
+ raise XMPPError("not-acceptable", text=error_msg)
300
+
301
+ await session.on_react(entity, legacy_id, emojis, thread=thread)
302
+ if isinstance(entity, LegacyMUC):
303
+ await entity.echo(msg, None)
304
+ else:
305
+ self.__ack(msg)
306
+
307
+ multi = self.xmpp.store.multi.get_xmpp_ids(session.user_pk, react_to)
308
+ if not multi:
309
+ return
310
+ multi = [m for m in multi if react_to != m]
311
+
312
+ if isinstance(entity, LegacyMUC):
313
+ for xmpp_id in multi:
314
+ mc = copy(msg)
315
+ mc["reactions"]["id"] = xmpp_id
316
+ await entity.echo(mc)
317
+ elif isinstance(entity, LegacyContact):
318
+ for xmpp_id in multi:
319
+ entity.react(legacy_id, emojis, xmpp_id=xmpp_id, carbon=True)
320
+
321
+ def __ack(self, msg: Message):
322
+ if not self.xmpp.PROPER_RECEIPTS:
323
+ self.xmpp.delivery_receipt.ack(msg)
324
+
325
+ async def __get_reply(
326
+ self, msg: Message, session: BaseSession, entity: Recipient
327
+ ) -> tuple[
328
+ str, str | int | None, LegacyContact | LegacyParticipant | None, str | None
329
+ ]:
330
+ try:
331
+ reply_to_msg_id = self._xmpp_msg_id_to_legacy(session, msg["reply"]["id"])
332
+ except XMPPError:
333
+ session.log.debug(
334
+ "Could not determine reply-to legacy msg ID, sending quote instead."
335
+ )
336
+ return msg["body"], None, None, None
337
+
338
+ reply_to_jid = JID(msg["reply"]["to"])
339
+ reply_to = None
340
+ if msg["type"] == "chat":
341
+ if reply_to_jid.bare != session.user_jid.bare:
342
+ try:
343
+ reply_to = await session.contacts.by_jid(reply_to_jid)
344
+ except XMPPError:
345
+ pass
346
+ elif msg["type"] == "groupchat":
347
+ nick = reply_to_jid.resource
348
+ try:
349
+ muc = await session.bookmarks.by_jid(reply_to_jid)
350
+ except XMPPError:
351
+ pass
352
+ else:
353
+ if nick != muc.user_nick:
354
+ reply_to = await muc.get_participant(
355
+ reply_to_jid.resource, store=False
356
+ )
357
+
358
+ if msg.get_plugin("fallback", check=True) and (
359
+ isinstance(entity, LegacyMUC) or entity.REPLIES
360
+ ):
361
+ text = msg["fallback"].get_stripped_body(self.xmpp["xep_0461"].namespace)
362
+ try:
363
+ reply_fallback = msg["reply"].get_fallback_body()
364
+ except AttributeError:
365
+ reply_fallback = None
366
+ else:
367
+ text = msg["body"]
368
+ reply_fallback = None
369
+
370
+ return text, reply_to_msg_id, reply_to, reply_fallback
371
+
372
+ async def __send_url(
373
+ self, url: str, session: BaseSession, entity: Recipient, **kwargs
374
+ ) -> int | str | None:
375
+ async with self.xmpp.http.get(url) as response:
376
+ if response.status >= 400:
377
+ session.log.warning(
378
+ "OOB url cannot be downloaded: %s, sending the URL as text"
379
+ " instead.",
380
+ response,
381
+ )
382
+ return await session.on_text(entity, url, **kwargs)
383
+
384
+ return await session.on_file(entity, url, http_response=response, **kwargs)
385
+
386
+ async def __send_bob(
387
+ self, from_: JID, cid: str, session: BaseSession, entity: Recipient, **kwargs
388
+ ) -> int | str | None:
389
+ sticker = self.xmpp.store.bob.get_sticker(cid)
390
+ if sticker is None:
391
+ await self.xmpp.plugin["xep_0231"].get_bob(from_, cid)
392
+ sticker = self.xmpp.store.bob.get_sticker(cid)
393
+ assert sticker is not None
394
+ return await session.on_sticker(entity, sticker, **kwargs)
395
+
396
+
397
+ log = logging.getLogger(__name__)