slidge 0.2.0a8__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 (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__)