slidge 0.2.0a0__py3-none-any.whl → 0.2.0a1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. slidge/__version__.py +1 -1
  2. slidge/command/admin.py +1 -1
  3. slidge/command/user.py +0 -1
  4. slidge/contact/contact.py +86 -32
  5. slidge/contact/roster.py +79 -19
  6. slidge/core/config.py +1 -4
  7. slidge/core/gateway/base.py +9 -2
  8. slidge/core/gateway/caps.py +7 -5
  9. slidge/core/gateway/muc_admin.py +1 -1
  10. slidge/core/gateway/session_dispatcher.py +20 -6
  11. slidge/core/gateway/vcard_temp.py +1 -1
  12. slidge/core/mixins/attachment.py +17 -4
  13. slidge/core/mixins/avatar.py +26 -9
  14. slidge/core/mixins/db.py +18 -0
  15. slidge/core/mixins/disco.py +0 -10
  16. slidge/core/mixins/message.py +7 -1
  17. slidge/core/mixins/presence.py +6 -3
  18. slidge/core/pubsub.py +7 -15
  19. slidge/core/session.py +6 -3
  20. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  21. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  22. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  23. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +11 -2
  24. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
  25. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  26. slidge/db/avatar.py +20 -9
  27. slidge/db/models.py +16 -6
  28. slidge/db/store.py +217 -115
  29. slidge/group/archive.py +46 -1
  30. slidge/group/bookmarks.py +17 -5
  31. slidge/group/participant.py +10 -3
  32. slidge/group/room.py +183 -125
  33. slidge/main.py +3 -3
  34. slidge/slixfix/xep_0292/vcard4.py +2 -0
  35. slidge/util/archive_msg.py +2 -1
  36. slidge/util/test.py +54 -4
  37. slidge/util/types.py +5 -0
  38. slidge/util/util.py +22 -0
  39. {slidge-0.2.0a0.dist-info → slidge-0.2.0a1.dist-info}/METADATA +2 -4
  40. {slidge-0.2.0a0.dist-info → slidge-0.2.0a1.dist-info}/RECORD +43 -37
  41. {slidge-0.2.0a0.dist-info → slidge-0.2.0a1.dist-info}/LICENSE +0 -0
  42. {slidge-0.2.0a0.dist-info → slidge-0.2.0a1.dist-info}/WHEEL +0 -0
  43. {slidge-0.2.0a0.dist-info → slidge-0.2.0a1.dist-info}/entry_points.txt +0 -0
slidge/__version__.py CHANGED
@@ -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"
slidge/command/admin.py CHANGED
@@ -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:
slidge/command/user.py CHANGED
@@ -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
  )
slidge/contact/contact.py CHANGED
@@ -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
 
slidge/contact/roster.py CHANGED
@@ -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()})
slidge/core/config.py CHANGED
@@ -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
 
@@ -9,7 +9,7 @@ import warnings
9
9
  from datetime import datetime
10
10
  from mimetypes import guess_type
11
11
  from pathlib import Path
12
- from typing import IO, Collection, Optional, Sequence, Union
12
+ from typing import IO, AsyncIterator, Collection, Optional, Sequence, Union
13
13
  from urllib.parse import quote as urlquote
14
14
  from uuid import uuid4
15
15
  from xml.etree import ElementTree as ET
@@ -141,6 +141,7 @@ class AttachmentMixin(MessageMaker):
141
141
  async def __get_url(
142
142
  self,
143
143
  file_path: Optional[Path] = None,
144
+ async_data_stream: Optional[AsyncIterator[bytes]] = None,
144
145
  data_stream: Optional[IO[bytes]] = None,
145
146
  data: Optional[bytes] = None,
146
147
  file_url: Optional[str] = None,
@@ -176,14 +177,23 @@ class AttachmentMixin(MessageMaker):
176
177
  with file_path.open("wb") as f:
177
178
  f.write(await r.read())
178
179
 
179
- else:
180
- if data_stream is not None:
181
- data = data_stream.read()
180
+ elif data_stream is not None:
181
+ data = data_stream.read()
182
182
  if data is None:
183
183
  raise RuntimeError
184
184
 
185
185
  with file_path.open("wb") as f:
186
186
  f.write(data)
187
+ elif async_data_stream is not None:
188
+ # TODO: patch slixmpp to allow this as data source for
189
+ # upload_file() so we don't even have to write anything
190
+ # to disk.
191
+ with file_path.open("wb") as f:
192
+ async for chunk in async_data_stream:
193
+ f.write(chunk)
194
+ elif data is not None:
195
+ with file_path.open("wb") as f:
196
+ f.write(data)
187
197
 
188
198
  is_temp = not bool(config.NO_UPLOAD_PATH)
189
199
  else:
@@ -296,6 +306,7 @@ class AttachmentMixin(MessageMaker):
296
306
  file_path: Optional[Union[Path, str]] = None,
297
307
  legacy_msg_id: Optional[LegacyMessageType] = None,
298
308
  *,
309
+ async_data_stream: Optional[AsyncIterator[bytes]] = None,
299
310
  data_stream: Optional[IO[bytes]] = None,
300
311
  data: Optional[bytes] = None,
301
312
  file_url: Optional[str] = None,
@@ -312,6 +323,7 @@ class AttachmentMixin(MessageMaker):
312
323
  Send a single file from this :term:`XMPP Entity`.
313
324
 
314
325
  :param file_path: Path to the attachment
326
+ :param async_data_stream: Alternatively (and ideally) an AsyncIterator yielding bytes
315
327
  :param data_stream: Alternatively, a stream of bytes (such as a File object)
316
328
  :param data: Alternatively, a bytes object
317
329
  :param file_url: Alternatively, a URL
@@ -342,6 +354,7 @@ class AttachmentMixin(MessageMaker):
342
354
 
343
355
  is_temp, local_path, new_url = await self.__get_url(
344
356
  Path(file_path) if file_path else None,
357
+ async_data_stream,
345
358
  data_stream,
346
359
  data,
347
360
  file_url,