slidge 0.1.3__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 (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__)