slidge 0.2.12__py3-none-any.whl → 0.3.0a0__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 (77) hide show
  1. slidge/__init__.py +5 -2
  2. slidge/command/adhoc.py +9 -3
  3. slidge/command/admin.py +16 -12
  4. slidge/command/base.py +16 -12
  5. slidge/command/chat_command.py +25 -16
  6. slidge/command/user.py +7 -8
  7. slidge/contact/contact.py +119 -209
  8. slidge/contact/roster.py +106 -105
  9. slidge/core/config.py +2 -43
  10. slidge/core/dispatcher/caps.py +9 -2
  11. slidge/core/dispatcher/disco.py +13 -3
  12. slidge/core/dispatcher/message/__init__.py +1 -1
  13. slidge/core/dispatcher/message/chat_state.py +17 -8
  14. slidge/core/dispatcher/message/marker.py +7 -5
  15. slidge/core/dispatcher/message/message.py +117 -92
  16. slidge/core/dispatcher/muc/__init__.py +1 -1
  17. slidge/core/dispatcher/muc/admin.py +4 -4
  18. slidge/core/dispatcher/muc/mam.py +10 -6
  19. slidge/core/dispatcher/muc/misc.py +4 -2
  20. slidge/core/dispatcher/muc/owner.py +5 -3
  21. slidge/core/dispatcher/muc/ping.py +3 -1
  22. slidge/core/dispatcher/presence.py +21 -15
  23. slidge/core/dispatcher/registration.py +20 -12
  24. slidge/core/dispatcher/search.py +7 -3
  25. slidge/core/dispatcher/session_dispatcher.py +13 -5
  26. slidge/core/dispatcher/util.py +37 -27
  27. slidge/core/dispatcher/vcard.py +7 -4
  28. slidge/core/gateway.py +168 -84
  29. slidge/core/mixins/__init__.py +1 -11
  30. slidge/core/mixins/attachment.py +163 -148
  31. slidge/core/mixins/avatar.py +100 -177
  32. slidge/core/mixins/db.py +50 -2
  33. slidge/core/mixins/message.py +19 -17
  34. slidge/core/mixins/message_maker.py +29 -15
  35. slidge/core/mixins/message_text.py +38 -30
  36. slidge/core/mixins/presence.py +91 -35
  37. slidge/core/pubsub.py +42 -47
  38. slidge/core/session.py +88 -57
  39. slidge/db/alembic/versions/0337c90c0b96_unify_legacy_xmpp_id_mappings.py +183 -0
  40. slidge/db/alembic/versions/4dbd23a3f868_new_avatar_store.py +56 -0
  41. slidge/db/alembic/versions/54ce3cde350c_use_hash_for_avatar_filenames.py +50 -0
  42. slidge/db/alembic/versions/58b98dacf819_refactor.py +118 -0
  43. slidge/db/alembic/versions/75a62b74b239_ditch_hats_table.py +74 -0
  44. slidge/db/avatar.py +150 -119
  45. slidge/db/meta.py +33 -22
  46. slidge/db/models.py +68 -117
  47. slidge/db/store.py +412 -1094
  48. slidge/group/archive.py +61 -54
  49. slidge/group/bookmarks.py +74 -55
  50. slidge/group/participant.py +135 -142
  51. slidge/group/room.py +315 -312
  52. slidge/main.py +28 -18
  53. slidge/migration.py +2 -12
  54. slidge/slixfix/__init__.py +20 -4
  55. slidge/slixfix/delivery_receipt.py +6 -4
  56. slidge/slixfix/link_preview/link_preview.py +1 -1
  57. slidge/slixfix/link_preview/stanza.py +1 -1
  58. slidge/slixfix/roster.py +5 -7
  59. slidge/slixfix/xep_0077/register.py +8 -8
  60. slidge/slixfix/xep_0077/stanza.py +7 -7
  61. slidge/slixfix/xep_0100/gateway.py +12 -13
  62. slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
  63. slidge/slixfix/xep_0292/vcard4.py +1 -1
  64. slidge/util/archive_msg.py +11 -5
  65. slidge/util/conf.py +23 -20
  66. slidge/util/jid_escaping.py +1 -1
  67. slidge/{core/mixins → util}/lock.py +6 -6
  68. slidge/util/test.py +30 -29
  69. slidge/util/types.py +22 -18
  70. slidge/util/util.py +19 -22
  71. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/METADATA +1 -1
  72. slidge-0.3.0a0.dist-info/RECORD +117 -0
  73. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/WHEEL +1 -1
  74. slidge-0.2.12.dist-info/RECORD +0 -112
  75. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/entry_points.txt +0 -0
  76. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
  77. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/top_level.txt +0 -0
@@ -1,25 +1,21 @@
1
- from asyncio import Task, create_task
2
- from hashlib import sha1
1
+ from asyncio import Task
3
2
  from pathlib import Path
4
3
  from typing import TYPE_CHECKING, Optional
5
4
 
6
5
  from PIL import UnidentifiedImageError
7
6
  from slixmpp import JID
7
+ from sqlalchemy.orm.exc import DetachedInstanceError
8
8
 
9
9
  from ...db.avatar import CachedAvatar, avatar_cache
10
- from ...util.types import (
11
- URL,
12
- AnyBaseSession,
13
- AvatarIdType,
14
- AvatarType,
15
- LegacyFileIdType,
16
- )
10
+ from ...db.models import Contact, Room
11
+ from ...util.types import AnyBaseSession, Avatar
12
+ from .db import UpdateInfoMixin
17
13
 
18
14
  if TYPE_CHECKING:
19
15
  from ..pubsub import PepAvatar
20
16
 
21
17
 
22
- class AvatarMixin:
18
+ class AvatarMixin(UpdateInfoMixin):
23
19
  """
24
20
  Mixin for XMPP entities that have avatars that represent them.
25
21
 
@@ -29,154 +25,119 @@ class AvatarMixin:
29
25
 
30
26
  jid: JID = NotImplemented
31
27
  session: AnyBaseSession = NotImplemented
32
- _avatar_bare_jid: bool = NotImplemented
28
+ stored: Contact | Room
33
29
 
34
30
  def __init__(self) -> None:
35
31
  super().__init__()
36
- self._set_avatar_task: Optional[Task] = None
37
- self.__broadcast_task: Optional[Task] = None
38
- self.__avatar_unique_id: Optional[AvatarIdType] = None
39
- self._avatar_pk: Optional[int] = None
32
+ self._set_avatar_task: Task | None = None
40
33
 
41
34
  @property
42
- def __avatar_jid(self):
43
- return JID(self.jid.bare) if self._avatar_bare_jid else self.jid
44
-
45
- @property
46
- def avatar_id(self) -> Optional[AvatarIdType]:
35
+ def avatar(self) -> Avatar | None:
47
36
  """
48
- The unique ID of this entity's avatar.
49
- """
50
- return self.__avatar_unique_id
37
+ This property can be used to set or unset the avatar.
51
38
 
52
- @property
53
- def avatar(self) -> Optional[AvatarIdType]:
39
+ Unlike the awaitable :method:`.set_avatar`, it schedules the update for
40
+ later execution and is not blocking
54
41
  """
55
- This property can be used to set the avatar, but
56
- :py:meth:`~.AvatarMixin.set_avatar()` should be preferred because you can
57
- provide a unique ID for the avatar for efficient caching.
58
- Setting this is OKish in case the avatar type is a URL or a local path
59
- that can act as a legacy ID.
60
-
61
- Python's ``property`` is abused here to maintain backwards
62
- compatibility, but when getting it you actually get the avatar legacy
63
- ID.
64
- """
65
- return self.__avatar_unique_id
42
+ try:
43
+ if self.stored.avatar is None:
44
+ return None
45
+ except DetachedInstanceError:
46
+ self.merge()
47
+ if self.stored.avatar is None:
48
+ return None
49
+ if self.stored.avatar.legacy_id is None:
50
+ unique_id = None
51
+ else:
52
+ unique_id = self.session.xmpp.AVATAR_ID_TYPE(self.stored.avatar.legacy_id)
53
+ return Avatar(
54
+ unique_id=unique_id,
55
+ url=self.stored.avatar.url,
56
+ )
66
57
 
67
58
  @avatar.setter
68
- def avatar(self, a: Optional[AvatarType]):
59
+ def avatar(self, avatar: Avatar | Path | str | None) -> None:
60
+ avatar = convert_avatar(avatar)
69
61
  if self._set_avatar_task:
70
62
  self._set_avatar_task.cancel()
71
63
  self.session.log.debug("Setting avatar with property")
72
- self._set_avatar_task = self.session.xmpp.loop.create_task(
73
- self.set_avatar(a, None, blocking=True, cancel=False),
74
- name=f"Set avatar of {self} from property",
75
- )
64
+ self._set_avatar_task = self.session.create_task(self.set_avatar(avatar))
76
65
 
77
- @property
78
- def avatar_pk(self) -> int | None:
79
- return self._avatar_pk
80
-
81
- @staticmethod
82
- def __get_uid(a: Optional[AvatarType]) -> Optional[AvatarIdType]:
83
- if isinstance(a, str):
84
- return URL(a)
85
- elif isinstance(a, Path):
86
- return str(a)
87
- elif isinstance(a, bytes):
88
- return sha1(a).hexdigest()
89
- elif a is None:
90
- return None
91
- raise TypeError("Bad avatar", a)
66
+ async def __has_changed(self, avatar: Avatar | None) -> bool:
67
+ if self.avatar is None:
68
+ return avatar is not None
69
+ if avatar is None:
70
+ return self.avatar is not None
92
71
 
93
- async def __set_avatar(
94
- self, a: Optional[AvatarType], uid: Optional[AvatarIdType], delete: bool
95
- ):
96
- self.__avatar_unique_id = uid
72
+ if self.avatar.unique_id is not None and avatar.unique_id is not None:
73
+ return self.avatar.unique_id != avatar.unique_id
97
74
 
98
- if a is None:
99
- cached_avatar = None
100
- self._avatar_pk = None
101
- else:
102
- try:
103
- cached_avatar = await avatar_cache.convert_or_get(a)
104
- except UnidentifiedImageError:
105
- self.session.log.warning("%s is not a valid avatar", a)
106
- self._avatar_pk = None
107
- self.__avatar_unique_id = uid
108
- return
109
- except Exception as e:
110
- self.session.log.error("Failed to set avatar %s", a, exc_info=e)
111
- self._avatar_pk = None
112
- self.__avatar_unique_id = uid
113
- return
114
- self._avatar_pk = cached_avatar.pk
75
+ if (
76
+ self.avatar.url is not None
77
+ and avatar.url is not None
78
+ and self.avatar.url == avatar.url
79
+ ):
80
+ return await avatar_cache.url_modified(avatar.url)
115
81
 
116
- if self.__should_pubsub_broadcast():
117
- await self.session.xmpp.pubsub.broadcast_avatar(
118
- self.__avatar_jid, self.session.user_jid, cached_avatar
119
- )
82
+ if avatar.path is not None:
83
+ cached = self.get_cached_avatar()
84
+ if cached is not None:
85
+ return cached.path.read_bytes() != avatar.path.read_bytes()
120
86
 
121
- if delete and isinstance(a, Path):
122
- a.unlink()
123
-
124
- self._post_avatar_update()
125
-
126
- def __should_pubsub_broadcast(self):
127
- return getattr(self, "is_friend", False) and getattr(
128
- self, "added_to_roster", False
129
- )
130
-
131
- async def _no_change(self, a: Optional[AvatarType], uid: Optional[AvatarIdType]):
132
- if a is None:
133
- return self.__avatar_unique_id is None
134
- if not self.__avatar_unique_id:
135
- return False
136
- if isinstance(uid, URL):
137
- if self.__avatar_unique_id != uid:
138
- return False
139
- return not await avatar_cache.url_modified(uid)
140
- return self.__avatar_unique_id == uid
87
+ return True
141
88
 
142
89
  async def set_avatar(
143
- self,
144
- a: Optional[AvatarType],
145
- avatar_unique_id: Optional[LegacyFileIdType] = None,
146
- delete: bool = False,
147
- blocking=False,
148
- cancel=True,
90
+ self, avatar: Avatar | Path | str | None = None, delete: bool = False
149
91
  ) -> None:
150
92
  """
151
93
  Set an avatar for this entity
152
94
 
153
- :param a: The avatar, in one of the types slidge supports
154
- :param avatar_unique_id: A globally unique ID for the avatar on the
155
- legacy network
95
+ :param avatar: The avatar. Should ideally come with a legacy network-wide unique
96
+ ID
156
97
  :param delete: If the avatar is provided as a Path, whether to delete
157
98
  it once used or not.
158
- :param blocking: Internal use by slidge for tests, do not use!
159
- :param cancel: Internal use by slidge, do not use!
160
99
  """
161
- if avatar_unique_id is None and a is not None:
162
- avatar_unique_id = self.__get_uid(a)
163
- if await self._no_change(a, avatar_unique_id):
100
+ avatar = convert_avatar(avatar)
101
+
102
+ if not await self.__has_changed(avatar):
164
103
  return
165
- if cancel and self._set_avatar_task:
166
- self._set_avatar_task.cancel()
167
- awaitable = create_task(
168
- self.__set_avatar(a, avatar_unique_id, delete),
169
- name=f"Set pubsub avatar of {self}",
170
- )
171
- if not self._set_avatar_task or self._set_avatar_task.done():
172
- self._set_avatar_task = awaitable
173
- if blocking:
174
- await awaitable
104
+
105
+ if avatar is None:
106
+ cached_avatar = None
107
+ else:
108
+ try:
109
+ cached_avatar = await avatar_cache.convert_or_get(avatar)
110
+ except UnidentifiedImageError:
111
+ self.session.log.warning("%s is not a valid image", avatar)
112
+ cached_avatar = None
113
+ except Exception as e:
114
+ self.session.log.error("Failed to set avatar %s: %s", avatar, e)
115
+ cached_avatar = None
116
+
117
+ if delete:
118
+ if avatar is None or avatar.path is None:
119
+ self.session.log.warning(
120
+ "Requested avatar path delete, but no path provided"
121
+ )
122
+ else:
123
+ avatar.path.unlink()
124
+
125
+ if cached_avatar is None:
126
+ self.stored.avatar = None
127
+ else:
128
+ self.stored.avatar = cached_avatar.stored
129
+ self.commit(merge=True)
130
+ self._post_avatar_update(cached_avatar)
175
131
 
176
132
  def get_cached_avatar(self) -> Optional["CachedAvatar"]:
177
- if self._avatar_pk is None:
178
- return None
179
- return avatar_cache.get_by_pk(self._avatar_pk)
133
+ try:
134
+ if self.stored.avatar is None:
135
+ return None
136
+ except DetachedInstanceError:
137
+ self.merge()
138
+ if self.stored.avatar is None:
139
+ return None
140
+ return avatar_cache.get(self.stored.avatar)
180
141
 
181
142
  def get_avatar(self) -> Optional["PepAvatar"]:
182
143
  cached_avatar = self.get_cached_avatar()
@@ -188,55 +149,17 @@ class AvatarMixin:
188
149
  item.set_avatar_from_cache(cached_avatar)
189
150
  return item
190
151
 
191
- def _post_avatar_update(self) -> None:
192
- return
193
-
194
- def __get_cached_avatar_id(self):
195
- i = self._get_cached_avatar_id()
196
- if i is None:
197
- return None
198
- return self.session.xmpp.AVATAR_ID_TYPE(i)
199
-
200
- def _get_cached_avatar_id(self) -> Optional[str]:
152
+ def _post_avatar_update(self, cached_avatar: Optional["CachedAvatar"]) -> None:
201
153
  raise NotImplementedError
202
154
 
203
- async def avatar_wrap_update_info(self):
204
- cached_id = self.__get_cached_avatar_id()
205
- self.__avatar_unique_id = cached_id
206
- try:
207
- await self.update_info() # type:ignore
208
- except NotImplementedError:
209
- return
210
- new_id = self.avatar
211
- if isinstance(new_id, URL) and not await avatar_cache.url_modified(new_id):
212
- return
213
- elif new_id != cached_id:
214
- # at this point it means that update_info set the avatar, and we don't
215
- # need to do anything else
216
- return
217
155
 
218
- if self.__should_pubsub_broadcast():
219
- if new_id is None and cached_id is None:
220
- return
221
- if self._avatar_pk is not None:
222
- cached_avatar = avatar_cache.get_by_pk(self._avatar_pk)
223
- else:
224
- cached_avatar = None
225
- self.__broadcast_task = self.session.xmpp.loop.create_task(
226
- self.session.xmpp.pubsub.broadcast_avatar(
227
- self.__avatar_jid, self.session.user_jid, cached_avatar
228
- )
229
- )
230
-
231
- def _set_avatar_from_store(self, stored):
232
- if stored.avatar_id is None:
233
- return
234
- if stored.avatar is None:
235
- # seems to happen after avatar cleanup for some reason?
236
- self.__avatar_unique_id = None
237
- return
238
- self.__avatar_unique_id = (
239
- stored.avatar.legacy_id
240
- if stored.avatar.legacy_id is not None
241
- else URL(stored.avatar.url)
242
- )
156
+ def convert_avatar(
157
+ avatar: Avatar | Path | str | None, unique_id: str | None = None
158
+ ) -> Avatar | None:
159
+ if isinstance(avatar, Path):
160
+ return Avatar(path=avatar, unique_id=unique_id)
161
+ if isinstance(avatar, str):
162
+ return Avatar(url=avatar)
163
+ if avatar is None or all(x is None for x in avatar):
164
+ return None
165
+ return avatar
slidge/core/mixins/db.py CHANGED
@@ -1,18 +1,66 @@
1
+ import logging
2
+ import typing
1
3
  from contextlib import contextmanager
2
4
 
5
+ from ...db.models import Base, Contact, Participant, Room
3
6
 
4
- class UpdateInfoMixin:
7
+ if typing.TYPE_CHECKING:
8
+ from slidge import BaseGateway
9
+
10
+
11
+ class DBMixin:
12
+ stored: Base
13
+ xmpp: "BaseGateway"
14
+ log: logging.Logger
15
+
16
+ def merge(self) -> None:
17
+ with self.xmpp.store.session() as orm:
18
+ self.stored = orm.merge(self.stored)
19
+
20
+ def commit(self, merge: bool = False) -> None:
21
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
22
+ if merge:
23
+ self.log.debug("Merging %s", self.stored)
24
+ self.stored = orm.merge(self.stored)
25
+ self.log.debug("Merged %s", self.stored)
26
+ orm.add(self.stored)
27
+ self.log.debug("Committing to DB")
28
+ orm.commit()
29
+
30
+
31
+ class UpdateInfoMixin(DBMixin):
5
32
  """
6
33
  This mixin just adds a context manager that prevents commiting to the DB
7
34
  on every attribute change.
8
35
  """
9
36
 
10
- def __init__(self, *args, **kwargs):
37
+ stored: Contact | Room
38
+ xmpp: "BaseGateway"
39
+ log: logging.Logger
40
+
41
+ def __init__(self, *args, **kwargs) -> None:
11
42
  super().__init__(*args, **kwargs)
12
43
  self._updating_info = False
44
+ if self.stored.extra_attributes is not None:
45
+ self.deserialize_extra_attributes(self.stored.extra_attributes)
46
+
47
+ def serialize_extra_attributes(self) -> dict | None:
48
+ return None
49
+
50
+ def deserialize_extra_attributes(self, data: dict) -> None:
51
+ pass
13
52
 
14
53
  @contextmanager
15
54
  def updating_info(self):
16
55
  self._updating_info = True
17
56
  yield
18
57
  self._updating_info = False
58
+ self.stored.updated = True
59
+ self.commit()
60
+
61
+ def commit(self, merge: bool = False) -> None:
62
+ if self._updating_info:
63
+ self.log.debug("Not updating %s right now", self.stored)
64
+ else:
65
+ self.stored.extra_attributes = self.serialize_extra_attributes()
66
+ super().commit(merge=merge)
@@ -27,11 +27,11 @@ PUBLISH_OPTIONS.add_field("pubsub#access_model", value="whitelist")
27
27
 
28
28
 
29
29
  class ChatStateMixin(MessageMaker):
30
- def __init__(self):
30
+ def __init__(self) -> None:
31
31
  super().__init__()
32
32
  self.__last_chat_state: Optional[ChatState] = None
33
33
 
34
- def _chat_state(self, state: ChatState, forced=False, **kwargs):
34
+ def _chat_state(self, state: ChatState, forced: bool = False, **kwargs) -> None:
35
35
  carbon = kwargs.get("carbon", False)
36
36
  if carbon or (state == self.__last_chat_state and not forced):
37
37
  return
@@ -39,28 +39,28 @@ class ChatStateMixin(MessageMaker):
39
39
  msg = self._make_message(state=state, hints={"no-store"})
40
40
  self._send(msg, **kwargs)
41
41
 
42
- def active(self, **kwargs):
42
+ def active(self, **kwargs) -> None:
43
43
  """
44
44
  Send an "active" chat state (:xep:`0085`) from this
45
45
  :term:`XMPP Entity`.
46
46
  """
47
47
  self._chat_state("active", **kwargs)
48
48
 
49
- def composing(self, **kwargs):
49
+ def composing(self, **kwargs) -> None:
50
50
  """
51
51
  Send a "composing" (ie "typing notification") chat state (:xep:`0085`)
52
52
  from this :term:`XMPP Entity`.
53
53
  """
54
54
  self._chat_state("composing", forced=True, **kwargs)
55
55
 
56
- def paused(self, **kwargs):
56
+ def paused(self, **kwargs) -> None:
57
57
  """
58
58
  Send a "paused" (ie "typing paused notification") chat state
59
59
  (:xep:`0085`) from this :term:`XMPP Entity`.
60
60
  """
61
61
  self._chat_state("paused", **kwargs)
62
62
 
63
- def inactive(self, **kwargs):
63
+ def inactive(self, **kwargs) -> None:
64
64
  """
65
65
  Send an "inactive" (ie "contact has not interacted with the chat session
66
66
  interface for an intermediate period of time") chat state (:xep:`0085`)
@@ -68,7 +68,7 @@ class ChatStateMixin(MessageMaker):
68
68
  """
69
69
  self._chat_state("inactive", **kwargs)
70
70
 
71
- def gone(self, **kwargs):
71
+ def gone(self, **kwargs) -> None:
72
72
  """
73
73
  Send a "gone" (ie "contact has not interacted with the chat session interface,
74
74
  system, or device for a relatively long period of time") chat state
@@ -81,13 +81,13 @@ class MarkerMixin(MessageMaker):
81
81
  is_group: bool = NotImplemented
82
82
 
83
83
  def _make_marker(
84
- self, legacy_msg_id: LegacyMessageType, marker: Marker, carbon=False
84
+ self, legacy_msg_id: LegacyMessageType, marker: Marker, carbon: bool = False
85
85
  ):
86
86
  msg = self._make_message(carbon=carbon)
87
87
  msg[marker]["id"] = self._legacy_to_xmpp(legacy_msg_id)
88
88
  return msg
89
89
 
90
- def ack(self, legacy_msg_id: LegacyMessageType, **kwargs):
90
+ def ack(self, legacy_msg_id: LegacyMessageType, **kwargs) -> None:
91
91
  """
92
92
  Send an "acknowledged" message marker (:xep:`0333`) from this :term:`XMPP Entity`.
93
93
 
@@ -95,12 +95,12 @@ class MarkerMixin(MessageMaker):
95
95
  """
96
96
  self._send(
97
97
  self._make_marker(
98
- legacy_msg_id, "acknowledged", carbon=kwargs.get("carbon")
98
+ legacy_msg_id, "acknowledged", carbon=bool(kwargs.get("carbon"))
99
99
  ),
100
100
  **kwargs,
101
101
  )
102
102
 
103
- def received(self, legacy_msg_id: LegacyMessageType, **kwargs):
103
+ def received(self, legacy_msg_id: LegacyMessageType, **kwargs) -> None:
104
104
  """
105
105
  Send a "received" message marker (:xep:`0333`) from this :term:`XMPP Entity`.
106
106
  If called on a :class:`LegacyContact`, also send a delivery receipt
@@ -108,7 +108,7 @@ class MarkerMixin(MessageMaker):
108
108
 
109
109
  :param legacy_msg_id: The message this marker refers to
110
110
  """
111
- carbon = kwargs.get("carbon")
111
+ carbon = bool(kwargs.get("carbon"))
112
112
  if self.mtype == "chat":
113
113
  self._send(
114
114
  self.xmpp.delivery_receipt.make_ack(
@@ -121,20 +121,22 @@ class MarkerMixin(MessageMaker):
121
121
  self._make_marker(legacy_msg_id, "received", carbon=carbon), **kwargs
122
122
  )
123
123
 
124
- def displayed(self, legacy_msg_id: LegacyMessageType, **kwargs):
124
+ def displayed(self, legacy_msg_id: LegacyMessageType, **kwargs) -> None:
125
125
  """
126
126
  Send a "displayed" message marker (:xep:`0333`) from this :term:`XMPP Entity`.
127
127
 
128
128
  :param legacy_msg_id: The message this marker refers to
129
129
  """
130
130
  self._send(
131
- self._make_marker(legacy_msg_id, "displayed", carbon=kwargs.get("carbon")),
131
+ self._make_marker(
132
+ legacy_msg_id, "displayed", carbon=bool(kwargs.get("carbon"))
133
+ ),
132
134
  **kwargs,
133
135
  )
134
136
  if getattr(self, "is_user", False):
135
137
  self.session.create_task(self.__send_mds(legacy_msg_id))
136
138
 
137
- async def __send_mds(self, legacy_msg_id: LegacyMessageType):
139
+ async def __send_mds(self, legacy_msg_id: LegacyMessageType) -> None:
138
140
  # Send a MDS displayed marker on behalf of the user for a group chat
139
141
  if muc := getattr(self, "muc", None):
140
142
  muc_jid = muc.jid.bare
@@ -166,7 +168,7 @@ class ContentMessageMixin(AttachmentMixin, TextMessageMixin):
166
168
 
167
169
 
168
170
  class CarbonMessageMixin(ContentMessageMixin, MarkerMixin):
169
- def _privileged_send(self, msg: Message):
171
+ def _privileged_send(self, msg: Message) -> None:
170
172
  i = msg.get_id()
171
173
  if i:
172
174
  self.session.ignore_messages.add(i)
@@ -192,7 +194,7 @@ class InviteMixin(MessageMaker):
192
194
  reason: Optional[str] = None,
193
195
  password: Optional[str] = None,
194
196
  **send_kwargs,
195
- ):
197
+ ) -> None:
196
198
  """
197
199
  Send an invitation to join a group (:xep:`0249`) from this :term:`XMPP Entity`.
198
200
 
@@ -1,5 +1,4 @@
1
1
  import warnings
2
- from copy import copy
3
2
  from datetime import datetime, timezone
4
3
  from typing import TYPE_CHECKING, Iterable, Optional, cast
5
4
  from uuid import uuid4
@@ -20,7 +19,7 @@ from .. import config
20
19
  from .base import BaseSender
21
20
 
22
21
  if TYPE_CHECKING:
23
- from ...group.participant import LegacyParticipant
22
+ from ...group import LegacyMUC, LegacyParticipant
24
23
 
25
24
 
26
25
  class MessageMaker(BaseSender):
@@ -29,6 +28,12 @@ class MessageMaker(BaseSender):
29
28
  STRIP_SHORT_DELAY = False
30
29
  USE_STANZA_ID = False
31
30
 
31
+ muc: "LegacyMUC"
32
+ is_group: bool
33
+
34
+ def _recipient_pk(self) -> int:
35
+ return self.muc.stored.id if self.is_group else self.stored.id # type:ignore
36
+
32
37
  def _make_message(
33
38
  self,
34
39
  state: Optional[ChatState] = None,
@@ -36,7 +41,7 @@ class MessageMaker(BaseSender):
36
41
  legacy_msg_id: Optional[LegacyMessageType] = None,
37
42
  when: Optional[datetime] = None,
38
43
  reply_to: Optional[MessageReference] = None,
39
- carbon=False,
44
+ carbon: bool = False,
40
45
  link_previews: Optional[Iterable[LinkPreview]] = None,
41
46
  **kwargs,
42
47
  ):
@@ -60,9 +65,14 @@ class MessageMaker(BaseSender):
60
65
  msg["body"] = body
61
66
  state = "active"
62
67
  if thread:
63
- msg["thread"] = self.xmpp.store.sent.get_legacy_thread(
64
- self.user_pk, str(thread)
65
- ) or str(thread)
68
+ with self.xmpp.store.session() as orm:
69
+ thread_str = str(thread)
70
+ msg["thread"] = (
71
+ self.xmpp.store.id_map.get_thread(
72
+ orm, self._recipient_pk(), thread_str, self.is_group
73
+ )
74
+ or thread_str
75
+ )
66
76
  if state:
67
77
  msg["chat_state"] = state
68
78
  for hint in hints:
@@ -77,9 +87,9 @@ class MessageMaker(BaseSender):
77
87
 
78
88
  def _set_msg_id(
79
89
  self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
80
- ):
90
+ ) -> None:
81
91
  if legacy_msg_id is not None:
82
- i = self._legacy_to_xmpp(legacy_msg_id)
92
+ i = self.session.legacy_to_xmpp_msg_id(legacy_msg_id)
83
93
  msg.set_id(i)
84
94
  if self.USE_STANZA_ID:
85
95
  msg["stanza_id"]["id"] = i
@@ -88,12 +98,16 @@ class MessageMaker(BaseSender):
88
98
  msg["stanza_id"]["id"] = str(uuid4())
89
99
  msg["stanza_id"]["by"] = self.muc.jid # type: ignore
90
100
 
91
- def _legacy_to_xmpp(self, legacy_id: LegacyMessageType):
92
- return self.xmpp.store.sent.get_xmpp_id(
93
- self.session.user_pk, str(legacy_id)
94
- ) or self.session.legacy_to_xmpp_msg_id(legacy_id)
101
+ def _legacy_to_xmpp(self, legacy_id: LegacyMessageType) -> str:
102
+ with self.xmpp.store.session() as orm:
103
+ ids = self.xmpp.store.id_map.get_xmpp(
104
+ orm, self._recipient_pk(), str(legacy_id), False
105
+ )
106
+ if ids:
107
+ return ids[-1]
108
+ return self.session.legacy_to_xmpp_msg_id(legacy_id)
95
109
 
96
- def _add_delay(self, msg: Message, when: Optional[datetime]):
110
+ def _add_delay(self, msg: Message, when: Optional[datetime]) -> None:
97
111
  if when:
98
112
  if when.tzinfo is None:
99
113
  when = when.astimezone(timezone.utc)
@@ -104,7 +118,7 @@ class MessageMaker(BaseSender):
104
118
  msg["delay"].set_stamp(when)
105
119
  msg["delay"].set_from(self.xmpp.boundjid.bare)
106
120
 
107
- def _add_reply_to(self, msg: Message, reply_to: MessageReference):
121
+ def _add_reply_to(self, msg: Message, reply_to: MessageReference) -> None:
108
122
  xmpp_id = self._legacy_to_xmpp(reply_to.legacy_id)
109
123
  msg["reply"]["id"] = xmpp_id
110
124
 
@@ -151,7 +165,7 @@ class MessageMaker(BaseSender):
151
165
  msg["reply"].add_quoted_fallback(fallback, fallback_nick)
152
166
 
153
167
  @staticmethod
154
- def _add_link_previews(msg: Message, link_previews: Iterable[LinkPreview]):
168
+ def _add_link_previews(msg: Message, link_previews: Iterable[LinkPreview]) -> None:
155
169
  for preview in link_previews:
156
170
  element = LinkPreviewStanza()
157
171
  for i, name in enumerate(preview._fields):