slidge 0.1.2__py3-none-any.whl → 0.2.0a0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -196
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +8 -1
  5. slidge/command/admin.py +5 -6
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -5
  9. slidge/contact/contact.py +93 -31
  10. slidge/contact/roster.py +54 -39
  11. slidge/core/config.py +13 -7
  12. slidge/core/gateway/base.py +139 -34
  13. slidge/core/gateway/disco.py +2 -4
  14. slidge/core/gateway/mam.py +1 -4
  15. slidge/core/gateway/ping.py +2 -3
  16. slidge/core/gateway/presence.py +1 -1
  17. slidge/core/gateway/registration.py +32 -21
  18. slidge/core/gateway/search.py +3 -5
  19. slidge/core/gateway/session_dispatcher.py +109 -51
  20. slidge/core/gateway/vcard_temp.py +6 -4
  21. slidge/core/mixins/__init__.py +11 -1
  22. slidge/core/mixins/attachment.py +15 -10
  23. slidge/core/mixins/avatar.py +66 -18
  24. slidge/core/mixins/base.py +8 -2
  25. slidge/core/mixins/message.py +11 -7
  26. slidge/core/mixins/message_maker.py +17 -9
  27. slidge/core/mixins/presence.py +14 -4
  28. slidge/core/pubsub.py +54 -212
  29. slidge/core/session.py +65 -33
  30. slidge/db/__init__.py +4 -0
  31. slidge/db/alembic/env.py +64 -0
  32. slidge/db/alembic/script.py.mako +26 -0
  33. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  34. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  35. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  36. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +76 -0
  37. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  38. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  39. slidge/db/avatar.py +224 -0
  40. slidge/db/meta.py +65 -0
  41. slidge/db/models.py +365 -0
  42. slidge/db/store.py +976 -0
  43. slidge/group/archive.py +13 -14
  44. slidge/group/bookmarks.py +59 -56
  45. slidge/group/participant.py +81 -29
  46. slidge/group/room.py +242 -142
  47. slidge/main.py +201 -0
  48. slidge/migration.py +30 -0
  49. slidge/slixfix/__init__.py +35 -2
  50. slidge/slixfix/roster.py +11 -4
  51. slidge/slixfix/xep_0292/vcard4.py +1 -0
  52. slidge/util/db.py +1 -47
  53. slidge/util/test.py +21 -4
  54. slidge/util/types.py +24 -4
  55. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/METADATA +3 -1
  56. slidge-0.2.0a0.dist-info/RECORD +108 -0
  57. slidge/core/cache.py +0 -183
  58. slidge/util/schema.sql +0 -126
  59. slidge/util/sql.py +0 -508
  60. slidge-0.1.2.dist-info/RECORD +0 -96
  61. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/LICENSE +0 -0
  62. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/WHEEL +0 -0
  63. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/entry_points.txt +0 -0
slidge/group/archive.py CHANGED
@@ -1,24 +1,22 @@
1
1
  import logging
2
2
  import uuid
3
3
  from copy import copy
4
- from datetime import datetime
4
+ from datetime import datetime, timezone
5
5
  from typing import TYPE_CHECKING, Collection, Optional
6
6
 
7
7
  from slixmpp import Iq, Message
8
8
 
9
+ from ..db.store import MAMStore
9
10
  from ..util.archive_msg import HistoryMessage
10
- from ..util.db import GatewayUser
11
- from ..util.sql import db
12
11
 
13
12
  if TYPE_CHECKING:
14
13
  from .participant import LegacyParticipant
15
14
 
16
15
 
17
16
  class MessageArchive:
18
- def __init__(self, db_id: str, user: GatewayUser):
19
- self.db_id = db_id
20
- self.user = user
21
- db.mam_add_muc(db_id, user)
17
+ def __init__(self, room_pk: int, store: MAMStore):
18
+ self.room_pk = room_pk
19
+ self.__store = store
22
20
 
23
21
  def add(
24
22
  self,
@@ -40,7 +38,7 @@ class MessageArchive:
40
38
  if participant.contact:
41
39
  new_msg["muc"]["jid"] = participant.contact.jid.bare
42
40
  elif participant.is_user:
43
- new_msg["muc"]["jid"] = participant.user.jid.bare
41
+ new_msg["muc"]["jid"] = participant.user_jid.bare
44
42
  elif participant.is_system:
45
43
  new_msg["muc"]["jid"] = participant.muc.jid
46
44
  else:
@@ -49,7 +47,7 @@ class MessageArchive:
49
47
  "jid"
50
48
  ] = f"{uuid.uuid4()}@{participant.xmpp.boundjid.bare}"
51
49
 
52
- db.mam_add_msg(self.db_id, HistoryMessage(new_msg), self.user)
50
+ self.__store.add_message(self.room_pk, HistoryMessage(new_msg))
53
51
 
54
52
  def __iter__(self):
55
53
  return iter(self.get_all())
@@ -65,9 +63,8 @@ class MessageArchive:
65
63
  sender: Optional[str] = None,
66
64
  flip=False,
67
65
  ):
68
- for msg in db.mam_get_messages(
69
- self.user,
70
- self.db_id,
66
+ for msg in self.__store.get_messages(
67
+ self.room_pk,
71
68
  before_id=before_id,
72
69
  after_id=after_id,
73
70
  ids=ids,
@@ -87,11 +84,13 @@ class MessageArchive:
87
84
  :return:
88
85
  """
89
86
  reply = iq.reply()
90
- messages = db.mam_get_first_and_last(self.db_id)
87
+ messages = self.__store.get_first_and_last(self.room_pk)
91
88
  if messages:
92
89
  for x, m in [("start", messages[0]), ("end", messages[-1])]:
93
90
  reply["mam_metadata"][x]["id"] = m.id
94
- reply["mam_metadata"][x]["timestamp"] = m.sent_on
91
+ reply["mam_metadata"][x]["timestamp"] = m.sent_on.replace(
92
+ tzinfo=timezone.utc
93
+ )
95
94
  else:
96
95
  reply.enable("mam_metadata")
97
96
  reply.send()
slidge/group/bookmarks.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import abc
2
2
  import logging
3
- from typing import TYPE_CHECKING, Generic, Type
3
+ from typing import TYPE_CHECKING, Generic, Iterator, Optional, Type
4
4
 
5
5
  from slixmpp import JID
6
6
  from slixmpp.jid import _unescape_node
@@ -9,6 +9,7 @@ from ..contact.roster import ESCAPE_TABLE
9
9
  from ..core.mixins.lock import NamedLockMixin
10
10
  from ..util import SubclassableOnce
11
11
  from ..util.types import LegacyGroupIdType, LegacyMUCType
12
+ from .archive import MessageArchive
12
13
  from .room import LegacyMUC
13
14
 
14
15
  if TYPE_CHECKING:
@@ -27,17 +28,15 @@ class LegacyBookmarks(
27
28
  def __init__(self, session: "BaseSession"):
28
29
  self.session = session
29
30
  self.xmpp = session.xmpp
30
- self.user = session.user
31
-
32
- self._mucs_by_legacy_id = dict[LegacyGroupIdType, LegacyMUCType]()
33
- self._mucs_by_bare_jid = dict[str, LegacyMUCType]()
31
+ self.user_jid = session.user_jid
32
+ self.__store = self.xmpp.store.rooms
34
33
 
35
34
  self._muc_class: Type[LegacyMUCType] = LegacyMUC.get_self_or_unique_subclass()
36
35
 
37
- self._user_nick: str = self.session.user.jid.node
36
+ self._user_nick: str = self.session.user_jid.node
38
37
 
39
38
  super().__init__()
40
- self.log = logging.getLogger(f"{self.user.bare_jid}:bookmarks")
39
+ self.log = logging.getLogger(f"{self.user_jid.bare}:bookmarks")
41
40
  self.ready = self.session.xmpp.loop.create_future()
42
41
  if not self.xmpp.GROUPS:
43
42
  self.ready.set_result(True)
@@ -50,20 +49,23 @@ class LegacyBookmarks(
50
49
  def user_nick(self, nick: str):
51
50
  self._user_nick = nick
52
51
 
53
- def __iter__(self):
54
- return iter(self._mucs_by_legacy_id.values())
52
+ def __iter__(self) -> Iterator[LegacyMUCType]:
53
+ for stored in self.__store.get_all(user_pk=self.session.user_pk):
54
+ yield self._muc_class.from_store(self.session, stored)
55
55
 
56
56
  def __repr__(self):
57
- return f"<Bookmarks of {self.user}>"
57
+ return f"<Bookmarks of {self.user_jid}>"
58
58
 
59
59
  async def __finish_init_muc(self, legacy_id: LegacyGroupIdType, jid: JID):
60
60
  muc = self._muc_class(self.session, legacy_id=legacy_id, jid=jid)
61
- await muc.avatar_wrap_update_info()
62
- if not muc.user_nick:
63
- muc.user_nick = self._user_nick
64
- self.log.debug("MUC created: %r", muc)
65
- self._mucs_by_legacy_id[muc.legacy_id] = muc
66
- self._mucs_by_bare_jid[muc.jid.bare] = muc
61
+ with self.__store.session():
62
+ muc.pk = self.__store.add(self.session.user_pk, str(muc.legacy_id), muc.jid)
63
+ muc.archive = MessageArchive(muc.pk, self.xmpp.store.mam)
64
+ await muc.avatar_wrap_update_info()
65
+ if not muc.user_nick:
66
+ muc.user_nick = self._user_nick
67
+ self.log.debug("MUC created: %r", muc)
68
+ self.__store.update(muc)
67
69
  return muc
68
70
 
69
71
  async def legacy_id_to_jid_local_part(self, legacy_id: LegacyGroupIdType):
@@ -94,36 +96,50 @@ class LegacyBookmarks(
94
96
  return _unescape_node(username)
95
97
 
96
98
  async def by_jid(self, jid: JID) -> LegacyMUCType:
99
+ if jid.resource:
100
+ jid = JID(jid.bare)
97
101
  bare = jid.bare
98
102
  async with self.lock(("bare", bare)):
99
- muc = self._mucs_by_bare_jid.get(bare)
100
- if muc is None:
101
- self.log.debug("Attempting to instantiate a new MUC for JID %s", jid)
102
- local_part = jid.node
103
- legacy_id = await self.jid_local_part_to_legacy_id(local_part)
104
- if self.get_lock(("legacy_id", legacy_id)):
105
- self.log.debug("Not instantiating %s after all", jid)
106
- return await self.by_legacy_id(legacy_id)
107
- self.log.debug("%r is group %r", local_part, legacy_id)
108
- muc = await self.__finish_init_muc(legacy_id, JID(bare))
109
- else:
110
- self.log.trace("Found an existing MUC instance: %s", muc) # type:ignore
111
- return muc
103
+ assert isinstance(jid.username, str)
104
+ legacy_id = await self.jid_local_part_to_legacy_id(jid.username)
105
+ if self.get_lock(("legacy_id", legacy_id)):
106
+ self.log.debug("Not instantiating %s after all", jid)
107
+ return await self.by_legacy_id(legacy_id)
108
+
109
+ with self.__store.session():
110
+ stored = self.__store.get_by_jid(self.session.user_pk, jid)
111
+ if stored is not None and stored.updated:
112
+ return self._muc_class.from_store(self.session, stored)
113
+
114
+ self.log.debug("Attempting to instantiate a new MUC for JID %s", jid)
115
+ local_part = jid.node
116
+
117
+ self.log.debug("%r is group %r", local_part, legacy_id)
118
+ return await self.__finish_init_muc(legacy_id, JID(bare))
119
+
120
+ def by_jid_only_if_exists(self, jid: JID) -> Optional[LegacyMUCType]:
121
+ with self.__store.session():
122
+ stored = self.__store.get_by_jid(self.session.user_pk, jid)
123
+ if stored is not None and stored.updated:
124
+ return self._muc_class.from_store(self.session, stored)
125
+ return None
112
126
 
113
127
  async def by_legacy_id(self, legacy_id: LegacyGroupIdType) -> LegacyMUCType:
114
128
  async with self.lock(("legacy_id", legacy_id)):
115
- muc = self._mucs_by_legacy_id.get(legacy_id)
116
- if muc is None:
117
- self.log.debug("Create new MUC instance for legacy ID %s", legacy_id)
118
- local = await self.legacy_id_to_jid_local_part(legacy_id)
119
- bare = f"{local}@{self.xmpp.boundjid}"
120
- jid = JID(bare)
121
- if self.get_lock(("bare", bare)):
122
- self.log.debug("Not instantiating %s after all", legacy_id)
123
- return await self.by_jid(jid)
124
- muc = await self.__finish_init_muc(legacy_id, jid)
125
- else:
126
- self.log.trace("Found an existing MUC instance: %s", muc) # type:ignore
129
+ with self.__store.session():
130
+ stored = self.__store.get_by_legacy_id(
131
+ self.session.user_pk, str(legacy_id)
132
+ )
133
+ if stored is not None and stored.updated:
134
+ return self._muc_class.from_store(self.session, stored)
135
+ self.log.debug("Create new MUC instance for legacy ID %s", legacy_id)
136
+ local = await self.legacy_id_to_jid_local_part(legacy_id)
137
+ bare = f"{local}@{self.xmpp.boundjid}"
138
+ jid = JID(bare)
139
+ if self.get_lock(("bare", bare)):
140
+ self.log.debug("Not instantiating %s after all", legacy_id)
141
+ return await self.by_jid(jid)
142
+ muc = await self.__finish_init_muc(legacy_id, jid)
127
143
 
128
144
  return muc
129
145
 
@@ -146,18 +162,5 @@ class LegacyBookmarks(
146
162
  )
147
163
 
148
164
  def remove(self, muc: LegacyMUC):
149
- try:
150
- del self._mucs_by_legacy_id[muc.legacy_id]
151
- except KeyError:
152
- self.log.warning("Removed a MUC that we didn't store by legacy ID")
153
- try:
154
- del self._mucs_by_bare_jid[muc.jid.bare]
155
- except KeyError:
156
- self.log.warning("Removed a MUC that we didn't store by JID")
157
- for part in muc._participants_by_contacts.values():
158
- try:
159
- part.contact.participants.remove(part)
160
- except KeyError:
161
- part.log.warning(
162
- "That participant wasn't stored in the contact's participants attribute"
163
- )
165
+ assert muc.pk is not None
166
+ self.__store.delete(muc.pk)
@@ -6,7 +6,7 @@ import warnings
6
6
  from copy import copy
7
7
  from datetime import datetime
8
8
  from functools import cached_property
9
- from typing import TYPE_CHECKING, Optional, Union
9
+ from typing import TYPE_CHECKING, Optional, Self, Union
10
10
 
11
11
  from slixmpp import JID, InvalidJID, Message, Presence
12
12
  from slixmpp.plugins.xep_0045.stanza import MUCAdminItem
@@ -15,10 +15,16 @@ from slixmpp.types import MessageTypes, OptJid
15
15
  from slixmpp.util.stringprep_profiles import StringPrepError, prohibit_output
16
16
 
17
17
  from ..contact import LegacyContact
18
- from ..core.mixins import ChatterDiscoMixin, MessageMixin, PresenceMixin
18
+ from ..core.mixins import (
19
+ ChatterDiscoMixin,
20
+ MessageMixin,
21
+ PresenceMixin,
22
+ StoredAttributeMixin,
23
+ )
24
+ from ..db.models import Participant
19
25
  from ..util import SubclassableOnce, strip_illegal_chars
20
- from ..util.sql import CachedPresence
21
26
  from ..util.types import (
27
+ CachedPresence,
22
28
  Hat,
23
29
  LegacyMessageType,
24
30
  MessageOrPresenceTypeVar,
@@ -40,6 +46,7 @@ def strip_non_printable(nickname: str):
40
46
 
41
47
 
42
48
  class LegacyParticipant(
49
+ StoredAttributeMixin,
43
50
  PresenceMixin,
44
51
  MessageMixin,
45
52
  ChatterDiscoMixin,
@@ -53,6 +60,7 @@ class LegacyParticipant(
53
60
  _can_send_carbon = False
54
61
  USE_STANZA_ID = True
55
62
  STRIP_SHORT_DELAY = False
63
+ pk: int
56
64
 
57
65
  def __init__(
58
66
  self,
@@ -63,12 +71,11 @@ class LegacyParticipant(
63
71
  role: MucRole = "participant",
64
72
  affiliation: MucAffiliation = "member",
65
73
  ):
74
+ self.session = session = muc.session
75
+ self.xmpp = session.xmpp
66
76
  super().__init__()
67
77
  self._hats = list[Hat]()
68
78
  self.muc = muc
69
- self.session = session = muc.session
70
- self.user = session.user
71
- self.xmpp = session.xmpp
72
79
  self._role = role
73
80
  self._affiliation = affiliation
74
81
  self.is_user: bool = is_user
@@ -84,8 +91,19 @@ class LegacyParticipant(
84
91
  # if we didn't, we send it before the first message.
85
92
  # this way, event in plugins that don't map "user has joined" events,
86
93
  # we send a "join"-presence from the participant before the first message
87
- self.__presence_sent = False
88
- self.log = logging.getLogger(f"{self.user.bare_jid}:{self.jid}")
94
+ self._presence_sent: bool = False
95
+ self.log = logging.getLogger(f"{self.user_jid.bare}:{self.jid}")
96
+ self.__part_store = self.xmpp.store.participants
97
+
98
+ @property
99
+ def contact_pk(self) -> Optional[int]: # type:ignore
100
+ if self.contact:
101
+ return self.contact.contact_pk
102
+ return None
103
+
104
+ @property
105
+ def user_jid(self):
106
+ return self.session.user_jid
89
107
 
90
108
  def __repr__(self):
91
109
  return f"<Participant '{self.nickname}'/'{self.jid}' of '{self.muc}'>"
@@ -99,7 +117,8 @@ class LegacyParticipant(
99
117
  if self._affiliation == affiliation:
100
118
  return
101
119
  self._affiliation = affiliation
102
- if not self.__presence_sent:
120
+ self.__part_store.set_affiliation(self.pk, affiliation)
121
+ if not self._presence_sent:
103
122
  return
104
123
  self.send_last_presence(force=True, no_cache_online=True)
105
124
 
@@ -126,7 +145,8 @@ class LegacyParticipant(
126
145
  if self._role == role:
127
146
  return
128
147
  self._role = role
129
- if not self.__presence_sent:
148
+ self.__part_store.set_role(self.pk, role)
149
+ if not self._presence_sent:
130
150
  return
131
151
  self.send_last_presence(force=True, no_cache_online=True)
132
152
 
@@ -134,7 +154,8 @@ class LegacyParticipant(
134
154
  if self._hats == hats:
135
155
  return
136
156
  self._hats = hats
137
- if not self.__presence_sent:
157
+ self.__part_store.set_hats(self.pk, hats)
158
+ if not self._presence_sent:
138
159
  return
139
160
  self.send_last_presence(force=True, no_cache_online=True)
140
161
 
@@ -143,6 +164,7 @@ class LegacyParticipant(
143
164
 
144
165
  if self.is_system:
145
166
  self.jid = j
167
+ self._nickname_no_illegal = ""
146
168
  return
147
169
 
148
170
  nickname = unescaped_nickname
@@ -170,9 +192,6 @@ class LegacyParticipant(
170
192
  except InvalidJID:
171
193
  j.resource = strip_non_printable(nickname)
172
194
 
173
- if nickname != unescaped_nickname:
174
- self.muc._participants_by_escaped_nicknames[nickname] = self # type:ignore
175
-
176
195
  self.jid = j
177
196
 
178
197
  def send_configuration_change(self, codes: tuple[int]):
@@ -213,12 +232,8 @@ class LegacyParticipant(
213
232
 
214
233
  kwargs["status_codes"] = set()
215
234
  p = self._make_presence(ptype="available", last_seen=last_seen, **kwargs)
216
- self.__add_nick_element(p)
217
235
  self._send(p)
218
236
 
219
- if old:
220
- self.muc.rename_participant(old, new_nickname)
221
-
222
237
  def _make_presence(
223
238
  self,
224
239
  *,
@@ -240,14 +255,14 @@ class LegacyParticipant(
240
255
  if user_full_jid:
241
256
  p["muc"]["jid"] = user_full_jid
242
257
  else:
243
- jid = copy(self.user.jid)
258
+ jid = copy(self.user_jid)
244
259
  try:
245
260
  jid.resource = next(
246
- iter(self.muc.user_resources) # type:ignore
261
+ iter(self.muc.get_user_resources()) # type:ignore
247
262
  )
248
263
  except StopIteration:
249
264
  jid.resource = "pseudo-resource"
250
- p["muc"]["jid"] = self.user.jid
265
+ p["muc"]["jid"] = self.user_jid
251
266
  codes.add(100)
252
267
  elif self.contact:
253
268
  p["muc"]["jid"] = self.contact.jid
@@ -257,7 +272,7 @@ class LegacyParticipant(
257
272
  warnings.warn(
258
273
  f"Private group but no 1:1 JID associated to '{self}'",
259
274
  )
260
- if self.is_user and (hash_ := self.session.avatar_hash):
275
+ if self.is_user and (hash_ := self.session.user.avatar_hash):
261
276
  p["vcard_temp_update"]["photo"] = hash_
262
277
  p["muc"]["status_codes"] = codes
263
278
  return p
@@ -273,7 +288,7 @@ class LegacyParticipant(
273
288
  archive_only
274
289
  or self.is_system
275
290
  or self.is_user
276
- or self.__presence_sent
291
+ or self._presence_sent
277
292
  or stanza["subject"]
278
293
  ):
279
294
  return
@@ -301,10 +316,12 @@ class LegacyParticipant(
301
316
  **send_kwargs,
302
317
  ) -> MessageOrPresenceTypeVar:
303
318
  stanza["occupant-id"]["id"] = self.__occupant_id
319
+ self.__add_nick_element(stanza)
304
320
  if isinstance(stanza, Presence):
305
- if stanza["type"] == "unavailable" and not self.__presence_sent:
321
+ if stanza["type"] == "unavailable" and not self._presence_sent:
306
322
  return stanza # type:ignore
307
- self.__presence_sent = True
323
+ self._presence_sent = True
324
+ self.__part_store.set_presence_sent(self.pk)
308
325
  if full_jid:
309
326
  stanza["to"] = full_jid
310
327
  self.__send_presence_if_needed(stanza, full_jid, archive_only)
@@ -332,7 +349,7 @@ class LegacyParticipant(
332
349
  item["role"] = self.role
333
350
  if not self.muc.is_anonymous:
334
351
  if self.is_user:
335
- item["jid"] = self.user.bare_jid
352
+ item["jid"] = self.user_jid.bare
336
353
  elif self.contact:
337
354
  item["jid"] = self.contact.jid.bare
338
355
  else:
@@ -344,11 +361,11 @@ class LegacyParticipant(
344
361
  )
345
362
  return item
346
363
 
347
- def __add_nick_element(self, p: Presence):
364
+ def __add_nick_element(self, stanza: Union[Presence, Message]):
348
365
  if (nick := self._nickname_no_illegal) != self.jid.resource:
349
366
  n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
350
367
  n["nick"] = nick
351
- p.append(n)
368
+ stanza.append(n)
352
369
 
353
370
  def _get_last_presence(self) -> Optional[CachedPresence]:
354
371
  own = super()._get_last_presence()
@@ -400,7 +417,6 @@ class LegacyParticipant(
400
417
  )
401
418
  if presence_id:
402
419
  p["id"] = presence_id
403
- self.__add_nick_element(p)
404
420
  self._send(p, full_jid)
405
421
 
406
422
  def leave(self):
@@ -454,5 +470,41 @@ class LegacyParticipant(
454
470
  msg["subject"] = subject or str(self.muc.name)
455
471
  self._send(msg, full_jid)
456
472
 
473
+ @classmethod
474
+ def from_store(
475
+ cls,
476
+ session,
477
+ stored: Participant,
478
+ contact: Optional[LegacyContact] = None,
479
+ muc: Optional["LegacyMUC"] = None,
480
+ ) -> Self:
481
+ from slidge.group.room import LegacyMUC
482
+
483
+ if muc is None:
484
+ muc = LegacyMUC.get_self_or_unique_subclass().from_store(
485
+ session, stored.room
486
+ )
487
+ part = cls(
488
+ muc,
489
+ stored.nickname,
490
+ role=stored.role,
491
+ affiliation=stored.affiliation,
492
+ )
493
+ part.pk = stored.id
494
+ if contact is not None:
495
+ part.contact = contact
496
+ elif stored.contact is not None:
497
+ contact = LegacyContact.get_self_or_unique_subclass().from_store(
498
+ session, stored.contact
499
+ )
500
+ part.contact = contact
501
+
502
+ part.is_user = stored.is_user
503
+ if (data := stored.extra_attributes) is not None:
504
+ muc.deserialize_extra_attributes(data)
505
+ part._presence_sent = stored.presence_sent
506
+ part._hats = [Hat(h.uri, h.title) for h in stored.hats]
507
+ return part
508
+
457
509
 
458
510
  log = logging.getLogger(__name__)