slidge 0.1.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. slidge/__init__.py +61 -0
  2. slidge/__main__.py +192 -0
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +3 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +209 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +892 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +757 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +525 -0
  41. slidge/core/session.py +752 -0
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +440 -0
  46. slidge/group/room.py +1095 -0
  47. slidge/migration.py +18 -0
  48. slidge/py.typed +0 -0
  49. slidge/slixfix/__init__.py +68 -0
  50. slidge/slixfix/link_preview/__init__.py +10 -0
  51. slidge/slixfix/link_preview/link_preview.py +17 -0
  52. slidge/slixfix/link_preview/stanza.py +99 -0
  53. slidge/slixfix/roster.py +60 -0
  54. slidge/slixfix/xep_0077/__init__.py +10 -0
  55. slidge/slixfix/xep_0077/register.py +289 -0
  56. slidge/slixfix/xep_0077/stanza.py +104 -0
  57. slidge/slixfix/xep_0100/__init__.py +5 -0
  58. slidge/slixfix/xep_0100/gateway.py +121 -0
  59. slidge/slixfix/xep_0100/stanza.py +9 -0
  60. slidge/slixfix/xep_0153/__init__.py +10 -0
  61. slidge/slixfix/xep_0153/stanza.py +25 -0
  62. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  63. slidge/slixfix/xep_0264/__init__.py +5 -0
  64. slidge/slixfix/xep_0264/stanza.py +36 -0
  65. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  66. slidge/slixfix/xep_0292/__init__.py +5 -0
  67. slidge/slixfix/xep_0292/vcard4.py +100 -0
  68. slidge/slixfix/xep_0313/__init__.py +12 -0
  69. slidge/slixfix/xep_0313/mam.py +262 -0
  70. slidge/slixfix/xep_0313/stanza.py +359 -0
  71. slidge/slixfix/xep_0317/__init__.py +5 -0
  72. slidge/slixfix/xep_0317/hats.py +17 -0
  73. slidge/slixfix/xep_0317/stanza.py +28 -0
  74. slidge/slixfix/xep_0356_old/__init__.py +7 -0
  75. slidge/slixfix/xep_0356_old/privilege.py +167 -0
  76. slidge/slixfix/xep_0356_old/stanza.py +44 -0
  77. slidge/slixfix/xep_0424/__init__.py +9 -0
  78. slidge/slixfix/xep_0424/retraction.py +77 -0
  79. slidge/slixfix/xep_0424/stanza.py +28 -0
  80. slidge/slixfix/xep_0490/__init__.py +8 -0
  81. slidge/slixfix/xep_0490/mds.py +47 -0
  82. slidge/slixfix/xep_0490/stanza.py +17 -0
  83. slidge/util/__init__.py +15 -0
  84. slidge/util/archive_msg.py +61 -0
  85. slidge/util/conf.py +206 -0
  86. slidge/util/db.py +229 -0
  87. slidge/util/schema.sql +126 -0
  88. slidge/util/sql.py +508 -0
  89. slidge/util/test.py +295 -0
  90. slidge/util/types.py +180 -0
  91. slidge/util/util.py +295 -0
  92. slidge-0.1.0.dist-info/LICENSE +661 -0
  93. slidge-0.1.0.dist-info/METADATA +109 -0
  94. slidge-0.1.0.dist-info/RECORD +96 -0
  95. slidge-0.1.0.dist-info/WHEEL +4 -0
  96. slidge-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,398 @@
1
+ import logging
2
+ import uuid
3
+ import warnings
4
+ from datetime import datetime
5
+ from typing import TYPE_CHECKING, Iterable, Optional
6
+
7
+ from slixmpp import Iq, Message
8
+
9
+ from ...slixfix.xep_0490.mds import PUBLISH_OPTIONS
10
+ from ...util.types import (
11
+ ChatState,
12
+ LegacyMessageType,
13
+ LegacyThreadType,
14
+ LinkPreview,
15
+ Marker,
16
+ MessageReference,
17
+ ProcessingHint,
18
+ )
19
+ from .attachment import AttachmentMixin
20
+ from .message_maker import MessageMaker
21
+
22
+ if TYPE_CHECKING:
23
+ from ...group import LegacyMUC
24
+
25
+
26
+ class ChatStateMixin(MessageMaker):
27
+ def __init__(self):
28
+ super().__init__()
29
+ self.__last_chat_state: Optional[ChatState] = None
30
+
31
+ def _chat_state(self, state: ChatState, forced=False, **kwargs):
32
+ carbon = kwargs.get("carbon", False)
33
+ if carbon or (state == self.__last_chat_state and not forced):
34
+ return
35
+ self.__last_chat_state = state
36
+ msg = self._make_message(state=state, hints={"no-store"})
37
+ self._send(msg, **kwargs)
38
+
39
+ def active(self, **kwargs):
40
+ """
41
+ Send an "active" chat state (:xep:`0085`) from this
42
+ :term:`XMPP Entity`.
43
+ """
44
+ self._chat_state("active", **kwargs)
45
+
46
+ def composing(self, **kwargs):
47
+ """
48
+ Send a "composing" (ie "typing notification") chat state (:xep:`0085`)
49
+ from this :term:`XMPP Entity`.
50
+ """
51
+ self._chat_state("composing", forced=True, **kwargs)
52
+
53
+ def paused(self, **kwargs):
54
+ """
55
+ Send a "paused" (ie "typing paused notification") chat state
56
+ (:xep:`0085`) from this :term:`XMPP Entity`.
57
+ """
58
+ self._chat_state("paused", **kwargs)
59
+
60
+ def inactive(self, **kwargs):
61
+ """
62
+ Send an "inactive" (ie "contact has not interacted with the chat session
63
+ interface for an intermediate period of time") chat state (:xep:`0085`)
64
+ from this :term:`XMPP Entity`.
65
+ """
66
+ self._chat_state("inactive", **kwargs)
67
+
68
+ def gone(self, **kwargs):
69
+ """
70
+ Send a "gone" (ie "contact has not interacted with the chat session interface,
71
+ system, or device for a relatively long period of time") chat state
72
+ (:xep:`0085`) from this :term:`XMPP Entity`.
73
+ """
74
+ self._chat_state("gone", **kwargs)
75
+
76
+
77
+ class MarkerMixin(MessageMaker):
78
+ is_group: bool = NotImplemented
79
+
80
+ def _make_marker(
81
+ self, legacy_msg_id: LegacyMessageType, marker: Marker, carbon=False
82
+ ):
83
+ msg = self._make_message(carbon=carbon)
84
+ msg[marker]["id"] = self._legacy_to_xmpp(legacy_msg_id)
85
+ return msg
86
+
87
+ def ack(self, legacy_msg_id: LegacyMessageType, **kwargs):
88
+ """
89
+ Send an "acknowledged" message marker (:xep:`0333`) from this :term:`XMPP Entity`.
90
+
91
+ :param legacy_msg_id: The message this marker refers to
92
+ """
93
+ self._send(
94
+ self._make_marker(
95
+ legacy_msg_id, "acknowledged", carbon=kwargs.get("carbon")
96
+ ),
97
+ **kwargs,
98
+ )
99
+
100
+ def received(self, legacy_msg_id: LegacyMessageType, **kwargs):
101
+ """
102
+ Send a "received" message marker (:xep:`0333`) from this :term:`XMPP Entity`.
103
+ If called on a :class:`LegacyContact`, also send a delivery receipt
104
+ marker (:xep:`0184`).
105
+
106
+ :param legacy_msg_id: The message this marker refers to
107
+ """
108
+ carbon = kwargs.get("carbon")
109
+ if self.mtype == "chat":
110
+ self._send(
111
+ self.xmpp.delivery_receipt.make_ack(
112
+ self._legacy_to_xmpp(legacy_msg_id),
113
+ mfrom=self.jid,
114
+ mto=self.user.jid,
115
+ )
116
+ )
117
+ self._send(
118
+ self._make_marker(legacy_msg_id, "received", carbon=carbon), **kwargs
119
+ )
120
+
121
+ def displayed(self, legacy_msg_id: LegacyMessageType, **kwargs):
122
+ """
123
+ Send a "displayed" message marker (:xep:`0333`) from this :term:`XMPP Entity`.
124
+
125
+ :param legacy_msg_id: The message this marker refers to
126
+ """
127
+ self._send(
128
+ self._make_marker(legacy_msg_id, "displayed", carbon=kwargs.get("carbon")),
129
+ **kwargs,
130
+ )
131
+ if getattr(self, "is_user", False):
132
+ self.xmpp.loop.create_task(self.__send_mds(legacy_msg_id))
133
+
134
+ async def __send_mds(self, legacy_msg_id: LegacyMessageType):
135
+ # Send a MDS displayed marker on behalf of the user for a group chat
136
+ if muc := getattr(self, "muc", None):
137
+ muc_jid = muc.jid.bare
138
+ else:
139
+ # This is not implemented for 1:1 chat because it would rely on
140
+ # storing the XMPP-server injected stanza-id, which we don't track
141
+ # ATM.
142
+ # In practice, MDS should mostly be useful for public group chats,
143
+ # so it should not be an issue.
144
+ # We'll see if we need to implement that later
145
+ return
146
+ xmpp_msg_id = self._legacy_to_xmpp(legacy_msg_id)
147
+ iq = Iq(sto=self.user.bare_jid, sfrom=self.user.bare_jid, stype="set")
148
+ iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0490"].stanza.NS
149
+ iq["pubsub"]["publish"]["item"]["id"] = muc_jid
150
+ displayed = self.xmpp["xep_0490"].stanza.Displayed()
151
+ displayed["stanza_id"]["id"] = xmpp_msg_id
152
+ displayed["stanza_id"]["by"] = muc_jid
153
+ iq["pubsub"]["publish"]["item"]["payload"] = displayed
154
+ iq["pubsub"]["publish_options"] = PUBLISH_OPTIONS
155
+ try:
156
+ await self.xmpp["xep_0356"].send_privileged_iq(iq)
157
+ except Exception as e:
158
+ self.session.log.debug("Could not MDS mark", exc_info=e)
159
+
160
+
161
+ class ContentMessageMixin(AttachmentMixin):
162
+ def __default_hints(self, hints: Optional[Iterable[ProcessingHint]] = None):
163
+ if hints is not None:
164
+ return hints
165
+ elif self.mtype == "chat":
166
+ return {"markable", "store"}
167
+ elif self.mtype == "groupchat":
168
+ return {"markable"}
169
+
170
+ def __replace_id(self, legacy_msg_id: LegacyMessageType):
171
+ if self.mtype == "groupchat":
172
+ return self.session.muc_sent_msg_ids.get(
173
+ legacy_msg_id
174
+ ) or self._legacy_to_xmpp(legacy_msg_id)
175
+ else:
176
+ return self._legacy_to_xmpp(legacy_msg_id)
177
+
178
+ def send_text(
179
+ self,
180
+ body: str,
181
+ legacy_msg_id: Optional[LegacyMessageType] = None,
182
+ *,
183
+ when: Optional[datetime] = None,
184
+ reply_to: Optional[MessageReference] = None,
185
+ thread: Optional[LegacyThreadType] = None,
186
+ hints: Optional[Iterable[ProcessingHint]] = None,
187
+ carbon=False,
188
+ archive_only=False,
189
+ correction=False,
190
+ correction_event_id: Optional[LegacyMessageType] = None,
191
+ link_previews: Optional[list[LinkPreview]] = None,
192
+ **send_kwargs,
193
+ ):
194
+ """
195
+ Send a text message from this :term:`XMPP Entity`.
196
+
197
+ :param body: Content of the message
198
+ :param legacy_msg_id: If you want to be able to transport read markers from the gateway
199
+ user to the legacy network, specify this
200
+ :param when: when the message was sent, for a "delay" tag (:xep:`0203`)
201
+ :param reply_to: Quote another message (:xep:`0461`)
202
+ :param hints:
203
+ :param thread:
204
+ :param carbon: (only used if called on a :class:`LegacyContact`)
205
+ Set this to ``True`` if this is actually a message sent **to** the
206
+ :class:`LegacyContact` by the :term:`User`.
207
+ Use this to synchronize outgoing history for legacy official apps.
208
+ :param correction: whether this message is a correction or not
209
+ :param correction_event_id: in the case where an ID is associated with the legacy
210
+ 'correction event', specify it here to use it on the XMPP side. If not specified,
211
+ a random ID will be used.
212
+ :param link_previews: A little of sender (or server, or gateway)-generated
213
+ previews of URLs linked in the body.
214
+ :param archive_only: (only in groups) Do not send this message to user,
215
+ but store it in the archive. Meant to be used during ``MUC.backfill()``
216
+ """
217
+ if carbon:
218
+ if not correction and legacy_msg_id in self.session.sent:
219
+ log.warning(
220
+ "Carbon message for a message an XMPP has sent? This is a bug! %s",
221
+ legacy_msg_id,
222
+ )
223
+ return
224
+ self.session.sent[legacy_msg_id] = self.session.legacy_to_xmpp_msg_id(
225
+ legacy_msg_id
226
+ )
227
+ hints = self.__default_hints(hints)
228
+ msg = self._make_message(
229
+ mbody=body,
230
+ legacy_msg_id=correction_event_id if correction else legacy_msg_id,
231
+ when=when,
232
+ reply_to=reply_to,
233
+ hints=hints or (),
234
+ carbon=carbon,
235
+ thread=thread,
236
+ link_previews=link_previews,
237
+ )
238
+ if correction:
239
+ msg["replace"]["id"] = self.__replace_id(legacy_msg_id)
240
+ return self._send(msg, archive_only=archive_only, carbon=carbon, **send_kwargs)
241
+
242
+ def correct(
243
+ self,
244
+ legacy_msg_id: LegacyMessageType,
245
+ new_text: str,
246
+ *,
247
+ when: Optional[datetime] = None,
248
+ reply_to: Optional[MessageReference] = None,
249
+ thread: Optional[LegacyThreadType] = None,
250
+ hints: Optional[Iterable[ProcessingHint]] = None,
251
+ carbon=False,
252
+ archive_only=False,
253
+ correction_event_id: Optional[LegacyMessageType] = None,
254
+ link_previews: Optional[list[LinkPreview]] = None,
255
+ **send_kwargs,
256
+ ):
257
+ """
258
+ Modify a message that was previously sent by this :term:`XMPP Entity`.
259
+
260
+ Uses last message correction (:xep:`0308`)
261
+
262
+ :param new_text: New content of the message
263
+ :param legacy_msg_id: The legacy message ID of the message to correct
264
+ :param when: when the message was sent, for a "delay" tag (:xep:`0203`)
265
+ :param reply_to: Quote another message (:xep:`0461`)
266
+ :param hints:
267
+ :param thread:
268
+ :param carbon: (only in 1:1) Reflect a message sent to this ``Contact`` by the user.
269
+ Use this to synchronize outgoing history for legacy official apps.
270
+ :param archive_only: (only in groups) Do not send this message to user,
271
+ but store it in the archive. Meant to be used during ``MUC.backfill()``
272
+ :param correction_event_id: in the case where an ID is associated with the legacy
273
+ 'correction event', specify it here to use it on the XMPP side. If not specified,
274
+ a random ID will be used.
275
+ :param link_previews: A little of sender (or server, or gateway)-generated
276
+ previews of URLs linked in the body.
277
+ """
278
+ self.send_text(
279
+ new_text,
280
+ legacy_msg_id,
281
+ when=when,
282
+ reply_to=reply_to,
283
+ hints=hints,
284
+ carbon=carbon,
285
+ thread=thread,
286
+ correction=True,
287
+ archive_only=archive_only,
288
+ correction_event_id=correction_event_id,
289
+ link_previews=link_previews,
290
+ **send_kwargs,
291
+ )
292
+
293
+ def react(
294
+ self,
295
+ legacy_msg_id: LegacyMessageType,
296
+ emojis: Iterable[str] = (),
297
+ thread: Optional[LegacyThreadType] = None,
298
+ **kwargs,
299
+ ):
300
+ """
301
+ Send a reaction (:xep:`0444`) from this :term:`XMPP Entity`.
302
+
303
+ :param legacy_msg_id: The message which the reaction refers to.
304
+ :param emojis: An iterable of emojis used as reactions
305
+ :param thread:
306
+ """
307
+ msg = self._make_message(
308
+ hints={"store"}, carbon=kwargs.get("carbon"), thread=thread
309
+ )
310
+ xmpp_id = kwargs.pop("xmpp_id", None)
311
+ if not xmpp_id:
312
+ xmpp_id = self._legacy_to_xmpp(legacy_msg_id)
313
+ self.xmpp["xep_0444"].set_reactions(msg, to_id=xmpp_id, reactions=emojis)
314
+ self._send(msg, **kwargs)
315
+
316
+ def retract(
317
+ self,
318
+ legacy_msg_id: LegacyMessageType,
319
+ thread: Optional[LegacyThreadType] = None,
320
+ **kwargs,
321
+ ):
322
+ """
323
+ Send a message retraction (:XEP:`0424`) from this :term:`XMPP Entity`.
324
+
325
+ :param legacy_msg_id: Legacy ID of the message to delete
326
+ :param thread:
327
+ """
328
+ msg = self._make_message(
329
+ state=None,
330
+ hints={"store"},
331
+ mbody=f"/me retracted the message {legacy_msg_id}",
332
+ carbon=kwargs.get("carbon"),
333
+ thread=thread,
334
+ )
335
+ msg.enable("fallback")
336
+ # namespace version mismatch between slidge and slixmpp, update me later
337
+ msg["fallback"]["for"] = self.xmpp["xep_0424"].namespace[:-1] + "1"
338
+ msg["retract"]["id"] = msg["replace"]["id"] = self.__replace_id(legacy_msg_id)
339
+ self._send(msg, **kwargs)
340
+
341
+
342
+ class CarbonMessageMixin(ContentMessageMixin, MarkerMixin):
343
+ def _privileged_send(self, msg: Message):
344
+ i = msg.get_id()
345
+ if not i:
346
+ i = str(uuid.uuid4())
347
+ msg.set_id(i)
348
+ msg.del_origin_id()
349
+ self.session.ignore_messages.add(i)
350
+ try:
351
+ self.xmpp["xep_0356"].send_privileged_message(msg)
352
+ except PermissionError:
353
+ try:
354
+ self.xmpp["xep_0356_old"].send_privileged_message(msg)
355
+ except PermissionError:
356
+ warnings.warn(
357
+ "Slidge does not have privileges to send message on behalf of"
358
+ " user.Refer to"
359
+ " https://slidge.readthedocs.io/en/latest/admin/xmpp_server.html"
360
+ " for more info."
361
+ )
362
+
363
+
364
+ class InviteMixin(MessageMaker):
365
+ def invite_to(
366
+ self,
367
+ muc: "LegacyMUC",
368
+ reason: Optional[str] = None,
369
+ password: Optional[str] = None,
370
+ **send_kwargs,
371
+ ):
372
+ """
373
+ Send an invitation to join a group (:xep:`0249`) from this :term:`XMPP Entity`.
374
+
375
+ :param muc: the muc the user is invited to
376
+ :param reason: a text explaining why the user should join this muc
377
+ :param password: maybe this will make sense later? not sure
378
+ :param send_kwargs: additional kwargs to be passed to _send()
379
+ (internal use by slidge)
380
+ """
381
+ msg = self._make_message(mtype="normal")
382
+ msg["groupchat_invite"]["jid"] = muc.jid
383
+ if reason:
384
+ msg["groupchat_invite"]["reason"] = reason
385
+ if password:
386
+ msg["groupchat_invite"]["password"] = password
387
+ self._send(msg, **send_kwargs)
388
+
389
+
390
+ class MessageMixin(InviteMixin, ChatStateMixin, MarkerMixin, ContentMessageMixin):
391
+ pass
392
+
393
+
394
+ class MessageCarbonMixin(InviteMixin, ChatStateMixin, CarbonMessageMixin):
395
+ pass
396
+
397
+
398
+ log = logging.getLogger(__name__)
@@ -0,0 +1,154 @@
1
+ import warnings
2
+ from copy import copy
3
+ from datetime import datetime, timezone
4
+ from typing import TYPE_CHECKING, Iterable, Optional, cast
5
+ from uuid import uuid4
6
+
7
+ from slixmpp import Message
8
+ from slixmpp.types import MessageTypes
9
+
10
+ from ...slixfix.link_preview.stanza import LinkPreview as LinkPreviewStanza
11
+ from ...util.db import GatewayUser
12
+ from ...util.types import (
13
+ ChatState,
14
+ LegacyMessageType,
15
+ LinkPreview,
16
+ MessageReference,
17
+ ProcessingHint,
18
+ )
19
+ from .. import config
20
+ from .base import BaseSender
21
+
22
+ if TYPE_CHECKING:
23
+ from ...group.participant import LegacyParticipant
24
+
25
+
26
+ class MessageMaker(BaseSender):
27
+ mtype: MessageTypes = NotImplemented
28
+ _can_send_carbon: bool = NotImplemented
29
+ STRIP_SHORT_DELAY = False
30
+ USE_STANZA_ID = False
31
+
32
+ def _make_message(
33
+ self,
34
+ state: Optional[ChatState] = None,
35
+ hints: Iterable[ProcessingHint] = (),
36
+ legacy_msg_id: Optional[LegacyMessageType] = None,
37
+ when: Optional[datetime] = None,
38
+ reply_to: Optional[MessageReference] = None,
39
+ carbon=False,
40
+ link_previews: Optional[Iterable[LinkPreview]] = None,
41
+ **kwargs,
42
+ ):
43
+ body = kwargs.pop("mbody", None)
44
+ mfrom = kwargs.pop("mfrom", self.jid)
45
+ mto = kwargs.pop("mto", None)
46
+ thread = kwargs.pop("thread", None)
47
+ if carbon and self._can_send_carbon:
48
+ # the msg needs to have jabber:client as xmlns, so
49
+ # we don't want to associate with the XML stream
50
+ msg_cls = Message
51
+ else:
52
+ msg_cls = self.xmpp.Message # type:ignore
53
+ msg = msg_cls(
54
+ sfrom=mfrom,
55
+ stype=kwargs.pop("mtype", None) or self.mtype,
56
+ sto=mto,
57
+ **kwargs,
58
+ )
59
+ if body:
60
+ msg["body"] = body
61
+ state = "active"
62
+ if thread:
63
+ known_threads = self.session.threads.inverse # type:ignore
64
+ msg["thread"] = known_threads.get(thread) or str(thread)
65
+ if state:
66
+ msg["chat_state"] = state
67
+ for hint in hints:
68
+ msg.enable(hint)
69
+ self._set_msg_id(msg, legacy_msg_id)
70
+ self._add_delay(msg, when)
71
+ if link_previews:
72
+ self._add_link_previews(msg, link_previews)
73
+ if reply_to:
74
+ self._add_reply_to(msg, reply_to)
75
+ return msg
76
+
77
+ def _set_msg_id(
78
+ self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
79
+ ):
80
+ if legacy_msg_id is not None:
81
+ i = self._legacy_to_xmpp(legacy_msg_id)
82
+ msg.set_id(i)
83
+ if self.USE_STANZA_ID:
84
+ msg["stanza_id"]["id"] = i
85
+ msg["stanza_id"]["by"] = self.muc.jid # type: ignore
86
+ elif self.USE_STANZA_ID:
87
+ msg["stanza_id"]["id"] = str(uuid4())
88
+ msg["stanza_id"]["by"] = self.muc.jid # type: ignore
89
+
90
+ def _legacy_to_xmpp(self, legacy_id: LegacyMessageType):
91
+ return self.session.sent.get(legacy_id) or self.session.legacy_to_xmpp_msg_id(
92
+ legacy_id
93
+ )
94
+
95
+ def _add_delay(self, msg: Message, when: Optional[datetime]):
96
+ if when:
97
+ if when.tzinfo is None:
98
+ when = when.astimezone(timezone.utc)
99
+ if self.STRIP_SHORT_DELAY:
100
+ delay = datetime.now().astimezone(timezone.utc) - when
101
+ if delay < config.IGNORE_DELAY_THRESHOLD:
102
+ return
103
+ msg["delay"].set_stamp(when)
104
+ msg["delay"].set_from(self.xmpp.boundjid.bare)
105
+
106
+ def _add_reply_to(self, msg: Message, reply_to: MessageReference):
107
+ xmpp_id = self._legacy_to_xmpp(reply_to.legacy_id)
108
+ msg["reply"]["id"] = xmpp_id
109
+
110
+ muc = getattr(self, "muc", None)
111
+
112
+ if entity := reply_to.author:
113
+ if isinstance(entity, GatewayUser):
114
+ if muc:
115
+ jid = copy(muc.jid)
116
+ jid.resource = fallback_nick = muc.user_nick
117
+ msg["reply"]["to"] = jid
118
+ else:
119
+ msg["reply"]["to"] = entity.jid
120
+ # TODO: here we should use preferably use the PEP nick of the user
121
+ # (but it doesn't matter much)
122
+ fallback_nick = entity.jid.local
123
+ else:
124
+ if muc:
125
+ if hasattr(entity, "muc"):
126
+ # TODO: accept a Contact here and use muc.get_participant_by_legacy_id()
127
+ # a bit of work because right now this is a sync function
128
+ entity = cast("LegacyParticipant", entity)
129
+ fallback_nick = entity.nickname
130
+ else:
131
+ warnings.warn(
132
+ "The author of a message reference in a MUC must be a"
133
+ " Participant instance, not a Contact"
134
+ )
135
+ fallback_nick = entity.name
136
+ else:
137
+ fallback_nick = entity.name
138
+ msg["reply"]["to"] = entity.jid
139
+ else:
140
+ fallback_nick = None
141
+
142
+ if fallback := reply_to.body:
143
+ msg["reply"].add_quoted_fallback(fallback, fallback_nick)
144
+
145
+ @staticmethod
146
+ def _add_link_previews(msg: Message, link_previews: Iterable[LinkPreview]):
147
+ for preview in link_previews:
148
+ element = LinkPreviewStanza()
149
+ for i, name in enumerate(preview._fields):
150
+ val = preview[i]
151
+ if not val:
152
+ continue
153
+ element[name] = val
154
+ msg.append(element)