slidge 0.2.0a0__tar.gz → 0.2.0a1__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. {slidge-0.2.0a0 → slidge-0.2.0a1}/PKG-INFO +2 -4
  2. {slidge-0.2.0a0 → slidge-0.2.0a1}/pyproject.toml +2 -2
  3. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/__version__.py +1 -1
  4. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/command/admin.py +1 -1
  5. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/command/user.py +0 -1
  6. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/contact/contact.py +86 -32
  7. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/contact/roster.py +79 -19
  8. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/config.py +1 -4
  9. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/base.py +9 -2
  10. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/caps.py +7 -5
  11. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/muc_admin.py +1 -1
  12. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/session_dispatcher.py +20 -6
  13. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/vcard_temp.py +1 -1
  14. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/attachment.py +17 -4
  15. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/avatar.py +26 -9
  16. slidge-0.2.0a1/slidge/core/mixins/db.py +18 -0
  17. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/disco.py +0 -10
  18. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/message.py +7 -1
  19. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/presence.py +6 -3
  20. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/pubsub.py +7 -15
  21. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/session.py +6 -3
  22. slidge-0.2.0a1/slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  23. slidge-0.2.0a1/slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  24. slidge-0.2.0a1/slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  25. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +11 -2
  26. slidge-0.2.0a1/slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
  27. slidge-0.2.0a1/slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  28. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/avatar.py +20 -9
  29. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/models.py +16 -6
  30. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/store.py +217 -115
  31. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/group/archive.py +46 -1
  32. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/group/bookmarks.py +17 -5
  33. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/group/participant.py +10 -3
  34. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/group/room.py +183 -125
  35. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/main.py +3 -3
  36. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0292/vcard4.py +2 -0
  37. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/util/archive_msg.py +2 -1
  38. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/util/test.py +54 -4
  39. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/util/types.py +5 -0
  40. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/util/util.py +22 -0
  41. {slidge-0.2.0a0 → slidge-0.2.0a1}/LICENSE +0 -0
  42. {slidge-0.2.0a0 → slidge-0.2.0a1}/README.md +0 -0
  43. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/__init__.py +0 -0
  44. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/__main__.py +0 -0
  45. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/command/__init__.py +0 -0
  46. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/command/adhoc.py +0 -0
  47. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/command/base.py +0 -0
  48. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/command/categories.py +0 -0
  49. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/command/chat_command.py +0 -0
  50. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/command/register.py +0 -0
  51. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/contact/__init__.py +0 -0
  52. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/__init__.py +0 -0
  53. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/__init__.py +0 -0
  54. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/delivery_receipt.py +0 -0
  55. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/disco.py +0 -0
  56. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/mam.py +0 -0
  57. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/ping.py +0 -0
  58. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/presence.py +0 -0
  59. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/registration.py +0 -0
  60. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/gateway/search.py +0 -0
  61. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/__init__.py +0 -0
  62. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/base.py +0 -0
  63. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/lock.py +0 -0
  64. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/message_maker.py +0 -0
  65. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/core/mixins/recipient.py +0 -0
  66. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/__init__.py +0 -0
  67. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/alembic/env.py +0 -0
  68. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/alembic/script.py.mako +0 -0
  69. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +0 -0
  70. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +0 -0
  71. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +0 -0
  72. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +0 -0
  73. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +0 -0
  74. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/db/meta.py +0 -0
  75. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/group/__init__.py +0 -0
  76. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/migration.py +0 -0
  77. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/py.typed +0 -0
  78. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/__init__.py +0 -0
  79. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/link_preview/__init__.py +0 -0
  80. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/link_preview/link_preview.py +0 -0
  81. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/link_preview/stanza.py +0 -0
  82. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/roster.py +0 -0
  83. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0077/__init__.py +0 -0
  84. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0077/register.py +0 -0
  85. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0077/stanza.py +0 -0
  86. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0100/__init__.py +0 -0
  87. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0100/gateway.py +0 -0
  88. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0100/stanza.py +0 -0
  89. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0153/__init__.py +0 -0
  90. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0153/stanza.py +0 -0
  91. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0153/vcard_avatar.py +0 -0
  92. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0264/__init__.py +0 -0
  93. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0264/stanza.py +0 -0
  94. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0264/thumbnail.py +0 -0
  95. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0292/__init__.py +0 -0
  96. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0313/__init__.py +0 -0
  97. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0313/mam.py +0 -0
  98. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0313/stanza.py +0 -0
  99. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0317/__init__.py +0 -0
  100. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0317/hats.py +0 -0
  101. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0317/stanza.py +0 -0
  102. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0356_old/__init__.py +0 -0
  103. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0356_old/privilege.py +0 -0
  104. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0356_old/stanza.py +0 -0
  105. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0424/__init__.py +0 -0
  106. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0424/retraction.py +0 -0
  107. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0424/stanza.py +0 -0
  108. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0490/__init__.py +0 -0
  109. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0490/mds.py +0 -0
  110. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/slixfix/xep_0490/stanza.py +0 -0
  111. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/util/__init__.py +0 -0
  112. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/util/conf.py +0 -0
  113. {slidge-0.2.0a0 → slidge-0.2.0a1}/slidge/util/db.py +0 -0
@@ -1,16 +1,14 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: slidge
3
- Version: 0.2.0a0
3
+ Version: 0.2.0a1
4
4
  Summary: XMPP bridging framework
5
5
  Home-page: https://sr.ht/~nicoco/slidge/
6
6
  License: AGPL-3.0-or-later
7
7
  Author: Nicolas Cedilnik
8
8
  Author-email: nicoco@nicoco.fr
9
- Requires-Python: >=3.9,<4.0
9
+ Requires-Python: >=3.11,<4.0
10
10
  Classifier: License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)
11
11
  Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
12
  Classifier: Programming Language :: Python :: 3.11
15
13
  Requires-Dist: ConfigArgParse (>=1.5.3,<2.0.0)
16
14
  Requires-Dist: Pillow (>=10,<11)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "slidge"
3
- version = "0.2.0alpha0"
3
+ version = "0.2.0alpha1"
4
4
  description = "XMPP bridging framework"
5
5
  authors = ["Nicolas Cedilnik <nicoco@nicoco.fr>"]
6
6
  readme = "README.md"
@@ -11,7 +11,7 @@ documentation = "https://slidge.im/"
11
11
  include = ["slidge/util/schema.sql"]
12
12
 
13
13
  [tool.poetry.dependencies]
14
- python = "^3.9"
14
+ python = "^3.11"
15
15
  qrcode = "^7.4.1"
16
16
  Pillow = "^10"
17
17
  aiohttp = {version = "^3.8.3", extras = ["speedups"]}
@@ -2,4 +2,4 @@ from slidge.util.util import get_version # noqa: F401
2
2
 
3
3
  # this is modified before publish, but if someone cloned from the repo,
4
4
  # it can help
5
- __version__ = "0.2.0alpha0"
5
+ __version__ = "0.2.0alpha1"
@@ -55,7 +55,7 @@ class SlidgeInfo(AdminCommand):
55
55
  async def run(self, _session, _ifrom, *_):
56
56
  from slidge.__version__ import __version__
57
57
 
58
- start = self.xmpp.datetime_started
58
+ start = self.xmpp.datetime_started # type:ignore
59
59
  uptime = datetime.now() - start
60
60
 
61
61
  if uptime.days:
@@ -131,7 +131,6 @@ class ListContacts(Command):
131
131
  self, session: Optional[AnyBaseSession], _ifrom: JID, *_
132
132
  ) -> TableResult:
133
133
  assert session is not None
134
- await session.contacts.fill()
135
134
  contacts = sorted(
136
135
  session.contacts, key=lambda c: c.name.casefold() if c.name else ""
137
136
  )
@@ -11,6 +11,7 @@ from slixmpp.types import MessageTypes
11
11
 
12
12
  from ..core import config
13
13
  from ..core.mixins import AvatarMixin, FullCarbonMixin, StoredAttributeMixin
14
+ from ..core.mixins.db import UpdateInfoMixin
14
15
  from ..core.mixins.disco import ContactAccountDiscoMixin
15
16
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
16
17
  from ..db.models import Contact
@@ -30,6 +31,7 @@ class LegacyContact(
30
31
  FullCarbonMixin,
31
32
  ReactionRecipientMixin,
32
33
  ThreadRecipientMixin,
34
+ UpdateInfoMixin,
33
35
  metaclass=SubclassableOnce,
34
36
  ):
35
37
  """
@@ -82,7 +84,6 @@ class LegacyContact(
82
84
  STRIP_SHORT_DELAY = True
83
85
  _NON_FRIEND_PRESENCES_FILTER = {"subscribe", "unsubscribed"}
84
86
 
85
- _avatar_pubsub_broadcast = True
86
87
  _avatar_bare_jid = True
87
88
 
88
89
  INVITATION_RECIPIENT = True
@@ -121,7 +122,8 @@ class LegacyContact(
121
122
  self.jid.resource = self.RESOURCE
122
123
  self.log = logging.getLogger(f"{self.user_jid.bare}:{self.jid.bare}")
123
124
  self._is_friend: bool = False
124
- self.added_to_roster = False
125
+ self._added_to_roster = False
126
+ self._caps_ver: str | None = None
125
127
 
126
128
  @property
127
129
  def is_friend(self):
@@ -132,9 +134,27 @@ class LegacyContact(
132
134
  if value == self._is_friend:
133
135
  return
134
136
  self._is_friend = value
137
+ if self._updating_info:
138
+ return
135
139
  assert self.contact_pk is not None
136
140
  self.xmpp.store.contacts.set_friend(self.contact_pk, value)
137
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
+
138
158
  @property
139
159
  def participants(self) -> list["LegacyParticipant"]:
140
160
  assert self.contact_pk is not None
@@ -152,7 +172,7 @@ class LegacyContact(
152
172
  return self.session.user_jid
153
173
 
154
174
  def __repr__(self):
155
- return f"<Contact '{self.legacy_id}'/'{self.jid.bare}'>"
175
+ return f"<Contact {self.jid.bare} - {self.name or self.legacy_id}'>"
156
176
 
157
177
  def __get_subscription_string(self):
158
178
  if self.is_friend:
@@ -198,7 +218,7 @@ class LegacyContact(
198
218
  self._privileged_send(stanza)
199
219
  return stanza # type:ignore
200
220
 
201
- if isinstance(stanza, Presence):
221
+ if not self._updating_info and isinstance(stanza, Presence):
202
222
  self.__propagate_to_participants(stanza)
203
223
  if (
204
224
  not self.is_friend
@@ -209,7 +229,11 @@ class LegacyContact(
209
229
  n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
210
230
  n["nick"] = self.name
211
231
  stanza.append(n)
212
- if self.xmpp.MARK_ALL_MESSAGES and is_markable(stanza):
232
+ if (
233
+ not self._updating_info
234
+ and self.xmpp.MARK_ALL_MESSAGES
235
+ and is_markable(stanza)
236
+ ):
213
237
  assert self.contact_pk is not None
214
238
  self.xmpp.store.contacts.add_to_sent(self.contact_pk, stanza["id"])
215
239
  stanza["to"] = self.user_jid
@@ -244,21 +268,32 @@ class LegacyContact(
244
268
  def name(self, n: Optional[str]):
245
269
  if self._name == n:
246
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
247
280
  for p in self.participants:
248
281
  p.nickname = n
249
- self._name = n
250
282
  assert self.contact_pk is not None
251
283
  self.xmpp.store.contacts.update_nick(self.contact_pk, n)
252
- self.xmpp.pubsub.broadcast_nick(
253
- user_jid=self.user_jid, jid=self.jid.bare, nick=n
254
- )
255
284
 
256
- def _get_cached_avatar_id(self):
257
- assert self.contact_pk is not None
285
+ def _get_cached_avatar_id(self) -> Optional[str]:
286
+ if self.contact_pk is None:
287
+ return None
258
288
  return self.xmpp.store.contacts.get_avatar_legacy_id(self.contact_pk)
259
289
 
260
290
  def _post_avatar_update(self):
261
- assert self.contact_pk is not None
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
262
297
  self.xmpp.store.contacts.set_avatar(self.contact_pk, self._avatar_pk)
263
298
  for p in self.participants:
264
299
  self.log.debug("Propagating new avatar to %s", p.muc)
@@ -313,6 +348,15 @@ class LegacyContact(
313
348
 
314
349
  self.xmpp.vcard.set_vcard(self.jid.bare, vcard, {self.user_jid.bare})
315
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}
359
+
316
360
  async def add_to_roster(self, force=False):
317
361
  """
318
362
  Add this contact to the user roster using :xep:`0356`
@@ -324,18 +368,10 @@ class LegacyContact(
324
368
  if config.NO_ROSTER_PUSH:
325
369
  log.debug("Roster push request by plugin ignored (--no-roster-push)")
326
370
  return
327
- item = {
328
- "subscription": self.__get_subscription_string(),
329
- "groups": [self.xmpp.ROSTER_GROUP],
330
- }
331
- if (n := self.name) is not None:
332
- item["name"] = n
333
- kw = dict(
334
- jid=self.user_jid,
335
- roster_items={self.jid.bare: item},
336
- )
337
371
  try:
338
- await self._set_roster(**kw)
372
+ await self._set_roster(
373
+ jid=self.user_jid, roster_items=self.get_roster_item()
374
+ )
339
375
  except PermissionError:
340
376
  warnings.warn(
341
377
  "Slidge does not have privileges to add contacts to the roster. Refer"
@@ -354,31 +390,32 @@ class LegacyContact(
354
390
  # we only broadcast pubsub events for contacts added to the roster
355
391
  # so if something was set before, we need to push it now
356
392
  self.added_to_roster = True
357
- self.session.create_task(self.__broadcast_pubsub_items())
358
393
  self.send_last_presence()
359
394
 
360
395
  async def __broadcast_pubsub_items(self):
396
+ if not self.is_friend:
397
+ return
398
+ if not self.added_to_roster:
399
+ return
361
400
  cached_avatar = self.get_cached_avatar()
362
401
  if cached_avatar is not None:
363
402
  await self.xmpp.pubsub.broadcast_avatar(
364
403
  self.jid.bare, self.session.user_jid, cached_avatar
365
404
  )
366
405
  nick = self.name
367
- from ..core.pubsub import PepNick
368
406
 
369
407
  if nick is not None:
370
- pep_nick = PepNick(nick)
371
- await self.xmpp.pubsub.broadcast(
372
- pep_nick.nick,
373
- self.jid.bare,
408
+ self.xmpp.pubsub.broadcast_nick(
374
409
  self.session.user_jid,
410
+ self.jid.bare,
411
+ nick,
375
412
  )
376
413
 
377
414
  async def _set_roster(self, **kw):
378
415
  try:
379
- return await self.xmpp["xep_0356"].set_roster(**kw)
416
+ await self.xmpp["xep_0356"].set_roster(**kw)
380
417
  except PermissionError:
381
- return await self.xmpp["xep_0356_old"].set_roster(**kw)
418
+ await self.xmpp["xep_0356_old"].set_roster(**kw)
382
419
 
383
420
  def send_friend_request(self, text: Optional[str] = None):
384
421
  presence = self._make_presence(ptype="subscribe", pstatus=text, bare=True)
@@ -392,8 +429,8 @@ class LegacyContact(
392
429
  :param text: Optional message from the friend to the user
393
430
  """
394
431
  self.is_friend = True
432
+ self.added_to_roster = True
395
433
  assert self.contact_pk is not None
396
- self.xmpp.store.contacts.set_friend(self.contact_pk, True)
397
434
  self.log.debug("Accepting friend request")
398
435
  presence = self._make_presence(ptype="subscribed", pstatus=text, bare=True)
399
436
  self._send(presence, nick=True)
@@ -486,6 +523,22 @@ class LegacyContact(
486
523
  """
487
524
  pass
488
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
+
489
542
  @classmethod
490
543
  def from_store(cls, session, stored: Contact, *args, **kwargs) -> Self:
491
544
  contact = cls(
@@ -502,6 +555,7 @@ class LegacyContact(
502
555
  if (data := stored.extra_attributes) is not None:
503
556
  contact.deserialize_extra_attributes(data)
504
557
  contact._set_avatar_from_store(stored)
558
+ contact._caps_ver = stored.caps_ver
505
559
  return contact
506
560
 
507
561
 
@@ -1,8 +1,10 @@
1
1
  import asyncio
2
2
  import logging
3
- from typing import TYPE_CHECKING, Generic, Iterator, 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
@@ -50,6 +52,7 @@ class LegacyRoster(
50
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):
@@ -63,25 +66,37 @@ class LegacyRoster(
63
66
  async def __finish_init_contact(
64
67
  self, legacy_id: LegacyUserIdType, jid_username: str, *args, **kwargs
65
68
  ):
66
- c = self._contact_cls(self.session, legacy_id, jid_username, *args, **kwargs)
67
- async with self.lock(("finish", c.legacy_id)):
69
+ async with self.lock(("finish", legacy_id)):
68
70
  with self.__store.session():
69
71
  stored = self.__store.get_by_legacy_id(
70
72
  self.session.user_pk, str(legacy_id)
71
73
  )
72
- if stored is not None and stored.updated:
73
- self.log.debug("Already updated %s", c)
74
- return self._contact_cls.from_store(self.session, stored)
75
- c.contact_pk = self.__store.add(
76
- self.session.user_pk, c.legacy_id, c.jid
77
- )
78
- await c.avatar_wrap_update_info()
79
- self.__store.update(c)
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()
80
95
  return c
81
96
 
82
97
  def known_contacts(self, only_friends=True) -> dict[str, LegacyContactType]:
83
98
  if only_friends:
84
- return {j: c for j, c in self if c.is_friend} # type:ignore
99
+ return {c.jid.bare: c for c in self if c.is_friend}
85
100
  return {c.jid.bare: c for c in self}
86
101
 
87
102
  async def by_jid(self, contact_jid: JID) -> LegacyContactType:
@@ -189,18 +204,63 @@ class LegacyRoster(
189
204
  """
190
205
  return _unescape_node(jid_username)
191
206
 
192
- 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]:
193
247
  """
194
248
  Populate slidge's "virtual roster".
195
249
 
196
- Override this and in it, ``await self.by_legacy_id(contact_id)``
197
- for the every legacy contacts of the user for which you'd like to
198
- 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.
199
260
 
200
- Await ``Contact.add_to_roster()`` in here to add the contact to the
201
- user's XMPP roster.
202
261
  """
203
- pass
262
+ return
263
+ yield
204
264
 
205
265
 
206
266
  ESCAPE_TABLE = "".maketrans({v: k for k, v in JID_UNESCAPE_TRANSFORMATIONS.items()})
@@ -185,10 +185,7 @@ LOG_FORMAT__DOC = (
185
185
  )
186
186
 
187
187
  MAM_MAX_DAYS = 7
188
- MAM_MAX_DAYS__DOC = (
189
- "Maximum number of days for group archive retention. "
190
- "Since all text content stored in RAM right now, "
191
- )
188
+ MAM_MAX_DAYS__DOC = "Maximum number of days for group archive retention."
192
189
 
193
190
  CORRECTION_EMPTY_BODY_AS_RETRACTION = True
194
191
  CORRECTION_EMPTY_BODY_AS_RETRACTION__DOC = (
@@ -39,6 +39,7 @@ from ...slixfix.roster import RosterBackend
39
39
  from ...slixfix.xep_0292.vcard4 import VCard4Provider
40
40
  from ...util import ABCSubclassableOnceAtMost
41
41
  from ...util.types import AvatarType, MessageOrPresenceTypeVar
42
+ from ...util.util import timeit
42
43
  from .. import config
43
44
  from ..mixins import MessageMixin
44
45
  from ..pubsub import PubSubComponent
@@ -271,6 +272,7 @@ class BaseGateway(
271
272
  """
272
273
 
273
274
  def __init__(self):
275
+ self.log = log
274
276
  self.datetime_started = datetime.now()
275
277
  self.xmpp = self # ugly hack to work with the BaseSender mixin :/
276
278
  self.default_ns = "jabber:component:accept"
@@ -335,6 +337,8 @@ class BaseGateway(
335
337
  # with this we receive user avatar updates
336
338
  self.plugin["xep_0030"].add_feature("urn:xmpp:avatar:metadata+notify")
337
339
 
340
+ self.plugin["xep_0030"].add_feature("urn:xmpp:chat-markers:0")
341
+
338
342
  if self.GROUPS:
339
343
  self.plugin["xep_0030"].add_feature("http://jabber.org/protocol/muc")
340
344
  self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2")
@@ -366,6 +370,8 @@ class BaseGateway(
366
370
 
367
371
  self.__mam_cleanup_task = self.loop.create_task(self.__mam_cleanup())
368
372
 
373
+ MessageMixin.__init__(self) # ComponentXMPP does not call super().__init__()
374
+
369
375
  async def __mam_cleanup(self):
370
376
  if not config.MAM_MAX_DAYS:
371
377
  return
@@ -399,7 +405,7 @@ class BaseGateway(
399
405
  log.debug("Context in the exception handler: %s", context)
400
406
  exc = context.get("exception")
401
407
  if exc is None:
402
- log.warning("No exception in this context: %s", context)
408
+ log.debug("No exception in this context: %s", context)
403
409
  elif isinstance(exc, SystemExit):
404
410
  log.debug("SystemExit called in an asyncio task")
405
411
  else:
@@ -540,6 +546,7 @@ class BaseGateway(
540
546
  exc_info=e,
541
547
  )
542
548
 
549
+ @timeit
543
550
  async def __login_wrap(self, session: "BaseSession"):
544
551
  session.send_gateway_status("Logging in…", show="dnd")
545
552
  try:
@@ -557,7 +564,7 @@ class BaseGateway(
557
564
  log.info("Login success for %s", session.user_jid)
558
565
  session.logged = True
559
566
  session.send_gateway_status("Syncing contacts…", show="dnd")
560
- await session.contacts.fill()
567
+ await session.contacts._fill()
561
568
  if not (r := session.contacts.ready).done():
562
569
  r.set_result(True)
563
570
  if self.GROUPS:
@@ -5,8 +5,6 @@ from slixmpp import Presence
5
5
  from slixmpp.exceptions import XMPPError
6
6
  from slixmpp.xmlstream import StanzaBase
7
7
 
8
- from ...contact import LegacyContact
9
-
10
8
  if TYPE_CHECKING:
11
9
  from .base import BaseGateway
12
10
 
@@ -25,6 +23,9 @@ class Caps:
25
23
  if not isinstance(stanza, Presence):
26
24
  return stanza
27
25
 
26
+ if stanza.get_plugin("caps", check=True):
27
+ return stanza
28
+
28
29
  if stanza["type"] not in ("available", "chat", "away", "dnd", "xa"):
29
30
  return stanza
30
31
 
@@ -44,10 +45,11 @@ class Caps:
44
45
 
45
46
  await session.ready
46
47
 
47
- entity = await session.get_contact_or_group_or_participant(pfrom)
48
- if not isinstance(entity, LegacyContact):
48
+ try:
49
+ contact = await session.contacts.by_jid(pfrom)
50
+ except XMPPError:
49
51
  return stanza
50
- ver = await entity.get_caps_ver(pfrom)
52
+ ver = await contact.get_caps_ver(pfrom)
51
53
  else:
52
54
  ver = await caps.get_verstring(pfrom)
53
55
 
@@ -28,7 +28,7 @@ class MucAdmin:
28
28
 
29
29
  reply = iq.reply()
30
30
  reply.enable("mucadmin_query")
31
- for participant in await muc.get_participants():
31
+ async for participant in muc.get_participants():
32
32
  if not participant.affiliation == affiliation:
33
33
  continue
34
34
  reply["mucadmin_query"].append(participant.mucadmin_item())
@@ -632,14 +632,28 @@ class SessionDispatcher:
632
632
  session = await self.__get_session(iq)
633
633
  session.raise_if_not_logged()
634
634
 
635
- item = iq["mucadmin_query"]["item"]
636
- contact = await session.contacts.by_jid(JID(item["jid"]))
637
-
638
635
  muc = await session.bookmarks.by_jid(iq.get_to())
639
636
 
640
- await muc.on_set_affiliation(
641
- contact, item["affiliation"], item["reason"] or None, item["nick"] or None
642
- )
637
+ item = iq["mucadmin_query"]["item"]
638
+ if item["jid"]:
639
+ contact = await session.contacts.by_jid(JID(item["jid"]))
640
+ else:
641
+ part = await muc.get_participant(
642
+ item["nick"], fill_first=True, raise_if_not_found=True
643
+ )
644
+ assert part.contact is not None
645
+ contact = part.contact
646
+
647
+ if item["affiliation"]:
648
+ await muc.on_set_affiliation(
649
+ contact,
650
+ item["affiliation"],
651
+ item["reason"] or None,
652
+ item["nick"] or None,
653
+ )
654
+ elif item["role"] == "none":
655
+ await muc.on_kick(contact, item["reason"] or None)
656
+
643
657
  iq.reply(clear=True).send()
644
658
 
645
659
  async def on_groupchat_direct_invite(self, msg: Message):
@@ -61,7 +61,7 @@ class VCardTemp:
61
61
 
62
62
  async def __handle_get_vcard_temp(self, iq: Iq):
63
63
  session = self.xmpp.get_session_from_stanza(iq)
64
- entity = await session.get_contact_or_group_or_participant(iq.get_to())
64
+ entity = await session.get_contact_or_group_or_participant(iq.get_to(), False)
65
65
  if not entity:
66
66
  raise XMPPError("item-not-found")
67
67