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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +111 -44
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -795
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.2.dist-info/RECORD +0 -96
  100. slidge-0.1.2.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.2.dist-info → slidge-0.2.0.dist-info}/WHEEL +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,17 @@
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
11
+ from ..db.models import Room
10
12
  from ..util import SubclassableOnce
11
13
  from ..util.types import LegacyGroupIdType, LegacyMUCType
14
+ from .archive import MessageArchive
12
15
  from .room import LegacyMUC
13
16
 
14
17
  if TYPE_CHECKING:
@@ -27,17 +30,15 @@ class LegacyBookmarks(
27
30
  def __init__(self, session: "BaseSession"):
28
31
  self.session = session
29
32
  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]()
33
+ self.user_jid = session.user_jid
34
+ self.__store = self.xmpp.store.rooms
34
35
 
35
36
  self._muc_class: Type[LegacyMUCType] = LegacyMUC.get_self_or_unique_subclass()
36
37
 
37
- self._user_nick: str = self.session.user.jid.node
38
+ self._user_nick: str = self.session.user_jid.node
38
39
 
39
40
  super().__init__()
40
- self.log = logging.getLogger(f"{self.user.bare_jid}:bookmarks")
41
+ self.log = logging.getLogger(f"{self.user_jid.bare}:bookmarks")
41
42
  self.ready = self.session.xmpp.loop.create_future()
42
43
  if not self.xmpp.GROUPS:
43
44
  self.ready.set_result(True)
@@ -50,21 +51,12 @@ class LegacyBookmarks(
50
51
  def user_nick(self, nick: str):
51
52
  self._user_nick = nick
52
53
 
53
- def __iter__(self):
54
- return iter(self._mucs_by_legacy_id.values())
54
+ def __iter__(self) -> Iterator[LegacyMUCType]:
55
+ for stored in self.__store.get_all(user_pk=self.session.user_pk):
56
+ yield self._muc_class.from_store(self.session, stored)
55
57
 
56
58
  def __repr__(self):
57
- return f"<Bookmarks of {self.user}>"
58
-
59
- 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
67
- return muc
59
+ return f"<Bookmarks of {self.user_jid}>"
68
60
 
69
61
  async def legacy_id_to_jid_local_part(self, legacy_id: LegacyGroupIdType):
70
62
  return await self.legacy_id_to_jid_username(legacy_id)
@@ -94,38 +86,63 @@ class LegacyBookmarks(
94
86
  return _unescape_node(username)
95
87
 
96
88
  async def by_jid(self, jid: JID) -> LegacyMUCType:
97
- bare = jid.bare
98
- 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
89
+ if jid.resource:
90
+ jid = JID(jid.bare)
91
+ async with self.lock(("bare", jid.bare)):
92
+ assert isinstance(jid.local, str)
93
+ legacy_id = await self.jid_local_part_to_legacy_id(jid.local)
94
+ if self.get_lock(("legacy_id", legacy_id)):
95
+ self.log.debug("Not instantiating %s after all", jid)
96
+ return await self.by_legacy_id(legacy_id)
97
+
98
+ with self.__store.session():
99
+ stored = self.__store.get_by_jid(self.session.user_pk, jid)
100
+ return await self.__update_muc(stored, legacy_id, jid)
101
+
102
+ def by_jid_only_if_exists(self, jid: JID) -> Optional[LegacyMUCType]:
103
+ with self.__store.session():
104
+ stored = self.__store.get_by_jid(self.session.user_pk, jid)
105
+ if stored is not None and stored.updated:
106
+ return self._muc_class.from_store(self.session, stored)
107
+ return None
112
108
 
113
109
  async def by_legacy_id(self, legacy_id: LegacyGroupIdType) -> LegacyMUCType:
114
110
  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
127
-
128
- return muc
111
+ local = await self.legacy_id_to_jid_local_part(legacy_id)
112
+ jid = JID(f"{local}@{self.xmpp.boundjid}")
113
+ if self.get_lock(("bare", jid.bare)):
114
+ self.log.debug("Not instantiating %s after all", legacy_id)
115
+ return await self.by_jid(jid)
116
+
117
+ with self.__store.session():
118
+ stored = self.__store.get_by_legacy_id(
119
+ self.session.user_pk, str(legacy_id)
120
+ )
121
+ return await self.__update_muc(stored, legacy_id, jid)
122
+
123
+ async def __update_muc(
124
+ self, stored: Room | None, legacy_id: LegacyGroupIdType, jid: JID
125
+ ):
126
+ if stored is None:
127
+ muc = self._muc_class(self.session, legacy_id=legacy_id, jid=jid)
128
+ else:
129
+ muc = self._muc_class.from_store(self.session, stored)
130
+ if stored.updated:
131
+ return muc
132
+
133
+ try:
134
+ with muc.updating_info():
135
+ await muc.avatar_wrap_update_info()
136
+ except XMPPError:
137
+ raise
138
+ except Exception as e:
139
+ raise XMPPError("internal-server-error", str(e))
140
+ if not muc.user_nick:
141
+ muc.user_nick = self._user_nick
142
+ self.log.debug("MUC created: %r", muc)
143
+ muc.pk = self.__store.update(muc)
144
+ muc.archive = MessageArchive(muc.pk, self.xmpp.store.mam)
145
+ return muc
129
146
 
130
147
  @abc.abstractmethod
131
148
  async def fill(self):
@@ -145,19 +162,26 @@ class LegacyBookmarks(
145
162
  " LegacyBookmarks.fill() was not overridden."
146
163
  )
147
164
 
148
- 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
+ async def remove(
166
+ self,
167
+ muc: LegacyMUC,
168
+ reason="You left this group from the official client.",
169
+ kick=True,
170
+ ) -> None:
171
+ """
172
+ Delete everything about a specific group.
173
+
174
+ This should be called when the user leaves the group from the official
175
+ app.
176
+
177
+ :param muc: The MUC to remove.
178
+ :param reason: Optionally, a reason why this group was removed.
179
+ :param kick: Whether the user should be kicked from this group. Set this
180
+ to False in case you do this somewhere else in your code, eg, on
181
+ receiving the confirmation that the group was deleted.
182
+ """
183
+ assert muc.pk is not None
184
+ if kick:
185
+ user_participant = await muc.get_user_participant()
186
+ user_participant.kick(reason)
187
+ 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
 
@@ -143,6 +170,7 @@ class LegacyParticipant(
143
170
 
144
171
  if self.is_system:
145
172
  self.jid = j
173
+ self._nickname_no_illegal = ""
146
174
  return
147
175
 
148
176
  nickname = unescaped_nickname
@@ -170,9 +198,6 @@ class LegacyParticipant(
170
198
  except InvalidJID:
171
199
  j.resource = strip_non_printable(nickname)
172
200
 
173
- if nickname != unescaped_nickname:
174
- self.muc._participants_by_escaped_nicknames[nickname] = self # type:ignore
175
-
176
201
  self.jid = j
177
202
 
178
203
  def send_configuration_change(self, codes: tuple[int]):
@@ -213,12 +238,8 @@ class LegacyParticipant(
213
238
 
214
239
  kwargs["status_codes"] = set()
215
240
  p = self._make_presence(ptype="available", last_seen=last_seen, **kwargs)
216
- self.__add_nick_element(p)
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,13 +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
- if isinstance(stanza, Presence):
305
- if stanza["type"] == "unavailable" and not self.__presence_sent:
326
+ self.__add_nick_element(stanza)
327
+ if not self.is_user and isinstance(stanza, Presence):
328
+ if stanza["type"] == "unavailable" and not self._presence_sent:
306
329
  return stanza # type:ignore
307
- self.__presence_sent = True
330
+ self._presence_sent = True
331
+ self.__part_store.set_presence_sent(self.pk)
308
332
  if full_jid:
309
333
  stanza["to"] = full_jid
310
334
  self.__send_presence_if_needed(stanza, full_jid, archive_only)
@@ -314,8 +338,8 @@ class LegacyParticipant(
314
338
  else:
315
339
  stanza.send()
316
340
  else:
317
- if isinstance(stanza, Message):
318
- 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)
319
343
  if archive_only:
320
344
  return stanza
321
345
  for user_full_jid in self.muc.user_full_jids():
@@ -332,7 +356,7 @@ class LegacyParticipant(
332
356
  item["role"] = self.role
333
357
  if not self.muc.is_anonymous:
334
358
  if self.is_user:
335
- item["jid"] = self.user.bare_jid
359
+ item["jid"] = self.user_jid.bare
336
360
  elif self.contact:
337
361
  item["jid"] = self.contact.jid.bare
338
362
  else:
@@ -344,11 +368,11 @@ class LegacyParticipant(
344
368
  )
345
369
  return item
346
370
 
347
- def __add_nick_element(self, p: Presence):
371
+ def __add_nick_element(self, stanza: Union[Presence, Message]):
348
372
  if (nick := self._nickname_no_illegal) != self.jid.resource:
349
373
  n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
350
374
  n["nick"] = nick
351
- p.append(n)
375
+ stanza.append(n)
352
376
 
353
377
  def _get_last_presence(self) -> Optional[CachedPresence]:
354
378
  own = super()._get_last_presence()
@@ -400,7 +424,6 @@ class LegacyParticipant(
400
424
  )
401
425
  if presence_id:
402
426
  p["id"] = presence_id
403
- self.__add_nick_element(p)
404
427
  self._send(p, full_jid)
405
428
 
406
429
  def leave(self):
@@ -409,17 +432,17 @@ class LegacyParticipant(
409
432
  """
410
433
  self.muc.remove_participant(self)
411
434
 
412
- def kick(self):
435
+ def kick(self, reason: str | None = None):
413
436
  """
414
437
  Call this when the participant is kicked from the room
415
438
  """
416
- self.muc.remove_participant(self, kick=True)
439
+ self.muc.remove_participant(self, kick=True, reason=reason)
417
440
 
418
- def ban(self):
441
+ def ban(self, reason: str | None = None):
419
442
  """
420
443
  Call this when the participant is banned from the room
421
444
  """
422
- self.muc.remove_participant(self, ban=True)
445
+ self.muc.remove_participant(self, ban=True, reason=reason)
423
446
 
424
447
  def get_disco_info(self, jid: OptJid = None, node: Optional[str] = None):
425
448
  if self.contact is not None:
@@ -427,13 +450,21 @@ class LegacyParticipant(
427
450
  return super().get_disco_info()
428
451
 
429
452
  def moderate(self, legacy_msg_id: LegacyMessageType, reason: Optional[str] = None):
430
- m = self.muc.get_system_participant()._make_message()
431
- m["apply_to"]["id"] = self._legacy_to_xmpp(legacy_msg_id)
432
- m["apply_to"]["moderated"].enable("retract")
433
- m["apply_to"]["moderated"]["by"] = self.jid
434
- if reason:
435
- m["apply_to"]["moderated"]["reason"] = reason
436
- self._send(m)
453
+ xmpp_id = self._legacy_to_xmpp(legacy_msg_id)
454
+ multi = self.xmpp.store.multi.get_xmpp_ids(self.session.user_pk, xmpp_id)
455
+ if multi is None:
456
+ msg_ids = [xmpp_id]
457
+ else:
458
+ msg_ids = multi + [xmpp_id]
459
+
460
+ for i in msg_ids:
461
+ m = self.muc.get_system_participant()._make_message()
462
+ m["apply_to"]["id"] = i
463
+ m["apply_to"]["moderated"].enable("retract")
464
+ m["apply_to"]["moderated"]["by"] = self.jid
465
+ if reason:
466
+ m["apply_to"]["moderated"]["reason"] = reason
467
+ self._send(m)
437
468
 
438
469
  def set_room_subject(
439
470
  self,
@@ -444,7 +475,7 @@ class LegacyParticipant(
444
475
  ):
445
476
  if update_muc:
446
477
  self.muc._subject = subject # type: ignore
447
- self.muc.subject_setter = self
478
+ self.muc.subject_setter = self.nickname
448
479
  self.muc.subject_date = when
449
480
 
450
481
  msg = self._make_message()
@@ -454,5 +485,41 @@ class LegacyParticipant(
454
485
  msg["subject"] = subject or str(self.muc.name)
455
486
  self._send(msg, full_jid)
456
487
 
488
+ @classmethod
489
+ def from_store(
490
+ cls,
491
+ session,
492
+ stored: Participant,
493
+ contact: Optional[LegacyContact] = None,
494
+ muc: Optional["LegacyMUC"] = None,
495
+ ) -> Self:
496
+ from slidge.group.room import LegacyMUC
497
+
498
+ if muc is None:
499
+ muc = LegacyMUC.get_self_or_unique_subclass().from_store(
500
+ session, stored.room
501
+ )
502
+ part = cls(
503
+ muc,
504
+ stored.nickname,
505
+ role=stored.role,
506
+ affiliation=stored.affiliation,
507
+ )
508
+ part.pk = stored.id
509
+ if contact is not None:
510
+ part.contact = contact
511
+ elif stored.contact is not None:
512
+ contact = LegacyContact.get_self_or_unique_subclass().from_store(
513
+ session, stored.contact
514
+ )
515
+ part.contact = contact
516
+
517
+ part.is_user = stored.is_user
518
+ if (data := stored.extra_attributes) is not None:
519
+ muc.deserialize_extra_attributes(data)
520
+ part._presence_sent = stored.presence_sent
521
+ part._hats = [Hat(h.uri, h.title) for h in stored.hats]
522
+ return part
523
+
457
524
 
458
525
  log = logging.getLogger(__name__)