slidge 0.2.12__py3-none-any.whl → 0.3.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 (77) hide show
  1. slidge/__init__.py +5 -2
  2. slidge/command/adhoc.py +9 -3
  3. slidge/command/admin.py +16 -12
  4. slidge/command/base.py +16 -12
  5. slidge/command/chat_command.py +25 -16
  6. slidge/command/user.py +7 -8
  7. slidge/contact/contact.py +119 -209
  8. slidge/contact/roster.py +106 -105
  9. slidge/core/config.py +2 -43
  10. slidge/core/dispatcher/caps.py +9 -2
  11. slidge/core/dispatcher/disco.py +13 -3
  12. slidge/core/dispatcher/message/__init__.py +1 -1
  13. slidge/core/dispatcher/message/chat_state.py +17 -8
  14. slidge/core/dispatcher/message/marker.py +7 -5
  15. slidge/core/dispatcher/message/message.py +117 -92
  16. slidge/core/dispatcher/muc/__init__.py +1 -1
  17. slidge/core/dispatcher/muc/admin.py +4 -4
  18. slidge/core/dispatcher/muc/mam.py +10 -6
  19. slidge/core/dispatcher/muc/misc.py +4 -2
  20. slidge/core/dispatcher/muc/owner.py +5 -3
  21. slidge/core/dispatcher/muc/ping.py +3 -1
  22. slidge/core/dispatcher/presence.py +21 -15
  23. slidge/core/dispatcher/registration.py +20 -12
  24. slidge/core/dispatcher/search.py +7 -3
  25. slidge/core/dispatcher/session_dispatcher.py +13 -5
  26. slidge/core/dispatcher/util.py +37 -27
  27. slidge/core/dispatcher/vcard.py +7 -4
  28. slidge/core/gateway.py +168 -84
  29. slidge/core/mixins/__init__.py +1 -11
  30. slidge/core/mixins/attachment.py +163 -148
  31. slidge/core/mixins/avatar.py +100 -177
  32. slidge/core/mixins/db.py +50 -2
  33. slidge/core/mixins/message.py +19 -17
  34. slidge/core/mixins/message_maker.py +29 -15
  35. slidge/core/mixins/message_text.py +38 -30
  36. slidge/core/mixins/presence.py +91 -35
  37. slidge/core/pubsub.py +42 -47
  38. slidge/core/session.py +88 -57
  39. slidge/db/alembic/versions/0337c90c0b96_unify_legacy_xmpp_id_mappings.py +183 -0
  40. slidge/db/alembic/versions/4dbd23a3f868_new_avatar_store.py +105 -0
  41. slidge/db/alembic/versions/54ce3cde350c_use_hash_for_avatar_filenames.py +50 -0
  42. slidge/db/alembic/versions/58b98dacf819_refactor.py +118 -0
  43. slidge/db/alembic/versions/75a62b74b239_ditch_hats_table.py +74 -0
  44. slidge/db/avatar.py +150 -119
  45. slidge/db/meta.py +33 -22
  46. slidge/db/models.py +68 -117
  47. slidge/db/store.py +412 -1094
  48. slidge/group/archive.py +61 -54
  49. slidge/group/bookmarks.py +74 -55
  50. slidge/group/participant.py +135 -142
  51. slidge/group/room.py +315 -312
  52. slidge/main.py +28 -18
  53. slidge/migration.py +2 -12
  54. slidge/slixfix/__init__.py +20 -4
  55. slidge/slixfix/delivery_receipt.py +6 -4
  56. slidge/slixfix/link_preview/link_preview.py +1 -1
  57. slidge/slixfix/link_preview/stanza.py +1 -1
  58. slidge/slixfix/roster.py +5 -7
  59. slidge/slixfix/xep_0077/register.py +8 -8
  60. slidge/slixfix/xep_0077/stanza.py +7 -7
  61. slidge/slixfix/xep_0100/gateway.py +12 -13
  62. slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
  63. slidge/slixfix/xep_0292/vcard4.py +1 -1
  64. slidge/util/archive_msg.py +11 -5
  65. slidge/util/conf.py +23 -20
  66. slidge/util/jid_escaping.py +1 -1
  67. slidge/{core/mixins → util}/lock.py +6 -6
  68. slidge/util/test.py +30 -29
  69. slidge/util/types.py +22 -18
  70. slidge/util/util.py +19 -22
  71. {slidge-0.2.12.dist-info → slidge-0.3.0a1.dist-info}/METADATA +1 -1
  72. slidge-0.3.0a1.dist-info/RECORD +117 -0
  73. {slidge-0.2.12.dist-info → slidge-0.3.0a1.dist-info}/WHEEL +1 -1
  74. slidge-0.2.12.dist-info/RECORD +0 -112
  75. {slidge-0.2.12.dist-info → slidge-0.3.0a1.dist-info}/entry_points.txt +0 -0
  76. {slidge-0.2.12.dist-info → slidge-0.3.0a1.dist-info}/licenses/LICENSE +0 -0
  77. {slidge-0.2.12.dist-info → slidge-0.3.0a1.dist-info}/top_level.txt +0 -0
@@ -5,19 +5,17 @@ import warnings
5
5
  from copy import copy
6
6
  from datetime import datetime
7
7
  from functools import cached_property
8
- from typing import TYPE_CHECKING, Optional, Self, Union
8
+ from typing import TYPE_CHECKING, Any, Optional, Union
9
9
 
10
+ import sqlalchemy as sa
10
11
  from slixmpp import JID, InvalidJID, Message, Presence
11
12
  from slixmpp.plugins.xep_0045.stanza import MUCAdminItem
12
13
  from slixmpp.types import MessageTypes, OptJid
14
+ from sqlalchemy.orm.exc import DetachedInstanceError
13
15
 
14
16
  from ..contact import LegacyContact
15
- from ..core.mixins import (
16
- ChatterDiscoMixin,
17
- MessageMixin,
18
- PresenceMixin,
19
- StoredAttributeMixin,
20
- )
17
+ from ..core.mixins import ChatterDiscoMixin, MessageMixin, PresenceMixin
18
+ from ..core.mixins.db import DBMixin
21
19
  from ..db.models import Participant
22
20
  from ..util import SubclassableOnce, strip_illegal_chars
23
21
  from ..util.types import (
@@ -43,10 +41,10 @@ def strip_non_printable(nickname: str):
43
41
 
44
42
 
45
43
  class LegacyParticipant(
46
- StoredAttributeMixin,
47
44
  PresenceMixin,
48
45
  MessageMixin,
49
46
  ChatterDiscoMixin,
47
+ DBMixin,
50
48
  metaclass=SubclassableOnce,
51
49
  ):
52
50
  """
@@ -57,83 +55,111 @@ class LegacyParticipant(
57
55
  _can_send_carbon = False
58
56
  USE_STANZA_ID = True
59
57
  STRIP_SHORT_DELAY = False
60
- pk: int
58
+ stored: Participant
59
+ contact: LegacyContact[Any] | None
61
60
 
62
61
  def __init__(
63
62
  self,
64
63
  muc: "LegacyMUC",
65
- nickname: Optional[str] = None,
66
- is_user=False,
67
- is_system=False,
68
- role: MucRole = "participant",
69
- affiliation: MucAffiliation = "member",
70
- resource: str | None = None,
71
- nickname_no_illegal: str | None = None,
72
- ):
73
- self.session = session = muc.session
74
- self.xmpp = session.xmpp
75
- super().__init__()
76
- self._hats = list[Hat]()
64
+ stored: Participant,
65
+ is_system: bool = False,
66
+ contact: LegacyContact[Any] | None = None,
67
+ ) -> None:
77
68
  self.muc = muc
78
- self._role = role
79
- self._affiliation = affiliation
80
- self.is_user: bool = is_user
81
- self.is_system: bool = is_system
69
+ self.session = muc.session
70
+ self.xmpp = muc.session.xmpp
71
+ self.is_system = is_system
82
72
 
83
- self._nickname = nickname
73
+ if contact is None and stored.contact is not None:
74
+ contact = self.session.contacts.from_store(stored=stored.contact)
75
+ if contact is not None and stored.contact is None:
76
+ stored.contact = contact.stored
84
77
 
85
- if resource is None:
86
- self.__update_jid(nickname)
87
- else:
88
- assert nickname_no_illegal is not None
89
- self._nickname_no_illegal = nickname_no_illegal
90
- self.jid = JID(self.muc.jid)
91
- self.jid.resource = resource
78
+ self.stored = stored
79
+ self.contact = contact
92
80
 
93
- log.debug("Instantiation of: %r", self)
81
+ super().__init__()
82
+
83
+ if stored.resource is None:
84
+ self.__update_resource(stored.nickname)
94
85
 
95
- self.contact: Optional["LegacyContact"] = None
96
- # we track if we already sent a presence for this participant.
97
- # if we didn't, we send it before the first message.
98
- # this way, event in plugins that don't map "user has joined" events,
99
- # we send a "join"-presence from the participant before the first message
100
- self._presence_sent: bool = False
101
86
  self.log = logging.getLogger(f"{self.user_jid.bare}:{self.jid}")
102
- self.__part_store = self.xmpp.store.participants
103
87
 
104
88
  @property
105
- def contact_pk(self) -> Optional[int]: # type:ignore
106
- if self.contact:
107
- return self.contact.contact_pk
108
- return None
89
+ def is_user(self) -> bool:
90
+ try:
91
+ return self.stored.is_user
92
+ except DetachedInstanceError:
93
+ self.merge()
94
+ return self.stored.is_user
95
+
96
+ @property
97
+ def jid(self) -> JID:
98
+ jid = JID(self.muc.jid)
99
+ if self.stored.resource:
100
+ jid.resource = self.stored.resource
101
+ return jid
102
+
103
+ @jid.setter
104
+ def jid(self, x: JID):
105
+ # FIXME: without this, mypy yields
106
+ # "Cannot override writeable attribute with read-only property"
107
+ # But it does not happen for LegacyContact. WTF?
108
+ raise RuntimeError
109
+
110
+ def commit(self, *args, **kwargs) -> None:
111
+ if self.is_system:
112
+ return
113
+ if self.muc.get_lock("fill participants") or self.muc.get_lock("fill history"):
114
+ return
115
+ super().commit(*args, **kwargs)
109
116
 
110
117
  @property
111
118
  def user_jid(self):
112
119
  return self.session.user_jid
113
120
 
114
- def __repr__(self):
121
+ def __repr__(self) -> str:
115
122
  return f"<Participant '{self.nickname}'/'{self.jid}' of '{self.muc}'>"
116
123
 
124
+ @property
125
+ def _presence_sent(self) -> bool:
126
+ # we track if we already sent a presence for this participant.
127
+ # if we didn't, we send it before the first message.
128
+ # this way, event in plugins that don't map "user has joined" events,
129
+ # we send a "join"-presence from the participant before the first message
130
+ return self.stored.presence_sent
131
+
132
+ @_presence_sent.setter
133
+ def _presence_sent(self, val: bool) -> None:
134
+ if self._presence_sent == val:
135
+ return
136
+ self.stored.presence_sent = val
137
+ self.commit(merge=True)
138
+
139
+ @property
140
+ def nickname_no_illegal(self) -> str:
141
+ return self.stored.nickname_no_illegal
142
+
117
143
  @property
118
144
  def affiliation(self):
119
- return self._affiliation
145
+ return self.stored.affiliation
120
146
 
121
147
  @affiliation.setter
122
- def affiliation(self, affiliation: MucAffiliation):
123
- if self._affiliation == affiliation:
148
+ def affiliation(self, affiliation: MucAffiliation) -> None:
149
+ if self.affiliation == affiliation:
124
150
  return
125
- self._affiliation = affiliation
126
- if not self.muc._participants_filled:
151
+ self.stored.affiliation = affiliation
152
+ if not self.muc.participants_filled:
127
153
  return
128
- self.__part_store.set_affiliation(self.pk, affiliation)
154
+ self.commit()
129
155
  if not self._presence_sent:
130
156
  return
131
157
  self.send_last_presence(force=True, no_cache_online=True)
132
158
 
133
- def send_affiliation_change(self):
159
+ def send_affiliation_change(self) -> None:
134
160
  # internal use by slidge
135
161
  msg = self._make_message()
136
- msg["muc"]["affiliation"] = self._affiliation
162
+ msg["muc"]["affiliation"] = self.affiliation
137
163
  msg["type"] = "normal"
138
164
  if not self.muc.is_anonymous and not self.is_system:
139
165
  if self.contact:
@@ -146,48 +172,53 @@ class LegacyParticipant(
146
172
 
147
173
  @property
148
174
  def role(self):
149
- return self._role
175
+ return self.stored.role
150
176
 
151
177
  @role.setter
152
- def role(self, role: MucRole):
153
- if self._role == role:
178
+ def role(self, role: MucRole) -> None:
179
+ if self.role == role:
154
180
  return
155
- self._role = role
156
- if not self.muc._participants_filled:
181
+ self.stored.role = role
182
+ if not self.muc.participants_filled:
157
183
  return
158
- self.__part_store.set_role(self.pk, role)
184
+ self.commit()
159
185
  if not self._presence_sent:
160
186
  return
161
187
  self.send_last_presence(force=True, no_cache_online=True)
162
188
 
163
- def set_hats(self, hats: list[Hat]):
164
- if self._hats == hats:
189
+ @property
190
+ def hats(self) -> list[Hat]:
191
+ return [Hat(*h) for h in self.stored.hats] if self.stored.hats else []
192
+
193
+ def set_hats(self, hats: list[Hat]) -> None:
194
+ if self.hats == hats:
165
195
  return
166
- self._hats = hats
167
- if not self.muc._participants_filled:
196
+ self.stored.hats = hats # type:ignore[assignment]
197
+ if not self.muc.participants_filled:
168
198
  return
169
- self.__part_store.set_hats(self.pk, hats)
199
+ self.commit(merge=True)
170
200
  if not self._presence_sent:
171
201
  return
172
202
  self.send_last_presence(force=True, no_cache_online=True)
173
203
 
174
- def __update_jid(self, unescaped_nickname: Optional[str]):
204
+ def __update_resource(self, unescaped_nickname: Optional[str]) -> None:
175
205
  if not unescaped_nickname:
176
- self.jid = JID(self.muc.jid)
206
+ self.stored.resource = ""
177
207
  if self.is_system:
178
- self._nickname_no_illegal = ""
208
+ self.stored.nickname_no_illegal = ""
179
209
  else:
180
210
  warnings.warn(
181
211
  "Only the system participant is allowed to not have a nickname"
182
212
  )
183
213
  nickname = f"unnamed-{uuid.uuid4()}"
184
- self.jid.resource = self._nickname_no_illegal = nickname
214
+ self.stored.resource = self.stored.nickname_no_illegal = nickname
185
215
  return
186
216
 
187
- self._nickname_no_illegal, self.jid = escape_nickname(
217
+ self.stored.nickname_no_illegal, jid = escape_nickname(
188
218
  self.muc.jid,
189
219
  unescaped_nickname,
190
220
  )
221
+ self.stored.resource = jid.resource
191
222
 
192
223
  def send_configuration_change(self, codes: tuple[int]):
193
224
  if not self.is_system:
@@ -198,11 +229,11 @@ class LegacyParticipant(
198
229
 
199
230
  @property
200
231
  def nickname(self):
201
- return self._nickname
232
+ return self.stored.nickname
202
233
 
203
234
  @nickname.setter
204
- def nickname(self, new_nickname: str):
205
- old = self._nickname
235
+ def nickname(self, new_nickname: str) -> None:
236
+ old = self.nickname
206
237
  if new_nickname == old:
207
238
  return
208
239
 
@@ -219,18 +250,16 @@ class LegacyParticipant(
219
250
  p = self._make_presence(ptype="unavailable", last_seen=last_seen, **kwargs)
220
251
  # in this order so pfrom=old resource and we actually use the escaped nick
221
252
  # in the muc/item/nick element
222
- self.__update_jid(new_nickname)
253
+ self.__update_resource(new_nickname)
223
254
  p["muc"]["item"]["nick"] = self.jid.resource
224
255
  self._send(p)
225
256
 
226
- self._nickname = new_nickname
227
-
257
+ self.stored.nickname = new_nickname
258
+ self.commit()
228
259
  kwargs["status_codes"] = set()
229
260
  p = self._make_presence(ptype="available", last_seen=last_seen, **kwargs)
230
261
  self._send(p)
231
262
 
232
- self.__part_store.update(self)
233
-
234
263
  def _make_presence(
235
264
  self,
236
265
  *,
@@ -242,8 +271,8 @@ class LegacyParticipant(
242
271
  p = super()._make_presence(last_seen=last_seen, **presence_kwargs)
243
272
  p["muc"]["affiliation"] = self.affiliation
244
273
  p["muc"]["role"] = self.role
245
- if self._hats:
246
- p["hats"].add_hats(self._hats)
274
+ if self.hats:
275
+ p["hats"].add_hats(self.hats)
247
276
  codes = status_codes or set()
248
277
  if self.is_user:
249
278
  codes.add(110)
@@ -254,9 +283,7 @@ class LegacyParticipant(
254
283
  else:
255
284
  jid = JID(self.user_jid)
256
285
  try:
257
- jid.resource = next(
258
- iter(self.muc.get_user_resources()) # type:ignore
259
- )
286
+ jid.resource = next(iter(self.muc.get_user_resources()))
260
287
  except StopIteration:
261
288
  jid.resource = "pseudo-resource"
262
289
  p["muc"]["jid"] = self.user_jid
@@ -280,7 +307,7 @@ class LegacyParticipant(
280
307
 
281
308
  def __send_presence_if_needed(
282
309
  self, stanza: Union[Message, Presence], full_jid: JID, archive_only: bool
283
- ):
310
+ ) -> None:
284
311
  if (
285
312
  archive_only
286
313
  or self.is_system
@@ -309,8 +336,9 @@ class LegacyParticipant(
309
336
  self,
310
337
  stanza: MessageOrPresenceTypeVar,
311
338
  full_jid: Optional[JID] = None,
312
- archive_only=False,
339
+ archive_only: bool = False,
313
340
  legacy_msg_id=None,
341
+ initial_presence=False,
314
342
  **send_kwargs,
315
343
  ) -> MessageOrPresenceTypeVar:
316
344
  if stanza.get_from().resource:
@@ -321,8 +349,10 @@ class LegacyParticipant(
321
349
  if not self.is_user and isinstance(stanza, Presence):
322
350
  if stanza["type"] == "unavailable" and not self._presence_sent:
323
351
  return stanza # type:ignore
324
- self._presence_sent = True
325
- self.__part_store.set_presence_sent(self.pk)
352
+ if initial_presence:
353
+ self.stored.presence_sent = True
354
+ else:
355
+ self._presence_sent = True
326
356
  if full_jid:
327
357
  stanza["to"] = full_jid
328
358
  self.__send_presence_if_needed(stanza, full_jid, archive_only)
@@ -362,8 +392,8 @@ class LegacyParticipant(
362
392
  )
363
393
  return item
364
394
 
365
- def __add_nick_element(self, stanza: Union[Presence, Message]):
366
- if (nick := self._nickname_no_illegal) != self.jid.resource:
395
+ def __add_nick_element(self, stanza: Union[Presence, Message]) -> None:
396
+ if (nick := self.nickname_no_illegal) != self.jid.resource:
367
397
  n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
368
398
  n["nick"] = nick
369
399
  stanza.append(n)
@@ -377,9 +407,9 @@ class LegacyParticipant(
377
407
  def send_initial_presence(
378
408
  self,
379
409
  full_jid: JID,
380
- nick_change=False,
410
+ nick_change: bool = False,
381
411
  presence_id: Optional[str] = None,
382
- ):
412
+ ) -> None:
383
413
  """
384
414
  Called when the user joins a MUC, as a mechanism
385
415
  to indicate to the joining XMPP client the list of "participants".
@@ -418,21 +448,21 @@ class LegacyParticipant(
418
448
  )
419
449
  if presence_id:
420
450
  p["id"] = presence_id
421
- self._send(p, full_jid)
451
+ self._send(p, full_jid, initial_presence=True)
422
452
 
423
- def leave(self):
453
+ def leave(self) -> None:
424
454
  """
425
455
  Call this when the participant leaves the room
426
456
  """
427
457
  self.muc.remove_participant(self)
428
458
 
429
- def kick(self, reason: str | None = None):
459
+ def kick(self, reason: str | None = None) -> None:
430
460
  """
431
461
  Call this when the participant is kicked from the room
432
462
  """
433
463
  self.muc.remove_participant(self, kick=True, reason=reason)
434
464
 
435
- def ban(self, reason: str | None = None):
465
+ def ban(self, reason: str | None = None) -> None:
436
466
  """
437
467
  Call this when the participant is banned from the room
438
468
  """
@@ -443,15 +473,16 @@ class LegacyParticipant(
443
473
  return self.contact.get_disco_info()
444
474
  return super().get_disco_info()
445
475
 
446
- def moderate(self, legacy_msg_id: LegacyMessageType, reason: Optional[str] = None):
476
+ def moderate(
477
+ self, legacy_msg_id: LegacyMessageType, reason: Optional[str] = None
478
+ ) -> None:
447
479
  xmpp_id = self._legacy_to_xmpp(legacy_msg_id)
448
- multi = self.xmpp.store.multi.get_xmpp_ids(self.session.user_pk, xmpp_id)
449
- if multi is None:
450
- msg_ids = [xmpp_id]
451
- else:
452
- msg_ids = multi + [xmpp_id]
480
+ with self.xmpp.store.session() as orm:
481
+ msg_ids = self.xmpp.store.id_map.get_xmpp(
482
+ orm, self.muc.stored.id, str(legacy_msg_id), True
483
+ )
453
484
 
454
- for i in msg_ids:
485
+ for i in set(msg_ids + [xmpp_id]):
455
486
  m = self.muc.get_system_participant()._make_message()
456
487
  m["retract"]["id"] = i
457
488
  if self.is_system:
@@ -468,8 +499,8 @@ class LegacyParticipant(
468
499
  subject: str,
469
500
  full_jid: Optional[JID] = None,
470
501
  when: Optional[datetime] = None,
471
- update_muc=True,
472
- ):
502
+ update_muc: bool = True,
503
+ ) -> None:
473
504
  if update_muc:
474
505
  self.muc._subject = subject # type: ignore
475
506
  self.muc.subject_setter = self.nickname
@@ -482,44 +513,6 @@ class LegacyParticipant(
482
513
  msg["subject"] = subject or str(self.muc.name)
483
514
  self._send(msg, full_jid)
484
515
 
485
- @classmethod
486
- def from_store(
487
- cls,
488
- session,
489
- stored: Participant,
490
- contact: Optional[LegacyContact] = None,
491
- muc: Optional["LegacyMUC"] = None,
492
- ) -> Self:
493
- from slidge.group.room import LegacyMUC
494
-
495
- if muc is None:
496
- muc = LegacyMUC.get_self_or_unique_subclass().from_store(
497
- session, stored.room
498
- )
499
- part = cls(
500
- muc,
501
- stored.nickname,
502
- role=stored.role,
503
- affiliation=stored.affiliation,
504
- resource=stored.resource,
505
- nickname_no_illegal=stored.nickname_no_illegal,
506
- )
507
- part.pk = stored.id
508
- if contact is not None:
509
- part.contact = contact
510
- elif stored.contact is not None:
511
- contact = LegacyContact.get_self_or_unique_subclass().from_store(
512
- session, stored.contact
513
- )
514
- part.contact = contact
515
-
516
- part.is_user = stored.is_user
517
- if (data := stored.extra_attributes) is not None:
518
- muc.deserialize_extra_attributes(data)
519
- part._presence_sent = stored.presence_sent
520
- part._hats = [Hat(h.uri, h.title) for h in stored.hats]
521
- return part
522
-
523
516
 
524
517
  def escape_nickname(muc_jid: JID, nickname: str) -> tuple[str, JID]:
525
518
  nickname = nickname_no_illegal = strip_illegal_chars(nickname)