slidge 0.1.3__py3-none-any.whl → 0.2.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 (74) 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 +6 -7
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -6
  9. slidge/contact/contact.py +165 -49
  10. slidge/contact/roster.py +122 -47
  11. slidge/core/config.py +14 -11
  12. slidge/core/gateway/base.py +148 -36
  13. slidge/core/gateway/caps.py +7 -5
  14. slidge/core/gateway/disco.py +2 -4
  15. slidge/core/gateway/mam.py +1 -4
  16. slidge/core/gateway/muc_admin.py +1 -1
  17. slidge/core/gateway/ping.py +2 -3
  18. slidge/core/gateway/presence.py +1 -1
  19. slidge/core/gateway/registration.py +32 -21
  20. slidge/core/gateway/search.py +3 -5
  21. slidge/core/gateway/session_dispatcher.py +120 -57
  22. slidge/core/gateway/vcard_temp.py +7 -5
  23. slidge/core/mixins/__init__.py +11 -1
  24. slidge/core/mixins/attachment.py +32 -14
  25. slidge/core/mixins/avatar.py +90 -25
  26. slidge/core/mixins/base.py +8 -2
  27. slidge/core/mixins/db.py +18 -0
  28. slidge/core/mixins/disco.py +0 -10
  29. slidge/core/mixins/message.py +18 -8
  30. slidge/core/mixins/message_maker.py +17 -9
  31. slidge/core/mixins/presence.py +17 -4
  32. slidge/core/pubsub.py +54 -220
  33. slidge/core/session.py +69 -34
  34. slidge/db/__init__.py +4 -0
  35. slidge/db/alembic/env.py +64 -0
  36. slidge/db/alembic/script.py.mako +26 -0
  37. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  38. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  39. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  40. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  41. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  42. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  43. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +85 -0
  44. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  45. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
  46. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  47. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  48. slidge/db/avatar.py +235 -0
  49. slidge/db/meta.py +65 -0
  50. slidge/db/models.py +375 -0
  51. slidge/db/store.py +1078 -0
  52. slidge/group/archive.py +58 -14
  53. slidge/group/bookmarks.py +72 -57
  54. slidge/group/participant.py +87 -28
  55. slidge/group/room.py +369 -211
  56. slidge/main.py +201 -0
  57. slidge/migration.py +30 -0
  58. slidge/slixfix/__init__.py +35 -2
  59. slidge/slixfix/roster.py +11 -4
  60. slidge/slixfix/xep_0292/vcard4.py +3 -0
  61. slidge/util/archive_msg.py +2 -1
  62. slidge/util/db.py +1 -47
  63. slidge/util/test.py +71 -4
  64. slidge/util/types.py +29 -4
  65. slidge/util/util.py +22 -0
  66. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/METADATA +4 -4
  67. slidge-0.2.0a1.dist-info/RECORD +114 -0
  68. slidge/core/cache.py +0 -183
  69. slidge/util/schema.sql +0 -126
  70. slidge/util/sql.py +0 -508
  71. slidge-0.1.3.dist-info/RECORD +0 -96
  72. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/LICENSE +0 -0
  73. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/WHEEL +0 -0
  74. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/entry_points.txt +0 -0
slidge/group/archive.py CHANGED
@@ -1,35 +1,39 @@
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.models import ArchivedMessage, ArchivedMessageSource
10
+ from ..db.store import MAMStore
9
11
  from ..util.archive_msg import HistoryMessage
10
- from ..util.db import GatewayUser
11
- from ..util.sql import db
12
+ from ..util.types import HoleBound
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from .participant import LegacyParticipant
15
16
 
16
17
 
17
18
  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)
19
+ def __init__(self, room_pk: int, store: MAMStore):
20
+ self.room_pk = room_pk
21
+ self.__store = store
22
22
 
23
23
  def add(
24
24
  self,
25
25
  msg: Message,
26
26
  participant: Optional["LegacyParticipant"] = None,
27
+ archive_only=False,
28
+ legacy_msg_id=None,
27
29
  ):
28
30
  """
29
31
  Add a message to the archive if it is deemed archivable
30
32
 
31
33
  :param msg:
32
34
  :param participant:
35
+ :param archive_only:
36
+ :param legacy_msg_id:
33
37
  """
34
38
  if not archivable(msg):
35
39
  return
@@ -40,7 +44,7 @@ class MessageArchive:
40
44
  if participant.contact:
41
45
  new_msg["muc"]["jid"] = participant.contact.jid.bare
42
46
  elif participant.is_user:
43
- new_msg["muc"]["jid"] = participant.user.jid.bare
47
+ new_msg["muc"]["jid"] = participant.user_jid.bare
44
48
  elif participant.is_system:
45
49
  new_msg["muc"]["jid"] = participant.muc.jid
46
50
  else:
@@ -49,11 +53,50 @@ class MessageArchive:
49
53
  "jid"
50
54
  ] = f"{uuid.uuid4()}@{participant.xmpp.boundjid.bare}"
51
55
 
52
- db.mam_add_msg(self.db_id, HistoryMessage(new_msg), self.user)
56
+ self.__store.add_message(
57
+ self.room_pk,
58
+ HistoryMessage(new_msg),
59
+ archive_only,
60
+ None if legacy_msg_id is None else str(legacy_msg_id),
61
+ )
53
62
 
54
63
  def __iter__(self):
55
64
  return iter(self.get_all())
56
65
 
66
+ @staticmethod
67
+ def __to_bound(stored: ArchivedMessage):
68
+ return HoleBound(
69
+ stored.legacy_id, # type:ignore
70
+ stored.timestamp.replace(tzinfo=timezone.utc),
71
+ )
72
+
73
+ def get_hole_bounds(self) -> tuple[HoleBound | None, HoleBound | None]:
74
+ most_recent = self.__store.get_most_recent_with_legacy_id(self.room_pk)
75
+ if most_recent is None:
76
+ return None, None
77
+ if most_recent.source == ArchivedMessageSource.BACKFILL:
78
+ # most recent = only backfill, fetch everything since last backfill
79
+ return self.__to_bound(most_recent), None
80
+
81
+ most_recent_back_filled = self.__store.get_most_recent_with_legacy_id(
82
+ self.room_pk, ArchivedMessageSource.BACKFILL
83
+ )
84
+ if most_recent_back_filled is None:
85
+ # group was never back-filled, fetch everything before first live
86
+ least_recent_live = self.__store.get_first(self.room_pk, True)
87
+ assert least_recent_live is not None
88
+ return None, self.__to_bound(least_recent_live)
89
+
90
+ assert most_recent_back_filled.legacy_id is not None
91
+ least_recent_live = self.__store.get_least_recent_with_legacy_id_after(
92
+ self.room_pk, most_recent_back_filled.legacy_id
93
+ )
94
+ assert least_recent_live is not None
95
+ # this is a hole caused by slidge downtime
96
+ return self.__to_bound(most_recent_back_filled), self.__to_bound(
97
+ least_recent_live
98
+ )
99
+
57
100
  def get_all(
58
101
  self,
59
102
  start_date: Optional[datetime] = None,
@@ -65,9 +108,8 @@ class MessageArchive:
65
108
  sender: Optional[str] = None,
66
109
  flip=False,
67
110
  ):
68
- for msg in db.mam_get_messages(
69
- self.user,
70
- self.db_id,
111
+ for msg in self.__store.get_messages(
112
+ self.room_pk,
71
113
  before_id=before_id,
72
114
  after_id=after_id,
73
115
  ids=ids,
@@ -87,11 +129,13 @@ class MessageArchive:
87
129
  :return:
88
130
  """
89
131
  reply = iq.reply()
90
- messages = db.mam_get_first_and_last(self.db_id)
132
+ messages = self.__store.get_first_and_last(self.room_pk)
91
133
  if messages:
92
134
  for x, m in [("start", messages[0]), ("end", messages[-1])]:
93
135
  reply["mam_metadata"][x]["id"] = m.id
94
- reply["mam_metadata"][x]["timestamp"] = m.sent_on
136
+ reply["mam_metadata"][x]["timestamp"] = m.sent_on.replace(
137
+ tzinfo=timezone.utc
138
+ )
95
139
  else:
96
140
  reply.enable("mam_metadata")
97
141
  reply.send()
slidge/group/bookmarks.py CHANGED
@@ -1,14 +1,16 @@
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
+ from slixmpp.exceptions import XMPPError
6
7
  from slixmpp.jid import _unescape_node
7
8
 
8
9
  from ..contact.roster import ESCAPE_TABLE
9
10
  from ..core.mixins.lock import NamedLockMixin
10
11
  from ..util import SubclassableOnce
11
12
  from ..util.types import LegacyGroupIdType, LegacyMUCType
13
+ from .archive import MessageArchive
12
14
  from .room import LegacyMUC
13
15
 
14
16
  if TYPE_CHECKING:
@@ -27,17 +29,15 @@ class LegacyBookmarks(
27
29
  def __init__(self, session: "BaseSession"):
28
30
  self.session = session
29
31
  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]()
32
+ self.user_jid = session.user_jid
33
+ self.__store = self.xmpp.store.rooms
34
34
 
35
35
  self._muc_class: Type[LegacyMUCType] = LegacyMUC.get_self_or_unique_subclass()
36
36
 
37
- self._user_nick: str = self.session.user.jid.node
37
+ self._user_nick: str = self.session.user_jid.node
38
38
 
39
39
  super().__init__()
40
- self.log = logging.getLogger(f"{self.user.bare_jid}:bookmarks")
40
+ self.log = logging.getLogger(f"{self.user_jid.bare}:bookmarks")
41
41
  self.ready = self.session.xmpp.loop.create_future()
42
42
  if not self.xmpp.GROUPS:
43
43
  self.ready.set_result(True)
@@ -50,20 +50,34 @@ class LegacyBookmarks(
50
50
  def user_nick(self, nick: str):
51
51
  self._user_nick = nick
52
52
 
53
- def __iter__(self):
54
- return iter(self._mucs_by_legacy_id.values())
53
+ def __iter__(self) -> Iterator[LegacyMUCType]:
54
+ for stored in self.__store.get_all(user_pk=self.session.user_pk):
55
+ yield self._muc_class.from_store(self.session, stored)
55
56
 
56
57
  def __repr__(self):
57
- return f"<Bookmarks of {self.user}>"
58
+ return f"<Bookmarks of {self.user_jid}>"
58
59
 
59
60
  async def __finish_init_muc(self, legacy_id: LegacyGroupIdType, jid: JID):
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
+ stored = self.__store.get_by_legacy_id(self.session.user_pk, str(legacy_id))
63
+ if stored is not None:
64
+ if stored.updated:
65
+ return self._muc_class.from_store(self.session, stored)
66
+ muc = self._muc_class(self.session, legacy_id=legacy_id, jid=jid)
67
+ muc.pk = stored.id
68
+ else:
69
+ muc = self._muc_class(self.session, legacy_id=legacy_id, jid=jid)
70
+
71
+ try:
72
+ with muc.updating_info():
73
+ await muc.avatar_wrap_update_info()
74
+ except Exception as e:
75
+ raise XMPPError("internal-server-error", str(e))
76
+ if not muc.user_nick:
77
+ muc.user_nick = self._user_nick
78
+ self.log.debug("MUC created: %r", muc)
79
+ muc.pk = self.__store.update(muc)
80
+ muc.archive = MessageArchive(muc.pk, self.xmpp.store.mam)
67
81
  return muc
68
82
 
69
83
  async def legacy_id_to_jid_local_part(self, legacy_id: LegacyGroupIdType):
@@ -94,36 +108,50 @@ class LegacyBookmarks(
94
108
  return _unescape_node(username)
95
109
 
96
110
  async def by_jid(self, jid: JID) -> LegacyMUCType:
111
+ if jid.resource:
112
+ jid = JID(jid.bare)
97
113
  bare = jid.bare
98
114
  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
115
+ assert isinstance(jid.username, str)
116
+ legacy_id = await self.jid_local_part_to_legacy_id(jid.username)
117
+ if self.get_lock(("legacy_id", legacy_id)):
118
+ self.log.debug("Not instantiating %s after all", jid)
119
+ return await self.by_legacy_id(legacy_id)
120
+
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
+
126
+ self.log.debug("Attempting to instantiate a new MUC for JID %s", jid)
127
+ local_part = jid.node
128
+
129
+ self.log.debug("%r is group %r", local_part, legacy_id)
130
+ return await self.__finish_init_muc(legacy_id, JID(bare))
131
+
132
+ def by_jid_only_if_exists(self, jid: JID) -> Optional[LegacyMUCType]:
133
+ with self.__store.session():
134
+ stored = self.__store.get_by_jid(self.session.user_pk, jid)
135
+ if stored is not None and stored.updated:
136
+ return self._muc_class.from_store(self.session, stored)
137
+ return None
112
138
 
113
139
  async def by_legacy_id(self, legacy_id: LegacyGroupIdType) -> LegacyMUCType:
114
140
  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
141
+ with self.__store.session():
142
+ stored = self.__store.get_by_legacy_id(
143
+ self.session.user_pk, str(legacy_id)
144
+ )
145
+ if stored is not None and stored.updated:
146
+ return self._muc_class.from_store(self.session, stored)
147
+ self.log.debug("Create new MUC instance for legacy ID %s", legacy_id)
148
+ local = await self.legacy_id_to_jid_local_part(legacy_id)
149
+ bare = f"{local}@{self.xmpp.boundjid}"
150
+ jid = JID(bare)
151
+ if self.get_lock(("bare", bare)):
152
+ self.log.debug("Not instantiating %s after all", legacy_id)
153
+ return await self.by_jid(jid)
154
+ muc = await self.__finish_init_muc(legacy_id, jid)
127
155
 
128
156
  return muc
129
157
 
@@ -146,18 +174,5 @@ class LegacyBookmarks(
146
174
  )
147
175
 
148
176
  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
- )
177
+ assert muc.pk is not None
178
+ 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,10 @@ class LegacyParticipant(
99
117
  if self._affiliation == affiliation:
100
118
  return
101
119
  self._affiliation = affiliation
102
- if not self.__presence_sent:
120
+ if not self.muc._participants_filled:
121
+ return
122
+ self.__part_store.set_affiliation(self.pk, affiliation)
123
+ if not self._presence_sent:
103
124
  return
104
125
  self.send_last_presence(force=True, no_cache_online=True)
105
126
 
@@ -126,7 +147,10 @@ class LegacyParticipant(
126
147
  if self._role == role:
127
148
  return
128
149
  self._role = role
129
- if not self.__presence_sent:
150
+ if not self.muc._participants_filled:
151
+ return
152
+ self.__part_store.set_role(self.pk, role)
153
+ if not self._presence_sent:
130
154
  return
131
155
  self.send_last_presence(force=True, no_cache_online=True)
132
156
 
@@ -134,7 +158,10 @@ class LegacyParticipant(
134
158
  if self._hats == hats:
135
159
  return
136
160
  self._hats = hats
137
- if not self.__presence_sent:
161
+ if not self.muc._participants_filled:
162
+ return
163
+ self.__part_store.set_hats(self.pk, hats)
164
+ if not self._presence_sent:
138
165
  return
139
166
  self.send_last_presence(force=True, no_cache_online=True)
140
167
 
@@ -171,9 +198,6 @@ class LegacyParticipant(
171
198
  except InvalidJID:
172
199
  j.resource = strip_non_printable(nickname)
173
200
 
174
- if nickname != unescaped_nickname:
175
- self.muc._participants_by_escaped_nicknames[nickname] = self # type:ignore
176
-
177
201
  self.jid = j
178
202
 
179
203
  def send_configuration_change(self, codes: tuple[int]):
@@ -216,9 +240,6 @@ class LegacyParticipant(
216
240
  p = self._make_presence(ptype="available", last_seen=last_seen, **kwargs)
217
241
  self._send(p)
218
242
 
219
- if old:
220
- self.muc.rename_participant(old, new_nickname)
221
-
222
243
  def _make_presence(
223
244
  self,
224
245
  *,
@@ -240,14 +261,14 @@ class LegacyParticipant(
240
261
  if user_full_jid:
241
262
  p["muc"]["jid"] = user_full_jid
242
263
  else:
243
- jid = copy(self.user.jid)
264
+ jid = copy(self.user_jid)
244
265
  try:
245
266
  jid.resource = next(
246
- iter(self.muc.user_resources) # type:ignore
267
+ iter(self.muc.get_user_resources()) # type:ignore
247
268
  )
248
269
  except StopIteration:
249
270
  jid.resource = "pseudo-resource"
250
- p["muc"]["jid"] = self.user.jid
271
+ p["muc"]["jid"] = self.user_jid
251
272
  codes.add(100)
252
273
  elif self.contact:
253
274
  p["muc"]["jid"] = self.contact.jid
@@ -257,7 +278,7 @@ class LegacyParticipant(
257
278
  warnings.warn(
258
279
  f"Private group but no 1:1 JID associated to '{self}'",
259
280
  )
260
- if self.is_user and (hash_ := self.session.avatar_hash):
281
+ if self.is_user and (hash_ := self.session.user.avatar_hash):
261
282
  p["vcard_temp_update"]["photo"] = hash_
262
283
  p["muc"]["status_codes"] = codes
263
284
  return p
@@ -273,7 +294,7 @@ class LegacyParticipant(
273
294
  archive_only
274
295
  or self.is_system
275
296
  or self.is_user
276
- or self.__presence_sent
297
+ or self._presence_sent
277
298
  or stanza["subject"]
278
299
  ):
279
300
  return
@@ -298,14 +319,16 @@ class LegacyParticipant(
298
319
  stanza: MessageOrPresenceTypeVar,
299
320
  full_jid: Optional[JID] = None,
300
321
  archive_only=False,
322
+ legacy_msg_id=None,
301
323
  **send_kwargs,
302
324
  ) -> MessageOrPresenceTypeVar:
303
325
  stanza["occupant-id"]["id"] = self.__occupant_id
304
326
  self.__add_nick_element(stanza)
305
327
  if isinstance(stanza, Presence):
306
- if stanza["type"] == "unavailable" and not self.__presence_sent:
328
+ if stanza["type"] == "unavailable" and not self._presence_sent:
307
329
  return stanza # type:ignore
308
- self.__presence_sent = True
330
+ self._presence_sent = True
331
+ self.__part_store.set_presence_sent(self.pk)
309
332
  if full_jid:
310
333
  stanza["to"] = full_jid
311
334
  self.__send_presence_if_needed(stanza, full_jid, archive_only)
@@ -315,8 +338,8 @@ class LegacyParticipant(
315
338
  else:
316
339
  stanza.send()
317
340
  else:
318
- if isinstance(stanza, Message):
319
- self.muc.archive.add(stanza, self)
341
+ if hasattr(self.muc, "archive") and isinstance(stanza, Message):
342
+ self.muc.archive.add(stanza, self, archive_only, legacy_msg_id)
320
343
  if archive_only:
321
344
  return stanza
322
345
  for user_full_jid in self.muc.user_full_jids():
@@ -333,7 +356,7 @@ class LegacyParticipant(
333
356
  item["role"] = self.role
334
357
  if not self.muc.is_anonymous:
335
358
  if self.is_user:
336
- item["jid"] = self.user.bare_jid
359
+ item["jid"] = self.user_jid.bare
337
360
  elif self.contact:
338
361
  item["jid"] = self.contact.jid.bare
339
362
  else:
@@ -444,7 +467,7 @@ class LegacyParticipant(
444
467
  ):
445
468
  if update_muc:
446
469
  self.muc._subject = subject # type: ignore
447
- self.muc.subject_setter = self
470
+ self.muc.subject_setter = self.nickname
448
471
  self.muc.subject_date = when
449
472
 
450
473
  msg = self._make_message()
@@ -454,5 +477,41 @@ class LegacyParticipant(
454
477
  msg["subject"] = subject or str(self.muc.name)
455
478
  self._send(msg, full_jid)
456
479
 
480
+ @classmethod
481
+ def from_store(
482
+ cls,
483
+ session,
484
+ stored: Participant,
485
+ contact: Optional[LegacyContact] = None,
486
+ muc: Optional["LegacyMUC"] = None,
487
+ ) -> Self:
488
+ from slidge.group.room import LegacyMUC
489
+
490
+ if muc is None:
491
+ muc = LegacyMUC.get_self_or_unique_subclass().from_store(
492
+ session, stored.room
493
+ )
494
+ part = cls(
495
+ muc,
496
+ stored.nickname,
497
+ role=stored.role,
498
+ affiliation=stored.affiliation,
499
+ )
500
+ part.pk = stored.id
501
+ if contact is not None:
502
+ part.contact = contact
503
+ elif stored.contact is not None:
504
+ contact = LegacyContact.get_self_or_unique_subclass().from_store(
505
+ session, stored.contact
506
+ )
507
+ part.contact = contact
508
+
509
+ part.is_user = stored.is_user
510
+ if (data := stored.extra_attributes) is not None:
511
+ muc.deserialize_extra_attributes(data)
512
+ part._presence_sent = stored.presence_sent
513
+ part._hats = [Hat(h.uri, h.title) for h in stored.hats]
514
+ return part
515
+
457
516
 
458
517
  log = logging.getLogger(__name__)