slidge 0.2.11__py3-none-any.whl → 0.3.0__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 (93) 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 +123 -210
  8. slidge/contact/roster.py +108 -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 +120 -93
  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 +26 -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 +177 -87
  29. slidge/core/mixins/__init__.py +1 -11
  30. slidge/core/mixins/attachment.py +200 -147
  31. slidge/core/mixins/avatar.py +105 -177
  32. slidge/core/mixins/base.py +3 -1
  33. slidge/core/mixins/db.py +50 -2
  34. slidge/core/mixins/disco.py +1 -1
  35. slidge/core/mixins/message.py +19 -17
  36. slidge/core/mixins/message_maker.py +29 -15
  37. slidge/core/mixins/message_text.py +67 -30
  38. slidge/core/mixins/presence.py +94 -37
  39. slidge/core/pubsub.py +42 -47
  40. slidge/core/session.py +95 -60
  41. slidge/db/alembic/versions/cef02a8b1451_initial_schema.py +361 -0
  42. slidge/db/avatar.py +150 -119
  43. slidge/db/meta.py +33 -22
  44. slidge/db/models.py +69 -117
  45. slidge/db/store.py +414 -1094
  46. slidge/group/archive.py +65 -55
  47. slidge/group/bookmarks.py +96 -59
  48. slidge/group/participant.py +150 -144
  49. slidge/group/room.py +351 -328
  50. slidge/main.py +34 -22
  51. slidge/migration.py +17 -29
  52. slidge/slixfix/__init__.py +20 -4
  53. slidge/slixfix/delivery_receipt.py +6 -4
  54. slidge/slixfix/link_preview/link_preview.py +1 -1
  55. slidge/slixfix/link_preview/stanza.py +1 -1
  56. slidge/slixfix/roster.py +5 -7
  57. slidge/slixfix/xep_0077/register.py +8 -8
  58. slidge/slixfix/xep_0077/stanza.py +7 -7
  59. slidge/slixfix/xep_0100/gateway.py +12 -13
  60. slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
  61. slidge/slixfix/xep_0292/vcard4.py +12 -2
  62. slidge/util/archive_msg.py +11 -5
  63. slidge/util/conf.py +27 -21
  64. slidge/util/jid_escaping.py +1 -1
  65. slidge/{core/mixins → util}/lock.py +6 -6
  66. slidge/util/test.py +30 -29
  67. slidge/util/types.py +24 -18
  68. slidge/util/util.py +26 -22
  69. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/METADATA +1 -1
  70. slidge-0.3.0.dist-info/RECORD +95 -0
  71. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/WHEEL +1 -1
  72. slidge/db/alembic/versions/04cf35e3cf85_add_participant_nickname_no_illegal.py +0 -33
  73. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +0 -36
  74. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +0 -85
  75. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +0 -36
  76. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +0 -37
  77. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +0 -41
  78. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +0 -52
  79. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +0 -42
  80. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +0 -61
  81. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +0 -48
  82. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +0 -43
  83. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +0 -139
  84. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +0 -50
  85. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +0 -79
  86. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +0 -214
  87. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +0 -52
  88. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +0 -34
  89. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +0 -26
  90. slidge-0.2.11.dist-info/RECORD +0 -112
  91. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/entry_points.txt +0 -0
  92. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/licenses/LICENSE +0 -0
  93. {slidge-0.2.11.dist-info → slidge-0.3.0.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
+ from xml.etree import ElementTree as ET
9
10
 
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,97 +41,134 @@ 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
  """
53
51
  A legacy participant of a legacy group chat.
54
52
  """
55
53
 
54
+ is_participant = True
55
+
56
56
  mtype: MessageTypes = "groupchat"
57
57
  _can_send_carbon = False
58
58
  USE_STANZA_ID = True
59
59
  STRIP_SHORT_DELAY = False
60
- pk: int
60
+ stored: Participant
61
+ contact: LegacyContact[Any] | None
61
62
 
62
63
  def __init__(
63
64
  self,
64
65
  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]()
66
+ stored: Participant,
67
+ is_system: bool = False,
68
+ contact: LegacyContact[Any] | None = None,
69
+ ) -> None:
77
70
  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
71
+ self.session = muc.session
72
+ self.xmpp = muc.session.xmpp
73
+ self.is_system = is_system
82
74
 
83
- self._nickname = nickname
75
+ if contact is None and stored.contact is not None:
76
+ contact = self.session.contacts.from_store(stored=stored.contact)
77
+ if contact is not None and stored.contact is None:
78
+ stored.contact = contact.stored
84
79
 
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
80
+ self.stored = stored
81
+ self.contact = contact
92
82
 
93
- log.debug("Instantiation of: %r", self)
83
+ super().__init__()
84
+
85
+ if stored.resource is None:
86
+ self.__update_resource(stored.nickname)
94
87
 
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
88
  self.log = logging.getLogger(f"{self.user_jid.bare}:{self.jid}")
102
- self.__part_store = self.xmpp.store.participants
103
89
 
104
90
  @property
105
- def contact_pk(self) -> Optional[int]: # type:ignore
106
- if self.contact:
107
- return self.contact.contact_pk
108
- return None
91
+ def is_user(self) -> bool:
92
+ try:
93
+ return self.stored.is_user
94
+ except DetachedInstanceError:
95
+ self.merge()
96
+ return self.stored.is_user
97
+
98
+ @is_user.setter
99
+ def is_user(self, is_user: bool) -> None:
100
+ with self.xmpp.store.session(expire_on_commit=True) as orm:
101
+ orm.add(self.stored)
102
+ self.stored.is_user = is_user
103
+ orm.commit()
104
+
105
+ @property
106
+ def jid(self) -> JID:
107
+ jid = JID(self.muc.jid)
108
+ if self.stored.resource:
109
+ jid.resource = self.stored.resource
110
+ return jid
111
+
112
+ @jid.setter
113
+ def jid(self, x: JID):
114
+ # FIXME: without this, mypy yields
115
+ # "Cannot override writeable attribute with read-only property"
116
+ # But it does not happen for LegacyContact. WTF?
117
+ raise RuntimeError
118
+
119
+ def commit(self, *args, **kwargs) -> None:
120
+ if self.is_system:
121
+ return
122
+ if self.muc.get_lock("fill participants") or self.muc.get_lock("fill history"):
123
+ return
124
+ super().commit(*args, **kwargs)
109
125
 
110
126
  @property
111
127
  def user_jid(self):
112
128
  return self.session.user_jid
113
129
 
114
- def __repr__(self):
130
+ def __repr__(self) -> str:
115
131
  return f"<Participant '{self.nickname}'/'{self.jid}' of '{self.muc}'>"
116
132
 
133
+ @property
134
+ def _presence_sent(self) -> bool:
135
+ # we track if we already sent a presence for this participant.
136
+ # if we didn't, we send it before the first message.
137
+ # this way, event in plugins that don't map "user has joined" events,
138
+ # we send a "join"-presence from the participant before the first message
139
+ return self.stored.presence_sent
140
+
141
+ @_presence_sent.setter
142
+ def _presence_sent(self, val: bool) -> None:
143
+ if self._presence_sent == val:
144
+ return
145
+ self.stored.presence_sent = val
146
+ self.commit(merge=True)
147
+
148
+ @property
149
+ def nickname_no_illegal(self) -> str:
150
+ return self.stored.nickname_no_illegal
151
+
117
152
  @property
118
153
  def affiliation(self):
119
- return self._affiliation
154
+ return self.stored.affiliation
120
155
 
121
156
  @affiliation.setter
122
- def affiliation(self, affiliation: MucAffiliation):
123
- if self._affiliation == affiliation:
157
+ def affiliation(self, affiliation: MucAffiliation) -> None:
158
+ if self.affiliation == affiliation:
124
159
  return
125
- self._affiliation = affiliation
126
- if not self.muc._participants_filled:
160
+ self.stored.affiliation = affiliation
161
+ if not self.muc.participants_filled:
127
162
  return
128
- self.__part_store.set_affiliation(self.pk, affiliation)
163
+ self.commit()
129
164
  if not self._presence_sent:
130
165
  return
131
166
  self.send_last_presence(force=True, no_cache_online=True)
132
167
 
133
- def send_affiliation_change(self):
168
+ def send_affiliation_change(self) -> None:
134
169
  # internal use by slidge
135
170
  msg = self._make_message()
136
- msg["muc"]["affiliation"] = self._affiliation
171
+ msg["muc"]["affiliation"] = self.affiliation
137
172
  msg["type"] = "normal"
138
173
  if not self.muc.is_anonymous and not self.is_system:
139
174
  if self.contact:
@@ -146,48 +181,53 @@ class LegacyParticipant(
146
181
 
147
182
  @property
148
183
  def role(self):
149
- return self._role
184
+ return self.stored.role
150
185
 
151
186
  @role.setter
152
- def role(self, role: MucRole):
153
- if self._role == role:
187
+ def role(self, role: MucRole) -> None:
188
+ if self.role == role:
154
189
  return
155
- self._role = role
156
- if not self.muc._participants_filled:
190
+ self.stored.role = role
191
+ if not self.muc.participants_filled:
157
192
  return
158
- self.__part_store.set_role(self.pk, role)
193
+ self.commit()
159
194
  if not self._presence_sent:
160
195
  return
161
196
  self.send_last_presence(force=True, no_cache_online=True)
162
197
 
163
- def set_hats(self, hats: list[Hat]):
164
- if self._hats == hats:
198
+ @property
199
+ def hats(self) -> list[Hat]:
200
+ return [Hat(*h) for h in self.stored.hats] if self.stored.hats else []
201
+
202
+ def set_hats(self, hats: list[Hat]) -> None:
203
+ if self.hats == hats:
165
204
  return
166
- self._hats = hats
167
- if not self.muc._participants_filled:
205
+ self.stored.hats = hats # type:ignore[assignment]
206
+ if not self.muc.participants_filled:
168
207
  return
169
- self.__part_store.set_hats(self.pk, hats)
208
+ self.commit(merge=True)
170
209
  if not self._presence_sent:
171
210
  return
172
211
  self.send_last_presence(force=True, no_cache_online=True)
173
212
 
174
- def __update_jid(self, unescaped_nickname: Optional[str]):
213
+ def __update_resource(self, unescaped_nickname: Optional[str]) -> None:
175
214
  if not unescaped_nickname:
176
- self.jid = JID(self.muc.jid)
215
+ self.stored.resource = ""
177
216
  if self.is_system:
178
- self._nickname_no_illegal = ""
217
+ self.stored.nickname_no_illegal = ""
179
218
  else:
180
219
  warnings.warn(
181
220
  "Only the system participant is allowed to not have a nickname"
182
221
  )
183
222
  nickname = f"unnamed-{uuid.uuid4()}"
184
- self.jid.resource = self._nickname_no_illegal = nickname
223
+ self.stored.resource = self.stored.nickname_no_illegal = nickname
185
224
  return
186
225
 
187
- self._nickname_no_illegal, self.jid = escape_nickname(
226
+ self.stored.nickname_no_illegal, jid = escape_nickname(
188
227
  self.muc.jid,
189
228
  unescaped_nickname,
190
229
  )
230
+ self.stored.resource = jid.resource
191
231
 
192
232
  def send_configuration_change(self, codes: tuple[int]):
193
233
  if not self.is_system:
@@ -198,11 +238,11 @@ class LegacyParticipant(
198
238
 
199
239
  @property
200
240
  def nickname(self):
201
- return self._nickname
241
+ return self.stored.nickname
202
242
 
203
243
  @nickname.setter
204
- def nickname(self, new_nickname: str):
205
- old = self._nickname
244
+ def nickname(self, new_nickname: str) -> None:
245
+ old = self.nickname
206
246
  if new_nickname == old:
207
247
  return
208
248
 
@@ -219,18 +259,16 @@ class LegacyParticipant(
219
259
  p = self._make_presence(ptype="unavailable", last_seen=last_seen, **kwargs)
220
260
  # in this order so pfrom=old resource and we actually use the escaped nick
221
261
  # in the muc/item/nick element
222
- self.__update_jid(new_nickname)
262
+ self.__update_resource(new_nickname)
223
263
  p["muc"]["item"]["nick"] = self.jid.resource
224
264
  self._send(p)
225
265
 
226
- self._nickname = new_nickname
227
-
266
+ self.stored.nickname = new_nickname
267
+ self.commit()
228
268
  kwargs["status_codes"] = set()
229
269
  p = self._make_presence(ptype="available", last_seen=last_seen, **kwargs)
230
270
  self._send(p)
231
271
 
232
- self.__part_store.update(self)
233
-
234
272
  def _make_presence(
235
273
  self,
236
274
  *,
@@ -242,8 +280,8 @@ class LegacyParticipant(
242
280
  p = super()._make_presence(last_seen=last_seen, **presence_kwargs)
243
281
  p["muc"]["affiliation"] = self.affiliation
244
282
  p["muc"]["role"] = self.role
245
- if self._hats:
246
- p["hats"].add_hats(self._hats)
283
+ if self.hats:
284
+ p["hats"].add_hats(self.hats)
247
285
  codes = status_codes or set()
248
286
  if self.is_user:
249
287
  codes.add(110)
@@ -254,9 +292,7 @@ class LegacyParticipant(
254
292
  else:
255
293
  jid = JID(self.user_jid)
256
294
  try:
257
- jid.resource = next(
258
- iter(self.muc.get_user_resources()) # type:ignore
259
- )
295
+ jid.resource = next(iter(self.muc.get_user_resources()))
260
296
  except StopIteration:
261
297
  jid.resource = "pseudo-resource"
262
298
  p["muc"]["jid"] = self.user_jid
@@ -275,12 +311,12 @@ class LegacyParticipant(
275
311
  return p
276
312
 
277
313
  @property
278
- def DISCO_NAME(self):
314
+ def DISCO_NAME(self): # type:ignore[override]
279
315
  return self.nickname
280
316
 
281
317
  def __send_presence_if_needed(
282
318
  self, stanza: Union[Message, Presence], full_jid: JID, archive_only: bool
283
- ):
319
+ ) -> None:
284
320
  if (
285
321
  archive_only
286
322
  or self.is_system
@@ -309,8 +345,9 @@ class LegacyParticipant(
309
345
  self,
310
346
  stanza: MessageOrPresenceTypeVar,
311
347
  full_jid: Optional[JID] = None,
312
- archive_only=False,
348
+ archive_only: bool = False,
313
349
  legacy_msg_id=None,
350
+ initial_presence=False,
314
351
  **send_kwargs,
315
352
  ) -> MessageOrPresenceTypeVar:
316
353
  if stanza.get_from().resource:
@@ -321,8 +358,10 @@ class LegacyParticipant(
321
358
  if not self.is_user and isinstance(stanza, Presence):
322
359
  if stanza["type"] == "unavailable" and not self._presence_sent:
323
360
  return stanza # type:ignore
324
- self._presence_sent = True
325
- self.__part_store.set_presence_sent(self.pk)
361
+ if initial_presence:
362
+ self.stored.presence_sent = True
363
+ else:
364
+ self._presence_sent = True
326
365
  if full_jid:
327
366
  stanza["to"] = full_jid
328
367
  self.__send_presence_if_needed(stanza, full_jid, archive_only)
@@ -362,8 +401,8 @@ class LegacyParticipant(
362
401
  )
363
402
  return item
364
403
 
365
- def __add_nick_element(self, stanza: Union[Presence, Message]):
366
- if (nick := self._nickname_no_illegal) != self.jid.resource:
404
+ def __add_nick_element(self, stanza: Union[Presence, Message]) -> None:
405
+ if (nick := self.nickname_no_illegal) != self.jid.resource:
367
406
  n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
368
407
  n["nick"] = nick
369
408
  stanza.append(n)
@@ -377,9 +416,9 @@ class LegacyParticipant(
377
416
  def send_initial_presence(
378
417
  self,
379
418
  full_jid: JID,
380
- nick_change=False,
419
+ nick_change: bool = False,
381
420
  presence_id: Optional[str] = None,
382
- ):
421
+ ) -> None:
383
422
  """
384
423
  Called when the user joins a MUC, as a mechanism
385
424
  to indicate to the joining XMPP client the list of "participants".
@@ -418,21 +457,21 @@ class LegacyParticipant(
418
457
  )
419
458
  if presence_id:
420
459
  p["id"] = presence_id
421
- self._send(p, full_jid)
460
+ self._send(p, full_jid, initial_presence=True)
422
461
 
423
- def leave(self):
462
+ def leave(self) -> None:
424
463
  """
425
464
  Call this when the participant leaves the room
426
465
  """
427
466
  self.muc.remove_participant(self)
428
467
 
429
- def kick(self, reason: str | None = None):
468
+ def kick(self, reason: str | None = None) -> None:
430
469
  """
431
470
  Call this when the participant is kicked from the room
432
471
  """
433
472
  self.muc.remove_participant(self, kick=True, reason=reason)
434
473
 
435
- def ban(self, reason: str | None = None):
474
+ def ban(self, reason: str | None = None) -> None:
436
475
  """
437
476
  Call this when the participant is banned from the room
438
477
  """
@@ -443,15 +482,16 @@ class LegacyParticipant(
443
482
  return self.contact.get_disco_info()
444
483
  return super().get_disco_info()
445
484
 
446
- def moderate(self, legacy_msg_id: LegacyMessageType, reason: Optional[str] = None):
485
+ def moderate(
486
+ self, legacy_msg_id: LegacyMessageType, reason: Optional[str] = None
487
+ ) -> None:
447
488
  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]
489
+ with self.xmpp.store.session() as orm:
490
+ msg_ids = self.xmpp.store.id_map.get_xmpp(
491
+ orm, self.muc.stored.id, str(legacy_msg_id), True
492
+ )
453
493
 
454
- for i in msg_ids:
494
+ for i in set(msg_ids + [xmpp_id]):
455
495
  m = self.muc.get_system_participant()._make_message()
456
496
  m["retract"]["id"] = i
457
497
  if self.is_system:
@@ -468,8 +508,8 @@ class LegacyParticipant(
468
508
  subject: str,
469
509
  full_jid: Optional[JID] = None,
470
510
  when: Optional[datetime] = None,
471
- update_muc=True,
472
- ):
511
+ update_muc: bool = True,
512
+ ) -> None:
473
513
  if update_muc:
474
514
  self.muc._subject = subject # type: ignore
475
515
  self.muc.subject_setter = self.nickname
@@ -479,47 +519,13 @@ class LegacyParticipant(
479
519
  if when is not None:
480
520
  msg["delay"].set_stamp(when)
481
521
  msg["delay"]["from"] = self.muc.jid
482
- msg["subject"] = subject or str(self.muc.name)
522
+ if subject:
523
+ msg["subject"] = subject
524
+ else:
525
+ # may be simplified if slixmpp lets it do it more easily some day
526
+ msg.xml.append(ET.Element(f"{{{msg.namespace}}}subject"))
483
527
  self._send(msg, full_jid)
484
528
 
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
529
 
524
530
  def escape_nickname(muc_jid: JID, nickname: str) -> tuple[str, JID]:
525
531
  nickname = nickname_no_illegal = strip_illegal_chars(nickname)