slidge 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- slidge/__init__.py +3 -5
- slidge/__main__.py +2 -197
- slidge/__version__.py +5 -0
- slidge/command/adhoc.py +40 -17
- slidge/command/admin.py +24 -12
- slidge/command/base.py +10 -8
- slidge/command/categories.py +13 -3
- slidge/command/chat_command.py +29 -2
- slidge/command/register.py +32 -16
- slidge/command/user.py +106 -13
- slidge/contact/contact.py +254 -50
- slidge/contact/roster.py +124 -53
- slidge/core/config.py +19 -13
- slidge/core/dispatcher/__init__.py +3 -0
- slidge/core/{gateway → dispatcher}/caps.py +12 -8
- slidge/core/{gateway → dispatcher}/disco.py +10 -18
- slidge/core/dispatcher/message/__init__.py +10 -0
- slidge/core/dispatcher/message/chat_state.py +40 -0
- slidge/core/dispatcher/message/marker.py +62 -0
- slidge/core/dispatcher/message/message.py +397 -0
- slidge/core/dispatcher/muc/__init__.py +12 -0
- slidge/core/dispatcher/muc/admin.py +98 -0
- slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
- slidge/core/dispatcher/muc/misc.py +121 -0
- slidge/core/dispatcher/muc/owner.py +96 -0
- slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
- slidge/core/dispatcher/presence.py +176 -0
- slidge/core/dispatcher/registration.py +85 -0
- slidge/core/{gateway → dispatcher}/search.py +9 -16
- slidge/core/dispatcher/session_dispatcher.py +84 -0
- slidge/core/dispatcher/util.py +174 -0
- slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
- slidge/core/{gateway/base.py → gateway.py} +176 -153
- slidge/core/mixins/__init__.py +11 -1
- slidge/core/mixins/attachment.py +106 -67
- slidge/core/mixins/avatar.py +94 -25
- slidge/core/mixins/base.py +10 -4
- slidge/core/mixins/db.py +18 -0
- slidge/core/mixins/disco.py +0 -10
- slidge/core/mixins/lock.py +10 -8
- slidge/core/mixins/message.py +11 -195
- slidge/core/mixins/message_maker.py +17 -9
- slidge/core/mixins/message_text.py +211 -0
- slidge/core/mixins/presence.py +17 -4
- slidge/core/pubsub.py +114 -288
- slidge/core/session.py +101 -40
- slidge/db/__init__.py +4 -0
- slidge/db/alembic/__init__.py +0 -0
- slidge/db/alembic/env.py +64 -0
- slidge/db/alembic/old_user_store.py +183 -0
- slidge/db/alembic/script.py.mako +26 -0
- slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
- slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
- slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
- slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
- slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
- slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
- slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
- slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
- slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
- slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
- slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
- slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
- slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
- slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
- slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
- slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
- slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
- slidge/db/avatar.py +205 -0
- slidge/db/meta.py +72 -0
- slidge/db/models.py +405 -0
- slidge/db/store.py +1257 -0
- slidge/group/archive.py +58 -14
- slidge/group/bookmarks.py +89 -65
- slidge/group/participant.py +107 -40
- slidge/group/room.py +402 -213
- slidge/main.py +202 -0
- slidge/migration.py +45 -1
- slidge/slixfix/__init__.py +31 -1
- slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
- slidge/slixfix/roster.py +13 -4
- slidge/slixfix/xep_0292/vcard4.py +1 -87
- slidge/util/archive_msg.py +2 -1
- slidge/util/db.py +4 -228
- slidge/util/test.py +91 -4
- slidge/util/types.py +39 -4
- slidge/util/util.py +45 -2
- {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
- slidge-0.2.0.dist-info/RECORD +131 -0
- slidge-0.2.0.dist-info/entry_points.txt +3 -0
- slidge/core/cache.py +0 -183
- slidge/core/gateway/__init__.py +0 -3
- slidge/core/gateway/muc_admin.py +0 -35
- slidge/core/gateway/presence.py +0 -95
- slidge/core/gateway/registration.py +0 -53
- slidge/core/gateway/session_dispatcher.py +0 -804
- slidge/util/schema.sql +0 -126
- slidge/util/sql.py +0 -508
- slidge-0.1.3.dist-info/RECORD +0 -96
- slidge-0.1.3.dist-info/entry_points.txt +0 -3
- {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
- {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/WHEEL +0 -0
slidge/contact/contact.py
CHANGED
@@ -2,7 +2,8 @@ import datetime
|
|
2
2
|
import logging
|
3
3
|
import warnings
|
4
4
|
from datetime import date
|
5
|
-
from typing import TYPE_CHECKING, Generic, Iterable, Optional, Union
|
5
|
+
from typing import TYPE_CHECKING, Generic, Iterable, Optional, Self, Union
|
6
|
+
from xml.etree import ElementTree as ET
|
6
7
|
|
7
8
|
from slixmpp import JID, Message, Presence
|
8
9
|
from slixmpp.exceptions import IqError
|
@@ -10,12 +11,13 @@ from slixmpp.plugins.xep_0292.stanza import VCard4
|
|
10
11
|
from slixmpp.types import MessageTypes
|
11
12
|
|
12
13
|
from ..core import config
|
13
|
-
from ..core.mixins import FullCarbonMixin
|
14
|
-
from ..core.mixins.
|
14
|
+
from ..core.mixins import AvatarMixin, FullCarbonMixin, StoredAttributeMixin
|
15
|
+
from ..core.mixins.db import UpdateInfoMixin
|
15
16
|
from ..core.mixins.disco import ContactAccountDiscoMixin
|
16
17
|
from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
|
18
|
+
from ..db.models import Contact
|
17
19
|
from ..util import SubclassableOnce
|
18
|
-
from ..util.types import LegacyUserIdType, MessageOrPresenceTypeVar
|
20
|
+
from ..util.types import ClientType, LegacyUserIdType, MessageOrPresenceTypeVar
|
19
21
|
|
20
22
|
if TYPE_CHECKING:
|
21
23
|
from ..core.session import BaseSession
|
@@ -24,11 +26,13 @@ if TYPE_CHECKING:
|
|
24
26
|
|
25
27
|
class LegacyContact(
|
26
28
|
Generic[LegacyUserIdType],
|
29
|
+
StoredAttributeMixin,
|
27
30
|
AvatarMixin,
|
28
31
|
ContactAccountDiscoMixin,
|
29
32
|
FullCarbonMixin,
|
30
33
|
ReactionRecipientMixin,
|
31
34
|
ThreadRecipientMixin,
|
35
|
+
UpdateInfoMixin,
|
32
36
|
metaclass=SubclassableOnce,
|
33
37
|
):
|
34
38
|
"""
|
@@ -81,7 +85,6 @@ class LegacyContact(
|
|
81
85
|
STRIP_SHORT_DELAY = True
|
82
86
|
_NON_FRIEND_PRESENCES_FILTER = {"subscribe", "unsubscribed"}
|
83
87
|
|
84
|
-
_avatar_pubsub_broadcast = True
|
85
88
|
_avatar_bare_jid = True
|
86
89
|
|
87
90
|
INVITATION_RECIPIENT = True
|
@@ -100,7 +103,6 @@ class LegacyContact(
|
|
100
103
|
"""
|
101
104
|
super().__init__()
|
102
105
|
self.session = session
|
103
|
-
self.user = session.user
|
104
106
|
self.legacy_id: LegacyUserIdType = legacy_id
|
105
107
|
"""
|
106
108
|
The legacy identifier of the :term:`Legacy Contact`.
|
@@ -116,19 +118,125 @@ class LegacyContact(
|
|
116
118
|
|
117
119
|
self._name: Optional[str] = None
|
118
120
|
|
119
|
-
if self.xmpp.MARK_ALL_MESSAGES:
|
120
|
-
self._sent_order = list[str]()
|
121
|
-
|
122
121
|
self.xmpp = session.xmpp
|
123
122
|
self.jid = JID(self.jid_username + "@" + self.xmpp.boundjid.bare)
|
124
123
|
self.jid.resource = self.RESOURCE
|
125
|
-
self.log = logging.getLogger(
|
126
|
-
self.
|
127
|
-
self.
|
128
|
-
self.
|
124
|
+
self.log = logging.getLogger(self.jid.bare)
|
125
|
+
self._set_logger_name()
|
126
|
+
self._is_friend: bool = False
|
127
|
+
self._added_to_roster = False
|
128
|
+
self._caps_ver: str | None = None
|
129
|
+
self._vcard_fetched = False
|
130
|
+
self._vcard: str | None = None
|
131
|
+
self._client_type: ClientType = "pc"
|
132
|
+
|
133
|
+
async def get_vcard(self, fetch=True) -> VCard4 | None:
|
134
|
+
if fetch and not self._vcard_fetched:
|
135
|
+
await self.fetch_vcard()
|
136
|
+
if self._vcard is None:
|
137
|
+
return None
|
138
|
+
|
139
|
+
return VCard4(xml=ET.fromstring(self._vcard))
|
140
|
+
|
141
|
+
@property
|
142
|
+
def is_friend(self):
|
143
|
+
return self._is_friend
|
144
|
+
|
145
|
+
@is_friend.setter
|
146
|
+
def is_friend(self, value: bool):
|
147
|
+
if value == self._is_friend:
|
148
|
+
return
|
149
|
+
self._is_friend = value
|
150
|
+
if self._updating_info:
|
151
|
+
return
|
152
|
+
self.__ensure_pk()
|
153
|
+
assert self.contact_pk is not None
|
154
|
+
self.xmpp.store.contacts.set_friend(self.contact_pk, value)
|
155
|
+
|
156
|
+
@property
|
157
|
+
def added_to_roster(self):
|
158
|
+
return self._added_to_roster
|
159
|
+
|
160
|
+
@added_to_roster.setter
|
161
|
+
def added_to_roster(self, value: bool):
|
162
|
+
if value == self._added_to_roster:
|
163
|
+
return
|
164
|
+
self._added_to_roster = value
|
165
|
+
if self._updating_info:
|
166
|
+
return
|
167
|
+
if self.contact_pk is None:
|
168
|
+
# during LegacyRoster.fill()
|
169
|
+
return
|
170
|
+
self.xmpp.store.contacts.set_added_to_roster(self.contact_pk, value)
|
171
|
+
|
172
|
+
@property
|
173
|
+
def participants(self) -> list["LegacyParticipant"]:
|
174
|
+
if self.contact_pk is None:
|
175
|
+
return []
|
176
|
+
|
177
|
+
self.__ensure_pk()
|
178
|
+
from ..group.participant import LegacyParticipant
|
179
|
+
|
180
|
+
return [
|
181
|
+
LegacyParticipant.get_self_or_unique_subclass().from_store(
|
182
|
+
self.session, stored, contact=self
|
183
|
+
)
|
184
|
+
for stored in self.xmpp.store.participants.get_for_contact(self.contact_pk)
|
185
|
+
]
|
186
|
+
|
187
|
+
@property
|
188
|
+
def user_jid(self):
|
189
|
+
return self.session.user_jid
|
190
|
+
|
191
|
+
@property # type:ignore
|
192
|
+
def DISCO_TYPE(self) -> ClientType:
|
193
|
+
return self._client_type
|
194
|
+
|
195
|
+
@DISCO_TYPE.setter
|
196
|
+
def DISCO_TYPE(self, value: ClientType) -> None:
|
197
|
+
self.client_type = value
|
198
|
+
|
199
|
+
@property
|
200
|
+
def client_type(self) -> ClientType:
|
201
|
+
"""
|
202
|
+
The client type of this contact, cf https://xmpp.org/registrar/disco-categories.html#client
|
203
|
+
|
204
|
+
Default is "pc".
|
205
|
+
"""
|
206
|
+
return self._client_type
|
207
|
+
|
208
|
+
@client_type.setter
|
209
|
+
def client_type(self, value: ClientType) -> None:
|
210
|
+
self._client_type = value
|
211
|
+
if self._updating_info:
|
212
|
+
return
|
213
|
+
self.__ensure_pk()
|
214
|
+
assert self.contact_pk is not None
|
215
|
+
self.xmpp.store.contacts.set_client_type(self.contact_pk, value)
|
216
|
+
|
217
|
+
def _set_logger_name(self):
|
218
|
+
self.log.name = f"{self.user_jid.bare}:contact:{self}"
|
129
219
|
|
130
220
|
def __repr__(self):
|
131
|
-
return f"<Contact '{self.
|
221
|
+
return f"<Contact #{self.contact_pk} '{self.name}' ({self.legacy_id} - {self.jid.local})'>"
|
222
|
+
|
223
|
+
def __ensure_pk(self):
|
224
|
+
if self.contact_pk is not None:
|
225
|
+
return
|
226
|
+
# This happens for legacy modules that don't follow the Roster.fill /
|
227
|
+
# populate contact attributes in Contact.update_info() method.
|
228
|
+
# This results in (even) less optimised SQL writes and read, but
|
229
|
+
# we allow it because it fits some legacy network libs better.
|
230
|
+
with self.xmpp.store.session() as orm:
|
231
|
+
orm.commit()
|
232
|
+
stored = self.xmpp.store.contacts.get_by_legacy_id(
|
233
|
+
self.user_pk, str(self.legacy_id)
|
234
|
+
)
|
235
|
+
if stored is None:
|
236
|
+
self.contact_pk = self.xmpp.store.contacts.update(self, commit=True)
|
237
|
+
else:
|
238
|
+
self.contact_pk = stored.id
|
239
|
+
assert self.contact_pk is not None
|
132
240
|
|
133
241
|
def __get_subscription_string(self):
|
134
242
|
if self.is_friend:
|
@@ -170,12 +278,13 @@ class LegacyContact(
|
|
170
278
|
) -> MessageOrPresenceTypeVar:
|
171
279
|
if carbon and isinstance(stanza, Message):
|
172
280
|
stanza["to"] = self.jid.bare
|
173
|
-
stanza["from"] = self.
|
281
|
+
stanza["from"] = self.user_jid
|
174
282
|
self._privileged_send(stanza)
|
175
283
|
return stanza # type:ignore
|
176
284
|
|
177
285
|
if isinstance(stanza, Presence):
|
178
|
-
self.
|
286
|
+
if not self._updating_info:
|
287
|
+
self.__propagate_to_participants(stanza)
|
179
288
|
if (
|
180
289
|
not self.is_friend
|
181
290
|
and stanza["type"] not in self._NON_FRIEND_PRESENCES_FILTER
|
@@ -185,13 +294,19 @@ class LegacyContact(
|
|
185
294
|
n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
|
186
295
|
n["nick"] = self.name
|
187
296
|
stanza.append(n)
|
188
|
-
if
|
189
|
-
self.
|
190
|
-
|
297
|
+
if (
|
298
|
+
not self._updating_info
|
299
|
+
and self.xmpp.MARK_ALL_MESSAGES
|
300
|
+
and is_markable(stanza)
|
301
|
+
):
|
302
|
+
self.__ensure_pk()
|
303
|
+
assert self.contact_pk is not None
|
304
|
+
self.xmpp.store.contacts.add_to_sent(self.contact_pk, stanza["id"])
|
305
|
+
stanza["to"] = self.user_jid
|
191
306
|
stanza.send()
|
192
307
|
return stanza
|
193
308
|
|
194
|
-
def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str):
|
309
|
+
def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str) -> list[str]:
|
195
310
|
"""
|
196
311
|
Return XMPP msg ids sent by this contact up to a given XMPP msg id.
|
197
312
|
|
@@ -205,15 +320,9 @@ class LegacyContact(
|
|
205
320
|
:param horizon_xmpp_id: The latest message
|
206
321
|
:return: A list of XMPP ids or None if horizon_xmpp_id was not found
|
207
322
|
"""
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
else:
|
212
|
-
return
|
213
|
-
i += 1
|
214
|
-
res = self._sent_order[:i]
|
215
|
-
self._sent_order = self._sent_order[i:]
|
216
|
-
return res
|
323
|
+
self.__ensure_pk()
|
324
|
+
assert self.contact_pk is not None
|
325
|
+
return self.xmpp.store.contacts.pop_sent_up_to(self.contact_pk, horizon_xmpp_id)
|
217
326
|
|
218
327
|
@property
|
219
328
|
def name(self):
|
@@ -226,12 +335,35 @@ class LegacyContact(
|
|
226
335
|
def name(self, n: Optional[str]):
|
227
336
|
if self._name == n:
|
228
337
|
return
|
338
|
+
self._name = n
|
339
|
+
self._set_logger_name()
|
340
|
+
if self.is_friend and self.added_to_roster:
|
341
|
+
self.xmpp.pubsub.broadcast_nick(
|
342
|
+
user_jid=self.user_jid, jid=self.jid.bare, nick=n
|
343
|
+
)
|
344
|
+
if self._updating_info:
|
345
|
+
# means we're in update_info(), so no participants, and no need
|
346
|
+
# to write to DB now, it will be called in Roster.__finish_init_contact
|
347
|
+
return
|
229
348
|
for p in self.participants:
|
230
349
|
p.nickname = n
|
231
|
-
self.
|
232
|
-
|
350
|
+
self.__ensure_pk()
|
351
|
+
assert self.contact_pk is not None
|
352
|
+
self.xmpp.store.contacts.update_nick(self.contact_pk, n)
|
353
|
+
|
354
|
+
def _get_cached_avatar_id(self) -> Optional[str]:
|
355
|
+
if self.contact_pk is None:
|
356
|
+
return None
|
357
|
+
return self.xmpp.store.contacts.get_avatar_legacy_id(self.contact_pk)
|
233
358
|
|
234
359
|
def _post_avatar_update(self):
|
360
|
+
self.__ensure_pk()
|
361
|
+
assert self.contact_pk is not None
|
362
|
+
self.xmpp.store.contacts.set_avatar(
|
363
|
+
self.contact_pk,
|
364
|
+
self._avatar_pk,
|
365
|
+
None if self.avatar_id is None else str(self.avatar_id),
|
366
|
+
)
|
235
367
|
for p in self.participants:
|
236
368
|
self.log.debug("Propagating new avatar to %s", p.muc)
|
237
369
|
p.send_last_presence(force=True, no_cache_online=True)
|
@@ -283,7 +415,26 @@ class LegacyContact(
|
|
283
415
|
elif country:
|
284
416
|
vcard.add_address(country, locality)
|
285
417
|
|
286
|
-
self.
|
418
|
+
self._vcard = str(vcard)
|
419
|
+
self._vcard_fetched = True
|
420
|
+
self.session.create_task(
|
421
|
+
self.xmpp.pubsub.broadcast_vcard_event(self.jid, self.user_jid, vcard)
|
422
|
+
)
|
423
|
+
|
424
|
+
if self._updating_info:
|
425
|
+
return
|
426
|
+
|
427
|
+
assert self.contact_pk is not None
|
428
|
+
self.xmpp.store.contacts.set_vcard(self.contact_pk, self._vcard)
|
429
|
+
|
430
|
+
def get_roster_item(self):
|
431
|
+
item = {
|
432
|
+
"subscription": self.__get_subscription_string(),
|
433
|
+
"groups": [self.xmpp.ROSTER_GROUP],
|
434
|
+
}
|
435
|
+
if (n := self.name) is not None:
|
436
|
+
item["name"] = n
|
437
|
+
return {self.jid.bare: item}
|
287
438
|
|
288
439
|
async def add_to_roster(self, force=False):
|
289
440
|
"""
|
@@ -291,23 +442,15 @@ class LegacyContact(
|
|
291
442
|
|
292
443
|
:param force: add even if the contact was already added successfully
|
293
444
|
"""
|
294
|
-
if self.
|
445
|
+
if self.added_to_roster and not force:
|
295
446
|
return
|
296
447
|
if config.NO_ROSTER_PUSH:
|
297
448
|
log.debug("Roster push request by plugin ignored (--no-roster-push)")
|
298
449
|
return
|
299
|
-
item = {
|
300
|
-
"subscription": self.__get_subscription_string(),
|
301
|
-
"groups": [self.xmpp.ROSTER_GROUP],
|
302
|
-
}
|
303
|
-
if (n := self.name) is not None:
|
304
|
-
item["name"] = n
|
305
|
-
kw = dict(
|
306
|
-
jid=self.user.jid,
|
307
|
-
roster_items={self.jid.bare: item},
|
308
|
-
)
|
309
450
|
try:
|
310
|
-
await self._set_roster(
|
451
|
+
await self._set_roster(
|
452
|
+
jid=self.user_jid, roster_items=self.get_roster_item()
|
453
|
+
)
|
311
454
|
except PermissionError:
|
312
455
|
warnings.warn(
|
313
456
|
"Slidge does not have privileges to add contacts to the roster. Refer"
|
@@ -325,18 +468,33 @@ class LegacyContact(
|
|
325
468
|
else:
|
326
469
|
# we only broadcast pubsub events for contacts added to the roster
|
327
470
|
# so if something was set before, we need to push it now
|
328
|
-
self.
|
329
|
-
self.session.create_task(self.__broadcast_pubsub_items())
|
471
|
+
self.added_to_roster = True
|
330
472
|
self.send_last_presence()
|
331
473
|
|
332
474
|
async def __broadcast_pubsub_items(self):
|
333
|
-
|
475
|
+
if not self.is_friend:
|
476
|
+
return
|
477
|
+
if not self.added_to_roster:
|
478
|
+
return
|
479
|
+
cached_avatar = self.get_cached_avatar()
|
480
|
+
if cached_avatar is not None:
|
481
|
+
await self.xmpp.pubsub.broadcast_avatar(
|
482
|
+
self.jid.bare, self.session.user_jid, cached_avatar
|
483
|
+
)
|
484
|
+
nick = self.name
|
485
|
+
|
486
|
+
if nick is not None:
|
487
|
+
self.xmpp.pubsub.broadcast_nick(
|
488
|
+
self.session.user_jid,
|
489
|
+
self.jid.bare,
|
490
|
+
nick,
|
491
|
+
)
|
334
492
|
|
335
493
|
async def _set_roster(self, **kw):
|
336
494
|
try:
|
337
|
-
|
495
|
+
await self.xmpp["xep_0356"].set_roster(**kw)
|
338
496
|
except PermissionError:
|
339
|
-
|
497
|
+
await self.xmpp["xep_0356_old"].set_roster(**kw)
|
340
498
|
|
341
499
|
def send_friend_request(self, text: Optional[str] = None):
|
342
500
|
presence = self._make_presence(ptype="subscribe", pstatus=text, bare=True)
|
@@ -350,6 +508,8 @@ class LegacyContact(
|
|
350
508
|
:param text: Optional message from the friend to the user
|
351
509
|
"""
|
352
510
|
self.is_friend = True
|
511
|
+
self.added_to_roster = True
|
512
|
+
self.__ensure_pk()
|
353
513
|
self.log.debug("Accepting friend request")
|
354
514
|
presence = self._make_presence(ptype="subscribed", pstatus=text, bare=True)
|
355
515
|
self._send(presence, nick=True)
|
@@ -415,7 +575,7 @@ class LegacyContact(
|
|
415
575
|
their 'friends'".
|
416
576
|
"""
|
417
577
|
for ptype in "unsubscribe", "unsubscribed", "unavailable":
|
418
|
-
self.xmpp.send_presence(pfrom=self.jid, pto=self.
|
578
|
+
self.xmpp.send_presence(pfrom=self.jid, pto=self.user_jid.bare, ptype=ptype) # type: ignore
|
419
579
|
|
420
580
|
async def update_info(self):
|
421
581
|
"""
|
@@ -442,6 +602,50 @@ class LegacyContact(
|
|
442
602
|
"""
|
443
603
|
pass
|
444
604
|
|
605
|
+
def _make_presence(
|
606
|
+
self,
|
607
|
+
*,
|
608
|
+
last_seen: Optional[datetime.datetime] = None,
|
609
|
+
status_codes: Optional[set[int]] = None,
|
610
|
+
user_full_jid: Optional[JID] = None,
|
611
|
+
**presence_kwargs,
|
612
|
+
):
|
613
|
+
p = super()._make_presence(last_seen=last_seen, **presence_kwargs)
|
614
|
+
caps = self.xmpp.plugin["xep_0115"]
|
615
|
+
if p.get_from().resource and self._caps_ver:
|
616
|
+
p["caps"]["node"] = caps.caps_node
|
617
|
+
p["caps"]["hash"] = caps.hash
|
618
|
+
p["caps"]["ver"] = self._caps_ver
|
619
|
+
return p
|
620
|
+
|
621
|
+
@classmethod
|
622
|
+
def from_store(cls, session, stored: Contact, *args, **kwargs) -> Self:
|
623
|
+
contact = cls(
|
624
|
+
session,
|
625
|
+
cls.xmpp.LEGACY_CONTACT_ID_TYPE(stored.legacy_id),
|
626
|
+
stored.jid.username, # type: ignore
|
627
|
+
*args, # type: ignore
|
628
|
+
**kwargs, # type: ignore
|
629
|
+
)
|
630
|
+
contact.contact_pk = stored.id
|
631
|
+
contact._name = stored.nick
|
632
|
+
contact._is_friend = stored.is_friend
|
633
|
+
contact._added_to_roster = stored.added_to_roster
|
634
|
+
if (data := stored.extra_attributes) is not None:
|
635
|
+
contact.deserialize_extra_attributes(data)
|
636
|
+
contact._caps_ver = stored.caps_ver
|
637
|
+
contact._set_logger_name()
|
638
|
+
contact._AvatarMixin__avatar_unique_id = ( # type:ignore
|
639
|
+
None
|
640
|
+
if stored.avatar_legacy_id is None
|
641
|
+
else session.xmpp.AVATAR_ID_TYPE(stored.avatar_legacy_id)
|
642
|
+
)
|
643
|
+
contact._avatar_pk = stored.avatar_id
|
644
|
+
contact._vcard = stored.vcard
|
645
|
+
contact._vcard_fetched = stored.vcard_fetched
|
646
|
+
contact._client_type = stored.client_type
|
647
|
+
return contact
|
648
|
+
|
445
649
|
|
446
650
|
def is_markable(stanza: Union[Message, Presence]):
|
447
651
|
if isinstance(stanza, Presence):
|
slidge/contact/roster.py
CHANGED
@@ -1,11 +1,15 @@
|
|
1
1
|
import asyncio
|
2
2
|
import logging
|
3
|
-
|
3
|
+
import warnings
|
4
|
+
from typing import TYPE_CHECKING, AsyncIterator, Generic, Iterator, Optional, Type
|
4
5
|
|
5
6
|
from slixmpp import JID
|
7
|
+
from slixmpp.exceptions import IqError, XMPPError
|
6
8
|
from slixmpp.jid import JID_UNESCAPE_TRANSFORMATIONS, _unescape_node
|
7
9
|
|
8
10
|
from ..core.mixins.lock import NamedLockMixin
|
11
|
+
from ..db.models import Contact
|
12
|
+
from ..db.store import ContactStore
|
9
13
|
from ..util import SubclassableOnce
|
10
14
|
from ..util.types import LegacyContactType, LegacyUserIdType
|
11
15
|
from .contact import LegacyContact
|
@@ -43,38 +47,27 @@ class LegacyRoster(
|
|
43
47
|
LegacyContact.get_self_or_unique_subclass()
|
44
48
|
)
|
45
49
|
self._contact_cls.xmpp = session.xmpp
|
50
|
+
self.__store: ContactStore = session.xmpp.store.contacts
|
46
51
|
|
47
52
|
self.session = session
|
48
|
-
self.
|
49
|
-
self._contacts_by_legacy_id: dict[LegacyUserIdType, LegacyContactType] = {}
|
50
|
-
self.log = logging.getLogger(f"{self.session.user.bare_jid}:roster")
|
53
|
+
self.log = logging.getLogger(f"{self.session.user_jid.bare}:roster")
|
51
54
|
self.user_legacy_id: Optional[LegacyUserIdType] = None
|
52
55
|
self.ready: asyncio.Future[bool] = self.session.xmpp.loop.create_future()
|
56
|
+
self.__filling = False
|
53
57
|
super().__init__()
|
54
58
|
|
55
59
|
def __repr__(self):
|
56
|
-
return f"<Roster of {self.session.
|
57
|
-
|
58
|
-
def __iter__(self):
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
self, legacy_id: LegacyUserIdType, jid_username: str, *args, **kwargs
|
63
|
-
):
|
64
|
-
c = self._contact_cls(self.session, legacy_id, jid_username, *args, **kwargs)
|
65
|
-
async with self.lock(("finish", c)):
|
66
|
-
if legacy_id in self._contacts_by_legacy_id:
|
67
|
-
self.log.debug("Already updated %s", c)
|
68
|
-
return c
|
69
|
-
await c.avatar_wrap_update_info()
|
70
|
-
self._contacts_by_legacy_id[legacy_id] = c
|
71
|
-
self._contacts_by_bare_jid[c.jid.bare] = c
|
72
|
-
return c
|
60
|
+
return f"<Roster of {self.session.user_jid}>"
|
61
|
+
|
62
|
+
def __iter__(self) -> Iterator[LegacyContactType]:
|
63
|
+
with self.__store.session():
|
64
|
+
for stored in self.__store.get_all(user_pk=self.session.user_pk):
|
65
|
+
yield self._contact_cls.from_store(self.session, stored)
|
73
66
|
|
74
67
|
def known_contacts(self, only_friends=True) -> dict[str, LegacyContactType]:
|
75
68
|
if only_friends:
|
76
|
-
return {
|
77
|
-
return self
|
69
|
+
return {c.jid.bare: c for c in self if c.is_friend}
|
70
|
+
return {c.jid.bare: c for c in self}
|
78
71
|
|
79
72
|
async def by_jid(self, contact_jid: JID) -> LegacyContactType:
|
80
73
|
# """
|
@@ -89,16 +82,22 @@ class LegacyRoster(
|
|
89
82
|
# """
|
90
83
|
username = contact_jid.node
|
91
84
|
async with self.lock(("username", username)):
|
92
|
-
|
93
|
-
|
94
|
-
if
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
85
|
+
legacy_id = await self.jid_username_to_legacy_id(username)
|
86
|
+
log.debug("Contact %s not found", contact_jid)
|
87
|
+
if self.get_lock(("legacy_id", legacy_id)):
|
88
|
+
log.debug("Already updating %s", contact_jid)
|
89
|
+
return await self.by_legacy_id(legacy_id)
|
90
|
+
|
91
|
+
with self.__store.session():
|
92
|
+
stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
|
93
|
+
return await self.__update_contact(stored, legacy_id, username)
|
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
|
102
101
|
|
103
102
|
async def by_legacy_id(
|
104
103
|
self, legacy_id: LegacyUserIdType, *args, **kwargs
|
@@ -121,20 +120,47 @@ class LegacyRoster(
|
|
121
120
|
if legacy_id == self.user_legacy_id:
|
122
121
|
raise ContactIsUser
|
123
122
|
async with self.lock(("legacy_id", legacy_id)):
|
124
|
-
|
125
|
-
if
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
123
|
+
username = await self.legacy_id_to_jid_username(legacy_id)
|
124
|
+
if self.get_lock(("username", username)):
|
125
|
+
log.debug("Already updating %s", username)
|
126
|
+
jid = JID()
|
127
|
+
jid.node = username
|
128
|
+
jid.domain = self.session.xmpp.boundjid.bare
|
129
|
+
return await self.by_jid(jid)
|
130
|
+
|
131
|
+
with self.__store.session():
|
132
|
+
stored = self.__store.get_by_legacy_id(
|
133
|
+
self.session.user_pk, str(legacy_id)
|
134
|
+
)
|
135
|
+
return await self.__update_contact(
|
136
|
+
stored, legacy_id, username, *args, **kwargs
|
136
137
|
)
|
137
|
-
|
138
|
+
|
139
|
+
async def __update_contact(
|
140
|
+
self,
|
141
|
+
stored: Contact | None,
|
142
|
+
legacy_id: LegacyUserIdType,
|
143
|
+
username: str,
|
144
|
+
*a,
|
145
|
+
**kw,
|
146
|
+
) -> LegacyContactType:
|
147
|
+
if stored is None:
|
148
|
+
contact = self._contact_cls(self.session, legacy_id, username, *a, **kw)
|
149
|
+
else:
|
150
|
+
contact = self._contact_cls.from_store(self.session, stored, *a, **kw)
|
151
|
+
if stored.updated:
|
152
|
+
return contact
|
153
|
+
|
154
|
+
try:
|
155
|
+
with contact.updating_info():
|
156
|
+
await contact.avatar_wrap_update_info()
|
157
|
+
except XMPPError:
|
158
|
+
raise
|
159
|
+
except Exception as e:
|
160
|
+
raise XMPPError("internal-server-error", str(e))
|
161
|
+
contact._caps_ver = await contact.get_caps_ver(contact.jid)
|
162
|
+
contact.contact_pk = self.__store.update(contact, commit=not self.__filling)
|
163
|
+
return contact
|
138
164
|
|
139
165
|
async def by_stanza(self, s) -> LegacyContact:
|
140
166
|
# """
|
@@ -174,18 +200,63 @@ class LegacyRoster(
|
|
174
200
|
"""
|
175
201
|
return _unescape_node(jid_username)
|
176
202
|
|
177
|
-
async def
|
203
|
+
async def _fill(self):
|
204
|
+
try:
|
205
|
+
if hasattr(self.session.xmpp, "TEST_MODE"):
|
206
|
+
# dirty hack to avoid mocking xmpp server replies to this
|
207
|
+
# during tests
|
208
|
+
raise PermissionError
|
209
|
+
iq = await self.session.xmpp["xep_0356"].get_roster(
|
210
|
+
self.session.user_jid.bare
|
211
|
+
)
|
212
|
+
user_roster = iq["roster"]["items"]
|
213
|
+
except (PermissionError, IqError):
|
214
|
+
user_roster = None
|
215
|
+
|
216
|
+
with self.__store.session() as orm:
|
217
|
+
self.__filling = True
|
218
|
+
async for contact in self.fill():
|
219
|
+
if user_roster is None:
|
220
|
+
continue
|
221
|
+
item = contact.get_roster_item()
|
222
|
+
old = user_roster.get(contact.jid.bare)
|
223
|
+
if old is not None and all(
|
224
|
+
old[k] == item[contact.jid.bare].get(k)
|
225
|
+
for k in ("subscription", "groups", "name")
|
226
|
+
):
|
227
|
+
self.log.debug("No need to update roster")
|
228
|
+
continue
|
229
|
+
self.log.debug("Updating roster")
|
230
|
+
try:
|
231
|
+
await self.session.xmpp["xep_0356"].set_roster(
|
232
|
+
self.session.user_jid.bare,
|
233
|
+
item,
|
234
|
+
)
|
235
|
+
except (PermissionError, IqError) as e:
|
236
|
+
warnings.warn(f"Could not add to roster: {e}")
|
237
|
+
else:
|
238
|
+
contact._added_to_roster = True
|
239
|
+
orm.commit()
|
240
|
+
self.__filling = False
|
241
|
+
|
242
|
+
async def fill(self) -> AsyncIterator[LegacyContact]:
|
178
243
|
"""
|
179
244
|
Populate slidge's "virtual roster".
|
180
245
|
|
181
|
-
|
182
|
-
|
183
|
-
|
246
|
+
This should yield contacts that are meant to be added to the user's
|
247
|
+
roster, typically by using ``await self.by_legacy_id(contact_id)``.
|
248
|
+
Setting the contact nicknames, avatar, etc. should be in
|
249
|
+
:meth:`LegacyContact.update_info()`
|
250
|
+
|
251
|
+
It's not mandatory to override this method, but it is recommended way
|
252
|
+
to populate "friends" of the user. Calling
|
253
|
+
``await (await self.by_legacy_id(contact_id)).add_to_roster()``
|
254
|
+
accomplishes the same thing, but doing it in here allows to batch
|
255
|
+
DB queries and is better performance-wise.
|
184
256
|
|
185
|
-
Await ``Contact.add_to_roster()`` in here to add the contact to the
|
186
|
-
user's XMPP roster.
|
187
257
|
"""
|
188
|
-
|
258
|
+
return
|
259
|
+
yield
|
189
260
|
|
190
261
|
|
191
262
|
ESCAPE_TABLE = "".maketrans({v: k for k, v in JID_UNESCAPE_TRANSFORMATIONS.items()})
|