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.
- slidge/__init__.py +5 -2
- slidge/command/adhoc.py +9 -3
- slidge/command/admin.py +16 -12
- slidge/command/base.py +16 -12
- slidge/command/chat_command.py +25 -16
- slidge/command/user.py +7 -8
- slidge/contact/contact.py +119 -209
- slidge/contact/roster.py +106 -105
- slidge/core/config.py +2 -43
- slidge/core/dispatcher/caps.py +9 -2
- slidge/core/dispatcher/disco.py +13 -3
- slidge/core/dispatcher/message/__init__.py +1 -1
- slidge/core/dispatcher/message/chat_state.py +17 -8
- slidge/core/dispatcher/message/marker.py +7 -5
- slidge/core/dispatcher/message/message.py +117 -92
- slidge/core/dispatcher/muc/__init__.py +1 -1
- slidge/core/dispatcher/muc/admin.py +4 -4
- slidge/core/dispatcher/muc/mam.py +10 -6
- slidge/core/dispatcher/muc/misc.py +4 -2
- slidge/core/dispatcher/muc/owner.py +5 -3
- slidge/core/dispatcher/muc/ping.py +3 -1
- slidge/core/dispatcher/presence.py +21 -15
- slidge/core/dispatcher/registration.py +20 -12
- slidge/core/dispatcher/search.py +7 -3
- slidge/core/dispatcher/session_dispatcher.py +13 -5
- slidge/core/dispatcher/util.py +37 -27
- slidge/core/dispatcher/vcard.py +7 -4
- slidge/core/gateway.py +168 -84
- slidge/core/mixins/__init__.py +1 -11
- slidge/core/mixins/attachment.py +163 -148
- slidge/core/mixins/avatar.py +100 -177
- slidge/core/mixins/db.py +50 -2
- slidge/core/mixins/message.py +19 -17
- slidge/core/mixins/message_maker.py +29 -15
- slidge/core/mixins/message_text.py +38 -30
- slidge/core/mixins/presence.py +91 -35
- slidge/core/pubsub.py +42 -47
- slidge/core/session.py +88 -57
- slidge/db/alembic/versions/0337c90c0b96_unify_legacy_xmpp_id_mappings.py +183 -0
- slidge/db/alembic/versions/4dbd23a3f868_new_avatar_store.py +56 -0
- slidge/db/alembic/versions/54ce3cde350c_use_hash_for_avatar_filenames.py +50 -0
- slidge/db/alembic/versions/58b98dacf819_refactor.py +118 -0
- slidge/db/alembic/versions/75a62b74b239_ditch_hats_table.py +74 -0
- slidge/db/avatar.py +150 -119
- slidge/db/meta.py +33 -22
- slidge/db/models.py +68 -117
- slidge/db/store.py +412 -1094
- slidge/group/archive.py +61 -54
- slidge/group/bookmarks.py +74 -55
- slidge/group/participant.py +135 -142
- slidge/group/room.py +315 -312
- slidge/main.py +28 -18
- slidge/migration.py +2 -12
- slidge/slixfix/__init__.py +20 -4
- slidge/slixfix/delivery_receipt.py +6 -4
- slidge/slixfix/link_preview/link_preview.py +1 -1
- slidge/slixfix/link_preview/stanza.py +1 -1
- slidge/slixfix/roster.py +5 -7
- slidge/slixfix/xep_0077/register.py +8 -8
- slidge/slixfix/xep_0077/stanza.py +7 -7
- slidge/slixfix/xep_0100/gateway.py +12 -13
- slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
- slidge/slixfix/xep_0292/vcard4.py +1 -1
- slidge/util/archive_msg.py +11 -5
- slidge/util/conf.py +23 -20
- slidge/util/jid_escaping.py +1 -1
- slidge/{core/mixins → util}/lock.py +6 -6
- slidge/util/test.py +30 -29
- slidge/util/types.py +22 -18
- slidge/util/util.py +19 -22
- {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/METADATA +1 -1
- slidge-0.3.0a0.dist-info/RECORD +117 -0
- {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/WHEEL +1 -1
- slidge-0.2.12.dist-info/RECORD +0 -112
- {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/entry_points.txt +0 -0
- {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
- {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/top_level.txt +0 -0
slidge/core/mixins/avatar.py
CHANGED
@@ -1,25 +1,21 @@
|
|
1
|
-
from asyncio import 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 ...
|
11
|
-
|
12
|
-
|
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
|
-
|
28
|
+
stored: Contact | Room
|
33
29
|
|
34
30
|
def __init__(self) -> None:
|
35
31
|
super().__init__()
|
36
|
-
self._set_avatar_task:
|
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
|
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
|
-
|
49
|
-
"""
|
50
|
-
return self.__avatar_unique_id
|
37
|
+
This property can be used to set or unset the avatar.
|
51
38
|
|
52
|
-
|
53
|
-
|
39
|
+
Unlike the awaitable :method:`.set_avatar`, it schedules the update for
|
40
|
+
later execution and is not blocking
|
54
41
|
"""
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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,
|
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.
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
94
|
-
|
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
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
117
|
-
|
118
|
-
|
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
|
-
|
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
|
154
|
-
|
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
|
-
|
162
|
-
|
163
|
-
if await self.
|
100
|
+
avatar = convert_avatar(avatar)
|
101
|
+
|
102
|
+
if not await self.__has_changed(avatar):
|
164
103
|
return
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
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
|
-
|
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)
|
slidge/core/mixins/message.py
CHANGED
@@ -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(
|
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
|
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
|
-
|
64
|
-
|
65
|
-
|
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.
|
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
|
-
|
93
|
-
self.
|
94
|
-
|
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):
|