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.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +107 -40
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -804
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.3.dist-info/RECORD +0 -96
  100. slidge-0.1.3.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {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.avatar import AvatarMixin
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(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
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.legacy_id}'/'{self.jid.bare}'>"
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.user.jid
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.__propagate_to_participants(stanza)
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 self.xmpp.MARK_ALL_MESSAGES and is_markable(stanza):
189
- self._sent_order.append(stanza["id"])
190
- stanza["to"] = self.user.jid
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
- 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
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._name = n
232
- self.xmpp.pubsub.set_nick(user=self.user, jid=self.jid.bare, nick=n)
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.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user.jid.bare})
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.__added_to_roster and not force:
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(**kw)
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.__added_to_roster = True
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
- await self.xmpp.pubsub.broadcast_all(JID(self.jid.bare), self.user.jid)
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
- return await self.xmpp["xep_0356"].set_roster(**kw)
495
+ await self.xmpp["xep_0356"].set_roster(**kw)
338
496
  except PermissionError:
339
- return await self.xmpp["xep_0356_old"].set_roster(**kw)
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.user.jid.bare, ptype=ptype) # type: ignore
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
- 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.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._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")
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.user}>"
57
-
58
- def __iter__(self):
59
- return iter(self._contacts_by_legacy_id.values())
60
-
61
- async def __finish_init_contact(
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 {j: c for j, c in self._contacts_by_bare_jid.items() if c.is_friend}
77
- return self._contacts_by_bare_jid
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
- 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
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
- 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
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
- return c
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 fill(self):
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
- 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…
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
- pass
258
+ return
259
+ yield
189
260
 
190
261
 
191
262
  ESCAPE_TABLE = "".maketrans({v: k for k, v in JID_UNESCAPE_TRANSFORMATIONS.items()})