slidge 0.1.3__py3-none-any.whl → 0.2.0a1__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 (74) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -196
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +8 -1
  5. slidge/command/admin.py +6 -7
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -6
  9. slidge/contact/contact.py +165 -49
  10. slidge/contact/roster.py +122 -47
  11. slidge/core/config.py +14 -11
  12. slidge/core/gateway/base.py +148 -36
  13. slidge/core/gateway/caps.py +7 -5
  14. slidge/core/gateway/disco.py +2 -4
  15. slidge/core/gateway/mam.py +1 -4
  16. slidge/core/gateway/muc_admin.py +1 -1
  17. slidge/core/gateway/ping.py +2 -3
  18. slidge/core/gateway/presence.py +1 -1
  19. slidge/core/gateway/registration.py +32 -21
  20. slidge/core/gateway/search.py +3 -5
  21. slidge/core/gateway/session_dispatcher.py +120 -57
  22. slidge/core/gateway/vcard_temp.py +7 -5
  23. slidge/core/mixins/__init__.py +11 -1
  24. slidge/core/mixins/attachment.py +32 -14
  25. slidge/core/mixins/avatar.py +90 -25
  26. slidge/core/mixins/base.py +8 -2
  27. slidge/core/mixins/db.py +18 -0
  28. slidge/core/mixins/disco.py +0 -10
  29. slidge/core/mixins/message.py +18 -8
  30. slidge/core/mixins/message_maker.py +17 -9
  31. slidge/core/mixins/presence.py +17 -4
  32. slidge/core/pubsub.py +54 -220
  33. slidge/core/session.py +69 -34
  34. slidge/db/__init__.py +4 -0
  35. slidge/db/alembic/env.py +64 -0
  36. slidge/db/alembic/script.py.mako +26 -0
  37. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  38. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  39. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  40. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  41. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  42. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  43. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +85 -0
  44. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  45. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
  46. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  47. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  48. slidge/db/avatar.py +235 -0
  49. slidge/db/meta.py +65 -0
  50. slidge/db/models.py +375 -0
  51. slidge/db/store.py +1078 -0
  52. slidge/group/archive.py +58 -14
  53. slidge/group/bookmarks.py +72 -57
  54. slidge/group/participant.py +87 -28
  55. slidge/group/room.py +369 -211
  56. slidge/main.py +201 -0
  57. slidge/migration.py +30 -0
  58. slidge/slixfix/__init__.py +35 -2
  59. slidge/slixfix/roster.py +11 -4
  60. slidge/slixfix/xep_0292/vcard4.py +3 -0
  61. slidge/util/archive_msg.py +2 -1
  62. slidge/util/db.py +1 -47
  63. slidge/util/test.py +71 -4
  64. slidge/util/types.py +29 -4
  65. slidge/util/util.py +22 -0
  66. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/METADATA +4 -4
  67. slidge-0.2.0a1.dist-info/RECORD +114 -0
  68. slidge/core/cache.py +0 -183
  69. slidge/util/schema.sql +0 -126
  70. slidge/util/sql.py +0 -508
  71. slidge-0.1.3.dist-info/RECORD +0 -96
  72. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/LICENSE +0 -0
  73. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/WHEEL +0 -0
  74. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/entry_points.txt +0 -0
slidge/contact/contact.py CHANGED
@@ -2,7 +2,7 @@ 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
6
 
7
7
  from slixmpp import JID, Message, Presence
8
8
  from slixmpp.exceptions import IqError
@@ -10,10 +10,11 @@ from slixmpp.plugins.xep_0292.stanza import VCard4
10
10
  from slixmpp.types import MessageTypes
11
11
 
12
12
  from ..core import config
13
- from ..core.mixins import FullCarbonMixin
14
- from ..core.mixins.avatar import AvatarMixin
13
+ from ..core.mixins import AvatarMixin, FullCarbonMixin, StoredAttributeMixin
14
+ from ..core.mixins.db import UpdateInfoMixin
15
15
  from ..core.mixins.disco import ContactAccountDiscoMixin
16
16
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
17
+ from ..db.models import Contact
17
18
  from ..util import SubclassableOnce
18
19
  from ..util.types import LegacyUserIdType, MessageOrPresenceTypeVar
19
20
 
@@ -24,11 +25,13 @@ if TYPE_CHECKING:
24
25
 
25
26
  class LegacyContact(
26
27
  Generic[LegacyUserIdType],
28
+ StoredAttributeMixin,
27
29
  AvatarMixin,
28
30
  ContactAccountDiscoMixin,
29
31
  FullCarbonMixin,
30
32
  ReactionRecipientMixin,
31
33
  ThreadRecipientMixin,
34
+ UpdateInfoMixin,
32
35
  metaclass=SubclassableOnce,
33
36
  ):
34
37
  """
@@ -81,7 +84,6 @@ class LegacyContact(
81
84
  STRIP_SHORT_DELAY = True
82
85
  _NON_FRIEND_PRESENCES_FILTER = {"subscribe", "unsubscribed"}
83
86
 
84
- _avatar_pubsub_broadcast = True
85
87
  _avatar_bare_jid = True
86
88
 
87
89
  INVITATION_RECIPIENT = True
@@ -100,7 +102,6 @@ class LegacyContact(
100
102
  """
101
103
  super().__init__()
102
104
  self.session = session
103
- self.user = session.user
104
105
  self.legacy_id: LegacyUserIdType = legacy_id
105
106
  """
106
107
  The legacy identifier of the :term:`Legacy Contact`.
@@ -116,19 +117,62 @@ class LegacyContact(
116
117
 
117
118
  self._name: Optional[str] = None
118
119
 
119
- if self.xmpp.MARK_ALL_MESSAGES:
120
- self._sent_order = list[str]()
121
-
122
120
  self.xmpp = session.xmpp
123
121
  self.jid = JID(self.jid_username + "@" + self.xmpp.boundjid.bare)
124
122
  self.jid.resource = self.RESOURCE
125
- self.log = logging.getLogger(f"{self.user.bare_jid}:{self.jid.bare}")
126
- self.participants = set["LegacyParticipant"]()
127
- self.is_friend: bool = False
128
- self.__added_to_roster = False
123
+ self.log = logging.getLogger(f"{self.user_jid.bare}:{self.jid.bare}")
124
+ self._is_friend: bool = False
125
+ self._added_to_roster = False
126
+ self._caps_ver: str | None = None
127
+
128
+ @property
129
+ def is_friend(self):
130
+ return self._is_friend
131
+
132
+ @is_friend.setter
133
+ def is_friend(self, value: bool):
134
+ if value == self._is_friend:
135
+ return
136
+ self._is_friend = value
137
+ if self._updating_info:
138
+ return
139
+ assert self.contact_pk is not None
140
+ self.xmpp.store.contacts.set_friend(self.contact_pk, value)
141
+
142
+ @property
143
+ def added_to_roster(self):
144
+ return self._added_to_roster
145
+
146
+ @added_to_roster.setter
147
+ def added_to_roster(self, value: bool):
148
+ if value == self._added_to_roster:
149
+ return
150
+ self._added_to_roster = value
151
+ if self._updating_info:
152
+ return
153
+ if self.contact_pk is None:
154
+ # during LegacyRoster.fill()
155
+ return
156
+ self.xmpp.store.contacts.set_added_to_roster(self.contact_pk, value)
157
+
158
+ @property
159
+ def participants(self) -> list["LegacyParticipant"]:
160
+ assert self.contact_pk is not None
161
+ from ..group.participant import LegacyParticipant
162
+
163
+ return [
164
+ LegacyParticipant.get_self_or_unique_subclass().from_store(
165
+ self.session, stored, contact=self
166
+ )
167
+ for stored in self.xmpp.store.participants.get_for_contact(self.contact_pk)
168
+ ]
169
+
170
+ @property
171
+ def user_jid(self):
172
+ return self.session.user_jid
129
173
 
130
174
  def __repr__(self):
131
- return f"<Contact '{self.legacy_id}'/'{self.jid.bare}'>"
175
+ return f"<Contact {self.jid.bare} - {self.name or self.legacy_id}'>"
132
176
 
133
177
  def __get_subscription_string(self):
134
178
  if self.is_friend:
@@ -170,11 +214,11 @@ class LegacyContact(
170
214
  ) -> MessageOrPresenceTypeVar:
171
215
  if carbon and isinstance(stanza, Message):
172
216
  stanza["to"] = self.jid.bare
173
- stanza["from"] = self.user.jid
217
+ stanza["from"] = self.user_jid
174
218
  self._privileged_send(stanza)
175
219
  return stanza # type:ignore
176
220
 
177
- if isinstance(stanza, Presence):
221
+ if not self._updating_info and isinstance(stanza, Presence):
178
222
  self.__propagate_to_participants(stanza)
179
223
  if (
180
224
  not self.is_friend
@@ -185,13 +229,18 @@ class LegacyContact(
185
229
  n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
186
230
  n["nick"] = self.name
187
231
  stanza.append(n)
188
- if self.xmpp.MARK_ALL_MESSAGES and is_markable(stanza):
189
- self._sent_order.append(stanza["id"])
190
- stanza["to"] = self.user.jid
232
+ if (
233
+ not self._updating_info
234
+ and self.xmpp.MARK_ALL_MESSAGES
235
+ and is_markable(stanza)
236
+ ):
237
+ assert self.contact_pk is not None
238
+ self.xmpp.store.contacts.add_to_sent(self.contact_pk, stanza["id"])
239
+ stanza["to"] = self.user_jid
191
240
  stanza.send()
192
241
  return stanza
193
242
 
194
- def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str):
243
+ def get_msg_xmpp_id_up_to(self, horizon_xmpp_id: str) -> list[str]:
195
244
  """
196
245
  Return XMPP msg ids sent by this contact up to a given XMPP msg id.
197
246
 
@@ -205,15 +254,8 @@ class LegacyContact(
205
254
  :param horizon_xmpp_id: The latest message
206
255
  :return: A list of XMPP ids or None if horizon_xmpp_id was not found
207
256
  """
208
- for i, xmpp_id in enumerate(self._sent_order):
209
- if xmpp_id == horizon_xmpp_id:
210
- break
211
- else:
212
- return
213
- i += 1
214
- res = self._sent_order[:i]
215
- self._sent_order = self._sent_order[i:]
216
- return res
257
+ assert self.contact_pk is not None
258
+ return self.xmpp.store.contacts.pop_sent_up_to(self.contact_pk, horizon_xmpp_id)
217
259
 
218
260
  @property
219
261
  def name(self):
@@ -226,12 +268,33 @@ class LegacyContact(
226
268
  def name(self, n: Optional[str]):
227
269
  if self._name == n:
228
270
  return
271
+ self._name = n
272
+ if self.is_friend and self.added_to_roster:
273
+ self.xmpp.pubsub.broadcast_nick(
274
+ user_jid=self.user_jid, jid=self.jid.bare, nick=n
275
+ )
276
+ if self._updating_info:
277
+ # means we're in update_info(), so no participants, and no need
278
+ # to write to DB now, it will be called in Roster.__finish_init_contact
279
+ return
229
280
  for p in self.participants:
230
281
  p.nickname = n
231
- self._name = n
232
- self.xmpp.pubsub.set_nick(user=self.user, jid=self.jid.bare, nick=n)
282
+ assert self.contact_pk is not None
283
+ self.xmpp.store.contacts.update_nick(self.contact_pk, n)
284
+
285
+ def _get_cached_avatar_id(self) -> Optional[str]:
286
+ if self.contact_pk is None:
287
+ return None
288
+ return self.xmpp.store.contacts.get_avatar_legacy_id(self.contact_pk)
233
289
 
234
290
  def _post_avatar_update(self):
291
+ if self._updating_info:
292
+ return
293
+ if self.contact_pk is None:
294
+ # happens in LegacyRoster.fill(), the contact primary key is not
295
+ # set yet, but this will eventually be called in LegacyRoster.__finish_init_contact
296
+ return
297
+ self.xmpp.store.contacts.set_avatar(self.contact_pk, self._avatar_pk)
235
298
  for p in self.participants:
236
299
  self.log.debug("Propagating new avatar to %s", p.muc)
237
300
  p.send_last_presence(force=True, no_cache_online=True)
@@ -283,7 +346,16 @@ class LegacyContact(
283
346
  elif country:
284
347
  vcard.add_address(country, locality)
285
348
 
286
- self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user.jid.bare})
349
+ self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user_jid.bare})
350
+
351
+ def get_roster_item(self):
352
+ item = {
353
+ "subscription": self.__get_subscription_string(),
354
+ "groups": [self.xmpp.ROSTER_GROUP],
355
+ }
356
+ if (n := self.name) is not None:
357
+ item["name"] = n
358
+ return {self.jid.bare: item}
287
359
 
288
360
  async def add_to_roster(self, force=False):
289
361
  """
@@ -291,23 +363,15 @@ class LegacyContact(
291
363
 
292
364
  :param force: add even if the contact was already added successfully
293
365
  """
294
- if self.__added_to_roster and not force:
366
+ if self.added_to_roster and not force:
295
367
  return
296
368
  if config.NO_ROSTER_PUSH:
297
369
  log.debug("Roster push request by plugin ignored (--no-roster-push)")
298
370
  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
371
  try:
310
- await self._set_roster(**kw)
372
+ await self._set_roster(
373
+ jid=self.user_jid, roster_items=self.get_roster_item()
374
+ )
311
375
  except PermissionError:
312
376
  warnings.warn(
313
377
  "Slidge does not have privileges to add contacts to the roster. Refer"
@@ -325,18 +389,33 @@ class LegacyContact(
325
389
  else:
326
390
  # we only broadcast pubsub events for contacts added to the roster
327
391
  # so if something was set before, we need to push it now
328
- self.__added_to_roster = True
329
- self.session.create_task(self.__broadcast_pubsub_items())
392
+ self.added_to_roster = True
330
393
  self.send_last_presence()
331
394
 
332
395
  async def __broadcast_pubsub_items(self):
333
- await self.xmpp.pubsub.broadcast_all(JID(self.jid.bare), self.user.jid)
396
+ if not self.is_friend:
397
+ return
398
+ if not self.added_to_roster:
399
+ return
400
+ cached_avatar = self.get_cached_avatar()
401
+ if cached_avatar is not None:
402
+ await self.xmpp.pubsub.broadcast_avatar(
403
+ self.jid.bare, self.session.user_jid, cached_avatar
404
+ )
405
+ nick = self.name
406
+
407
+ if nick is not None:
408
+ self.xmpp.pubsub.broadcast_nick(
409
+ self.session.user_jid,
410
+ self.jid.bare,
411
+ nick,
412
+ )
334
413
 
335
414
  async def _set_roster(self, **kw):
336
415
  try:
337
- return await self.xmpp["xep_0356"].set_roster(**kw)
416
+ await self.xmpp["xep_0356"].set_roster(**kw)
338
417
  except PermissionError:
339
- return await self.xmpp["xep_0356_old"].set_roster(**kw)
418
+ await self.xmpp["xep_0356_old"].set_roster(**kw)
340
419
 
341
420
  def send_friend_request(self, text: Optional[str] = None):
342
421
  presence = self._make_presence(ptype="subscribe", pstatus=text, bare=True)
@@ -350,6 +429,8 @@ class LegacyContact(
350
429
  :param text: Optional message from the friend to the user
351
430
  """
352
431
  self.is_friend = True
432
+ self.added_to_roster = True
433
+ assert self.contact_pk is not None
353
434
  self.log.debug("Accepting friend request")
354
435
  presence = self._make_presence(ptype="subscribed", pstatus=text, bare=True)
355
436
  self._send(presence, nick=True)
@@ -415,7 +496,7 @@ class LegacyContact(
415
496
  their 'friends'".
416
497
  """
417
498
  for ptype in "unsubscribe", "unsubscribed", "unavailable":
418
- self.xmpp.send_presence(pfrom=self.jid, pto=self.user.jid.bare, ptype=ptype) # type: ignore
499
+ self.xmpp.send_presence(pfrom=self.jid, pto=self.user_jid.bare, ptype=ptype) # type: ignore
419
500
 
420
501
  async def update_info(self):
421
502
  """
@@ -442,6 +523,41 @@ class LegacyContact(
442
523
  """
443
524
  pass
444
525
 
526
+ def _make_presence(
527
+ self,
528
+ *,
529
+ last_seen: Optional[datetime.datetime] = None,
530
+ status_codes: Optional[set[int]] = None,
531
+ user_full_jid: Optional[JID] = None,
532
+ **presence_kwargs,
533
+ ):
534
+ p = super()._make_presence(last_seen=last_seen, **presence_kwargs)
535
+ caps = self.xmpp.plugin["xep_0115"]
536
+ if p.get_from().resource and self._caps_ver:
537
+ p["caps"]["node"] = caps.caps_node
538
+ p["caps"]["hash"] = caps.hash
539
+ p["caps"]["ver"] = self._caps_ver
540
+ return p
541
+
542
+ @classmethod
543
+ def from_store(cls, session, stored: Contact, *args, **kwargs) -> Self:
544
+ contact = cls(
545
+ session,
546
+ cls.xmpp.LEGACY_CONTACT_ID_TYPE(stored.legacy_id),
547
+ stored.jid.username, # type: ignore
548
+ *args, # type: ignore
549
+ **kwargs, # type: ignore
550
+ )
551
+ contact.contact_pk = stored.id
552
+ contact._name = stored.nick
553
+ contact._is_friend = stored.is_friend
554
+ contact.added_to_roster = stored.added_to_roster
555
+ if (data := stored.extra_attributes) is not None:
556
+ contact.deserialize_extra_attributes(data)
557
+ contact._set_avatar_from_store(stored)
558
+ contact._caps_ver = stored.caps_ver
559
+ return contact
560
+
445
561
 
446
562
  def is_markable(stanza: Union[Message, Presence]):
447
563
  if isinstance(stanza, Presence):
slidge/contact/roster.py CHANGED
@@ -1,11 +1,14 @@
1
1
  import asyncio
2
2
  import logging
3
- from typing import TYPE_CHECKING, Generic, Optional, Type
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.store import ContactStore
9
12
  from ..util import SubclassableOnce
10
13
  from ..util.types import LegacyContactType, LegacyUserIdType
11
14
  from .contact import LegacyContact
@@ -43,38 +46,58 @@ class LegacyRoster(
43
46
  LegacyContact.get_self_or_unique_subclass()
44
47
  )
45
48
  self._contact_cls.xmpp = session.xmpp
49
+ self.__store: ContactStore = session.xmpp.store.contacts
46
50
 
47
51
  self.session = session
48
- self._contacts_by_bare_jid: dict[str, LegacyContactType] = {}
49
- self._contacts_by_legacy_id: dict[LegacyUserIdType, LegacyContactType] = {}
50
- self.log = logging.getLogger(f"{self.session.user.bare_jid}:roster")
52
+ self.log = logging.getLogger(f"{self.session.user_jid.bare}:roster")
51
53
  self.user_legacy_id: Optional[LegacyUserIdType] = None
52
54
  self.ready: asyncio.Future[bool] = self.session.xmpp.loop.create_future()
55
+ self.__filling = False
53
56
  super().__init__()
54
57
 
55
58
  def __repr__(self):
56
- return f"<Roster of {self.session.user}>"
59
+ return f"<Roster of {self.session.user_jid}>"
57
60
 
58
- def __iter__(self):
59
- return iter(self._contacts_by_legacy_id.values())
61
+ def __iter__(self) -> Iterator[LegacyContactType]:
62
+ with self.__store.session():
63
+ for stored in self.__store.get_all(user_pk=self.session.user_pk):
64
+ yield self._contact_cls.from_store(self.session, stored)
60
65
 
61
66
  async def __finish_init_contact(
62
67
  self, legacy_id: LegacyUserIdType, jid_username: str, *args, **kwargs
63
68
  ):
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
69
+ async with self.lock(("finish", legacy_id)):
70
+ with self.__store.session():
71
+ stored = self.__store.get_by_legacy_id(
72
+ self.session.user_pk, str(legacy_id)
73
+ )
74
+ if stored is not None:
75
+ if stored.updated:
76
+ return self._contact_cls.from_store(self.session, stored)
77
+ c: LegacyContact = self._contact_cls(
78
+ self.session, legacy_id, jid_username, *args, **kwargs
79
+ )
80
+ c.contact_pk = stored.id
81
+ else:
82
+ c = self._contact_cls(
83
+ self.session, legacy_id, jid_username, *args, **kwargs
84
+ )
85
+ try:
86
+ with c.updating_info():
87
+ await c.avatar_wrap_update_info()
88
+ except Exception as e:
89
+ raise XMPPError("internal-server-error", str(e))
90
+ c._caps_ver = await c.get_caps_ver(c.jid)
91
+ need_avatar = c.contact_pk is None
92
+ c.contact_pk = self.__store.update(c, commit=not self.__filling)
93
+ if need_avatar:
94
+ c._post_avatar_update()
72
95
  return c
73
96
 
74
97
  def known_contacts(self, only_friends=True) -> dict[str, LegacyContactType]:
75
98
  if only_friends:
76
- return {j: c for j, c in self._contacts_by_bare_jid.items() if c.is_friend}
77
- return self._contacts_by_bare_jid
99
+ return {c.jid.bare: c for c in self if c.is_friend}
100
+ return {c.jid.bare: c for c in self}
78
101
 
79
102
  async def by_jid(self, contact_jid: JID) -> LegacyContactType:
80
103
  # """
@@ -89,16 +112,17 @@ class LegacyRoster(
89
112
  # """
90
113
  username = contact_jid.node
91
114
  async with self.lock(("username", username)):
92
- bare = contact_jid.bare
93
- c = self._contacts_by_bare_jid.get(bare)
94
- if c is None:
95
- legacy_id = await self.jid_username_to_legacy_id(username)
96
- log.debug("Contact %s not found", contact_jid)
97
- if self.get_lock(("legacy_id", legacy_id)):
98
- log.debug("Already updating %s", contact_jid)
99
- return await self.by_legacy_id(legacy_id)
100
- c = await self.__finish_init_contact(legacy_id, username)
101
- return c
115
+ with self.__store.session():
116
+ stored = self.__store.get_by_jid(self.session.user_pk, contact_jid)
117
+ if stored is not None and stored.updated:
118
+ return self._contact_cls.from_store(self.session, stored)
119
+
120
+ legacy_id = await self.jid_username_to_legacy_id(username)
121
+ log.debug("Contact %s not found", contact_jid)
122
+ if self.get_lock(("legacy_id", legacy_id)):
123
+ log.debug("Already updating %s", contact_jid)
124
+ return await self.by_legacy_id(legacy_id)
125
+ return await self.__finish_init_contact(legacy_id, username)
102
126
 
103
127
  async def by_legacy_id(
104
128
  self, legacy_id: LegacyUserIdType, *args, **kwargs
@@ -121,20 +145,26 @@ class LegacyRoster(
121
145
  if legacy_id == self.user_legacy_id:
122
146
  raise ContactIsUser
123
147
  async with self.lock(("legacy_id", legacy_id)):
124
- c = self._contacts_by_legacy_id.get(legacy_id)
125
- if c is None:
126
- username = await self.legacy_id_to_jid_username(legacy_id)
127
- log.debug("Contact %s not found", legacy_id)
128
- if self.get_lock(("username", username)):
129
- log.debug("Already updating %s", username)
130
- jid = JID()
131
- jid.node = username
132
- jid.domain = self.session.xmpp.boundjid.bare
133
- return await self.by_jid(jid)
134
- c = await self.__finish_init_contact(
135
- legacy_id, username, *args, **kwargs
148
+ with self.__store.session():
149
+ stored = self.__store.get_by_legacy_id(
150
+ self.session.user_pk, str(legacy_id)
136
151
  )
137
- return c
152
+ if stored is not None and stored.updated:
153
+ return self._contact_cls.from_store(
154
+ self.session, stored, *args, **kwargs
155
+ )
156
+
157
+ username = await self.legacy_id_to_jid_username(legacy_id)
158
+ log.debug("Contact %s not found", legacy_id)
159
+ if self.get_lock(("username", username)):
160
+ log.debug("Already updating %s", username)
161
+ jid = JID()
162
+ jid.node = username
163
+ jid.domain = self.session.xmpp.boundjid.bare
164
+ return await self.by_jid(jid)
165
+ return await self.__finish_init_contact(
166
+ legacy_id, username, *args, **kwargs
167
+ )
138
168
 
139
169
  async def by_stanza(self, s) -> LegacyContact:
140
170
  # """
@@ -174,18 +204,63 @@ class LegacyRoster(
174
204
  """
175
205
  return _unescape_node(jid_username)
176
206
 
177
- async def fill(self):
207
+ async def _fill(self):
208
+ try:
209
+ if hasattr(self.session.xmpp, "TEST_MODE"):
210
+ # dirty hack to avoid mocking xmpp server replies to this
211
+ # during tests
212
+ raise PermissionError
213
+ iq = await self.session.xmpp["xep_0356"].get_roster(
214
+ self.session.user_jid.bare
215
+ )
216
+ user_roster = iq["roster"]["items"]
217
+ except (PermissionError, IqError):
218
+ user_roster = None
219
+
220
+ with self.__store.session() as orm:
221
+ self.__filling = True
222
+ async for contact in self.fill():
223
+ if user_roster is None:
224
+ continue
225
+ item = contact.get_roster_item()
226
+ old = user_roster.get(contact.jid.bare)
227
+ if old is not None and all(
228
+ old[k] == item[contact.jid.bare][k]
229
+ for k in ("subscription", "groups", "name")
230
+ ):
231
+ self.log.debug("No need to update roster")
232
+ continue
233
+ self.log.debug("Updating roster")
234
+ try:
235
+ await self.session.xmpp["xep_0356"].set_roster(
236
+ self.session.user_jid.bare,
237
+ item,
238
+ )
239
+ except (PermissionError, IqError) as e:
240
+ warnings.warn(f"Could not add to roster: {e}")
241
+ else:
242
+ contact._added_to_roster = True
243
+ orm.commit()
244
+ self.__filling = False
245
+
246
+ async def fill(self) -> AsyncIterator[LegacyContact]:
178
247
  """
179
248
  Populate slidge's "virtual roster".
180
249
 
181
- Override this and in it, ``await self.by_legacy_id(contact_id)``
182
- for the every legacy contacts of the user for which you'd like to
183
- set an avatar, nickname, vcard…
250
+ This should yield contacts that are meant to be added to the user's
251
+ roster, typically by using ``await self.by_legacy_id(contact_id)``.
252
+ Setting the contact nicknames, avatar, etc. should be in
253
+ :meth:`LegacyContact.update_info()`
254
+
255
+ It's not mandatory to override this method, but it is recommended way
256
+ to populate "friends" of the user. Calling
257
+ ``await (await self.by_legacy_id(contact_id)).add_to_roster()``
258
+ accomplishes the same thing, but doing it in here allows to batch
259
+ DB queries and is better performance-wise.
184
260
 
185
- Await ``Contact.add_to_roster()`` in here to add the contact to the
186
- user's XMPP roster.
187
261
  """
188
- pass
262
+ return
263
+ yield
189
264
 
190
265
 
191
266
  ESCAPE_TABLE = "".maketrans({v: k for k, v in JID_UNESCAPE_TRANSFORMATIONS.items()})
slidge/core/config.py CHANGED
@@ -43,11 +43,18 @@ PORT__SHORT = "p"
43
43
 
44
44
  HOME_DIR: Path
45
45
  HOME_DIR__DOC = (
46
- "Shelve file used to store persistent user data. "
46
+ "Directory where slidge will writes it persistent data and cache. "
47
47
  "Defaults to /var/lib/slidge/${SLIDGE_JID}. "
48
48
  )
49
49
  HOME_DIR__DYNAMIC_DEFAULT = True
50
50
 
51
+ DB_URL: str
52
+ DB_URL__DOC = (
53
+ "Database URL, see <https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls>. "
54
+ "Defaults to sqlite:///${HOME_DIR}/slidge.sqlite"
55
+ )
56
+ DB_URL__DYNAMIC_DEFAULT = True
57
+
51
58
  USER_JID_VALIDATOR: str
52
59
  USER_JID_VALIDATOR__DOC = (
53
60
  "Regular expression to restrict users that can register to the gateway, by JID. "
@@ -70,7 +77,7 @@ UPLOAD_SERVICE__DOC = (
70
77
  )
71
78
 
72
79
  SECRET_KEY: Optional[str] = None
73
- SECRET_KEY__DOC = "Encryption for disk storage"
80
+ SECRET_KEY__DOC = "Encryption for disk storage. Deprecated."
74
81
 
75
82
  NO_ROSTER_PUSH = False
76
83
  NO_ROSTER_PUSH__DOC = "Do not fill users' rosters with legacy contacts automatically"
@@ -135,11 +142,13 @@ PARTIAL_REGISTRATION_TIMEOUT__DOC = (
135
142
  "a single step registration process is not enough."
136
143
  )
137
144
 
138
- LAST_SEEN_FALLBACK = True
145
+ LAST_SEEN_FALLBACK = False
139
146
  LAST_SEEN_FALLBACK__DOC = (
140
147
  "When using XEP-0319 (Last User Interaction in Presence), use the presence status"
141
148
  " to display the last seen information in the presence status. Useful for clients"
142
- " that do not implement XEP-0319."
149
+ " that do not implement XEP-0319. Because of implementation details, this can increase"
150
+ " RAM usage and might be deprecated in the future. Ask your client dev for XEP-0319"
151
+ " support ;o)."
143
152
  )
144
153
 
145
154
  QR_TIMEOUT = 60
@@ -176,10 +185,7 @@ LOG_FORMAT__DOC = (
176
185
  )
177
186
 
178
187
  MAM_MAX_DAYS = 7
179
- MAM_MAX_DAYS__DOC = (
180
- "Maximum number of days for group archive retention. "
181
- "Since all text content stored in RAM right now, "
182
- )
188
+ MAM_MAX_DAYS__DOC = "Maximum number of days for group archive retention."
183
189
 
184
190
  CORRECTION_EMPTY_BODY_AS_RETRACTION = True
185
191
  CORRECTION_EMPTY_BODY_AS_RETRACTION__DOC = (
@@ -211,6 +217,3 @@ DEV_MODE__DOC = (
211
217
  "Enables an interactive python shell via chat commands, for admins."
212
218
  "Not safe to use in prod, but great during dev."
213
219
  )
214
-
215
- SYNC_AVATAR = True
216
- SYNC_AVATAR__DOC = "Sync the user XMPP avatar to legacy network (if supported)."