slidge 0.2.11__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. slidge/__init__.py +5 -2
  2. slidge/command/adhoc.py +9 -3
  3. slidge/command/admin.py +16 -12
  4. slidge/command/base.py +16 -12
  5. slidge/command/chat_command.py +25 -16
  6. slidge/command/user.py +7 -8
  7. slidge/contact/contact.py +123 -210
  8. slidge/contact/roster.py +108 -105
  9. slidge/core/config.py +2 -43
  10. slidge/core/dispatcher/caps.py +9 -2
  11. slidge/core/dispatcher/disco.py +13 -3
  12. slidge/core/dispatcher/message/__init__.py +1 -1
  13. slidge/core/dispatcher/message/chat_state.py +17 -8
  14. slidge/core/dispatcher/message/marker.py +7 -5
  15. slidge/core/dispatcher/message/message.py +120 -93
  16. slidge/core/dispatcher/muc/__init__.py +1 -1
  17. slidge/core/dispatcher/muc/admin.py +4 -4
  18. slidge/core/dispatcher/muc/mam.py +10 -6
  19. slidge/core/dispatcher/muc/misc.py +4 -2
  20. slidge/core/dispatcher/muc/owner.py +5 -3
  21. slidge/core/dispatcher/muc/ping.py +3 -1
  22. slidge/core/dispatcher/presence.py +26 -15
  23. slidge/core/dispatcher/registration.py +20 -12
  24. slidge/core/dispatcher/search.py +7 -3
  25. slidge/core/dispatcher/session_dispatcher.py +13 -5
  26. slidge/core/dispatcher/util.py +37 -27
  27. slidge/core/dispatcher/vcard.py +7 -4
  28. slidge/core/gateway.py +177 -87
  29. slidge/core/mixins/__init__.py +1 -11
  30. slidge/core/mixins/attachment.py +200 -147
  31. slidge/core/mixins/avatar.py +105 -177
  32. slidge/core/mixins/base.py +3 -1
  33. slidge/core/mixins/db.py +50 -2
  34. slidge/core/mixins/disco.py +1 -1
  35. slidge/core/mixins/message.py +19 -17
  36. slidge/core/mixins/message_maker.py +29 -15
  37. slidge/core/mixins/message_text.py +67 -30
  38. slidge/core/mixins/presence.py +94 -37
  39. slidge/core/pubsub.py +42 -47
  40. slidge/core/session.py +95 -60
  41. slidge/db/alembic/versions/cef02a8b1451_initial_schema.py +361 -0
  42. slidge/db/avatar.py +150 -119
  43. slidge/db/meta.py +33 -22
  44. slidge/db/models.py +69 -117
  45. slidge/db/store.py +414 -1094
  46. slidge/group/archive.py +65 -55
  47. slidge/group/bookmarks.py +96 -59
  48. slidge/group/participant.py +150 -144
  49. slidge/group/room.py +351 -328
  50. slidge/main.py +34 -22
  51. slidge/migration.py +17 -29
  52. slidge/slixfix/__init__.py +20 -4
  53. slidge/slixfix/delivery_receipt.py +6 -4
  54. slidge/slixfix/link_preview/link_preview.py +1 -1
  55. slidge/slixfix/link_preview/stanza.py +1 -1
  56. slidge/slixfix/roster.py +5 -7
  57. slidge/slixfix/xep_0077/register.py +8 -8
  58. slidge/slixfix/xep_0077/stanza.py +7 -7
  59. slidge/slixfix/xep_0100/gateway.py +12 -13
  60. slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
  61. slidge/slixfix/xep_0292/vcard4.py +12 -2
  62. slidge/util/archive_msg.py +11 -5
  63. slidge/util/conf.py +27 -21
  64. slidge/util/jid_escaping.py +1 -1
  65. slidge/{core/mixins → util}/lock.py +6 -6
  66. slidge/util/test.py +30 -29
  67. slidge/util/types.py +24 -18
  68. slidge/util/util.py +26 -22
  69. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/METADATA +1 -1
  70. slidge-0.3.0.dist-info/RECORD +95 -0
  71. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/WHEEL +1 -1
  72. slidge/db/alembic/versions/04cf35e3cf85_add_participant_nickname_no_illegal.py +0 -33
  73. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +0 -36
  74. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +0 -85
  75. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +0 -36
  76. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +0 -37
  77. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +0 -41
  78. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +0 -52
  79. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +0 -42
  80. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +0 -61
  81. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +0 -48
  82. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +0 -43
  83. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +0 -139
  84. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +0 -50
  85. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +0 -79
  86. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +0 -214
  87. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +0 -52
  88. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +0 -34
  89. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +0 -26
  90. slidge-0.2.11.dist-info/RECORD +0 -112
  91. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/entry_points.txt +0 -0
  92. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/licenses/LICENSE +0 -0
  93. {slidge-0.2.11.dist-info → slidge-0.3.0.dist-info}/top_level.txt +0 -0
slidge/group/room.py CHANGED
@@ -3,11 +3,23 @@ import logging
3
3
  import re
4
4
  import string
5
5
  import warnings
6
+ from asyncio import Lock
7
+ from contextlib import asynccontextmanager
6
8
  from copy import copy
7
9
  from datetime import datetime, timedelta, timezone
8
- from typing import TYPE_CHECKING, AsyncIterator, Generic, Optional, Self, Union
10
+ from typing import (
11
+ TYPE_CHECKING,
12
+ AsyncIterator,
13
+ Generic,
14
+ Literal,
15
+ Optional,
16
+ Type,
17
+ Union,
18
+ overload,
19
+ )
9
20
  from uuid import uuid4
10
21
 
22
+ import sqlalchemy as sa
11
23
  from slixmpp import JID, Iq, Message, Presence
12
24
  from slixmpp.exceptions import IqError, IqTimeout, XMPPError
13
25
  from slixmpp.plugins.xep_0004 import Form
@@ -17,18 +29,14 @@ from slixmpp.plugins.xep_0469.stanza import NS as PINNING_NS
17
29
  from slixmpp.plugins.xep_0492.stanza import NS as NOTIFY_NS
18
30
  from slixmpp.plugins.xep_0492.stanza import WhenLiteral
19
31
  from slixmpp.xmlstream import ET
32
+ from sqlalchemy.orm import Session as OrmSession
20
33
 
21
34
  from ..contact.contact import LegacyContact
22
35
  from ..contact.roster import ContactIsUser
23
- from ..core import config
24
- from ..core.mixins import StoredAttributeMixin
25
36
  from ..core.mixins.avatar import AvatarMixin
26
- from ..core.mixins.db import UpdateInfoMixin
27
37
  from ..core.mixins.disco import ChatterDiscoMixin
28
- from ..core.mixins.lock import NamedLockMixin
29
38
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
30
- from ..db.models import Room
31
- from ..util import ABCSubclassableOnceAtMost
39
+ from ..db.models import Participant, Room
32
40
  from ..util.jid_escaping import unescape_node
33
41
  from ..util.types import (
34
42
  HoleBound,
@@ -40,12 +48,11 @@ from ..util.types import (
40
48
  MucAffiliation,
41
49
  MucType,
42
50
  )
43
- from ..util.util import deprecated, timeit, with_session
51
+ from ..util.util import SubclassableOnce, deprecated, timeit
44
52
  from .archive import MessageArchive
45
53
  from .participant import LegacyParticipant, escape_nickname
46
54
 
47
55
  if TYPE_CHECKING:
48
- from ..core.gateway import BaseGateway
49
56
  from ..core.session import BaseSession
50
57
 
51
58
  ADMIN_NS = "http://jabber.org/protocol/muc#admin"
@@ -57,14 +64,11 @@ class LegacyMUC(
57
64
  Generic[
58
65
  LegacyGroupIdType, LegacyMessageType, LegacyParticipantType, LegacyUserIdType
59
66
  ],
60
- UpdateInfoMixin,
61
- StoredAttributeMixin,
62
67
  AvatarMixin,
63
- NamedLockMixin,
64
68
  ChatterDiscoMixin,
65
69
  ReactionRecipientMixin,
66
70
  ThreadRecipientMixin,
67
- metaclass=ABCSubclassableOnceAtMost,
71
+ metaclass=SubclassableOnce,
68
72
  ):
69
73
  """
70
74
  A room, a.k.a. a Multi-User Chat.
@@ -75,12 +79,10 @@ class LegacyMUC(
75
79
 
76
80
  max_history_fetch = 100
77
81
 
78
- type = MucType.CHANNEL
79
82
  is_group = True
80
83
 
81
84
  DISCO_TYPE = "text"
82
85
  DISCO_CATEGORY = "conference"
83
- DISCO_NAME = "unnamed-room"
84
86
 
85
87
  STABLE_ARCHIVE = False
86
88
  """
@@ -94,17 +96,6 @@ class LegacyMUC(
94
96
  This is just a flag on archive responses that most clients ignore anyway.
95
97
  """
96
98
 
97
- KEEP_BACKFILLED_PARTICIPANTS = False
98
- """
99
- Set this to ``True`` if the participant list is not full after calling
100
- ``fill_participants()``. This is a workaround for networks with huge
101
- participant lists which do not map really well the MUCs where all presences
102
- are sent on join.
103
- It allows to ensure that the participants that last spoke (within the
104
- ``fill_history()`` method are effectively participants, thus making possible
105
- for XMPP clients to fetch their avatars.
106
- """
107
-
108
99
  _ALL_INFO_FILLED_ON_STARTUP = False
109
100
  """
110
101
  Set this to true if the fill_participants() / fill_participants() design does not
@@ -128,150 +119,200 @@ class LegacyMUC(
128
119
  tries to set the room subject.
129
120
  """
130
121
 
131
- _avatar_bare_jid = True
132
122
  archive: MessageArchive
123
+ session: "BaseSession"
133
124
 
134
- def __init__(self, session: "BaseSession", legacy_id: LegacyGroupIdType, jid: JID):
135
- self.session = session
136
- self.xmpp: "BaseGateway" = session.xmpp
125
+ stored: Room
137
126
 
138
- self.legacy_id = legacy_id
139
- self.jid = jid
127
+ _participant_cls: Type[LegacyParticipantType]
140
128
 
141
- self._user_resources = set[str]()
129
+ def __init__(self, session: "BaseSession", stored: Room) -> None:
130
+ self.session = session
131
+ self.xmpp = session.xmpp
132
+ self.stored = stored
133
+ self._set_logger()
134
+ super().__init__()
142
135
 
143
- self.Participant = LegacyParticipant.get_self_or_unique_subclass()
136
+ self.archive = MessageArchive(stored, self.xmpp.store.mam)
144
137
 
145
- self._subject = ""
146
- self._subject_setter: Optional[str] = None
138
+ def participant_from_store(
139
+ self, stored: Participant, contact: LegacyContact | None = None
140
+ ) -> LegacyParticipantType:
141
+ if contact is None and stored.contact is not None:
142
+ contact = self.session.contacts.from_store(stored.contact)
143
+ return self._participant_cls(self, stored=stored, contact=contact)
147
144
 
148
- self.pk: Optional[int] = None
149
- self._user_nick: Optional[str] = None
145
+ @property
146
+ def jid(self) -> JID:
147
+ return self.stored.jid
150
148
 
151
- self._participants_filled = False
152
- self._history_filled = False
153
- self._description = ""
154
- self._subject_date: Optional[datetime] = None
149
+ @jid.setter
150
+ def jid(self, x: JID):
151
+ # FIXME: without this, mypy yields
152
+ # "Cannot override writeable attribute with read-only property"
153
+ # But it does not happen for LegacyContact. WTF?
154
+ raise RuntimeError
155
155
 
156
- self.__participants_store = self.xmpp.store.participants
157
- self.__store = self.xmpp.store.rooms
156
+ @property
157
+ def legacy_id(self):
158
+ return self.xmpp.LEGACY_ROOM_ID_TYPE(self.stored.legacy_id)
158
159
 
159
- self._n_participants: Optional[int] = None
160
+ def orm(self) -> OrmSession:
161
+ return self.xmpp.store.session()
160
162
 
161
- self.log = logging.getLogger(self.jid.bare)
162
- self._set_logger_name()
163
- super().__init__()
163
+ @property
164
+ def type(self) -> MucType:
165
+ return self.stored.muc_type
166
+
167
+ @type.setter
168
+ def type(self, type_: MucType) -> None:
169
+ if self.type == type_:
170
+ return
171
+ self.stored.muc_type = type_
172
+ self.commit()
164
173
 
165
174
  @property
166
175
  def n_participants(self):
167
- return self._n_participants
176
+ return self.stored.n_participants
168
177
 
169
178
  @n_participants.setter
170
- def n_participants(self, n_participants: Optional[int]):
171
- if self._n_participants == n_participants:
172
- return
173
- self._n_participants = n_participants
174
- if self._updating_info:
179
+ def n_participants(self, n_participants: Optional[int]) -> None:
180
+ if self.stored.n_participants == n_participants:
175
181
  return
176
- assert self.pk is not None
177
- self.__store.update_n_participants(self.pk, n_participants)
182
+ self.stored.n_participants = n_participants
183
+ self.commit()
178
184
 
179
185
  @property
180
186
  def user_jid(self):
181
187
  return self.session.user_jid
182
188
 
183
- def _set_logger_name(self):
189
+ def _set_logger(self) -> None:
184
190
  self.log = logging.getLogger(f"{self.user_jid}:muc:{self}")
185
191
 
186
- def __repr__(self):
187
- return f"<MUC #{self.pk} '{self.name}' ({self.legacy_id} - {self.jid.user})'>"
192
+ def __repr__(self) -> str:
193
+ return f"<MUC #{self.stored.id} '{self.name}' ({self.stored.legacy_id} - {self.jid.user})'>"
188
194
 
189
195
  @property
190
196
  def subject_date(self) -> Optional[datetime]:
191
- return self._subject_date
197
+ if self.stored.subject_date is None:
198
+ return None
199
+ return self.stored.subject_date.replace(tzinfo=timezone.utc)
192
200
 
193
201
  @subject_date.setter
194
202
  def subject_date(self, when: Optional[datetime]) -> None:
195
- self._subject_date = when
196
- if self._updating_info:
203
+ if self.subject_date == when:
197
204
  return
198
- assert self.pk is not None
199
- self.__store.update_subject_date(self.pk, when)
205
+ self.stored.subject_date = when
206
+ self.commit()
200
207
 
201
- def __send_configuration_change(self, codes):
208
+ def __send_configuration_change(self, codes) -> None:
202
209
  part = self.get_system_participant()
203
210
  part.send_configuration_change(codes)
204
211
 
205
212
  @property
206
213
  def user_nick(self):
207
- return self._user_nick or self.session.bookmarks.user_nick or self.user_jid.node
214
+ return (
215
+ self.stored.user_nick
216
+ or self.session.bookmarks.user_nick
217
+ or self.user_jid.node
218
+ )
208
219
 
209
220
  @user_nick.setter
210
- def user_nick(self, nick: str):
211
- self._user_nick = nick
212
- if not self._updating_info:
213
- self.__store.update_user_nick(self.pk, nick)
221
+ def user_nick(self, nick: str) -> None:
222
+ if nick == self.user_nick:
223
+ return
224
+ self.stored.user_nick = nick
225
+ self.commit()
214
226
 
215
227
  def add_user_resource(self, resource: str) -> None:
216
- self._user_resources.add(resource)
217
- assert self.pk is not None
218
- self.__store.set_resource(self.pk, self._user_resources)
228
+ stored_set = self.get_user_resources()
229
+ if resource in stored_set:
230
+ return
231
+ stored_set.add(resource)
232
+ self.stored.user_resources = (
233
+ json.dumps(list(stored_set)) if stored_set else None
234
+ )
235
+ self.commit()
219
236
 
220
237
  def get_user_resources(self) -> set[str]:
221
- return self._user_resources
238
+ stored_str = self.stored.user_resources
239
+ if stored_str is None:
240
+ return set()
241
+ return set(json.loads(stored_str))
222
242
 
223
243
  def remove_user_resource(self, resource: str) -> None:
224
- self._user_resources.remove(resource)
225
- assert self.pk is not None
226
- self.__store.set_resource(self.pk, self._user_resources)
227
-
228
- async def __fill_participants(self):
229
- if self._participants_filled:
230
- return
231
- assert self.pk is not None
232
- async with self.lock("fill participants"):
233
- self._participants_filled = True
234
- async for p in self.fill_participants():
235
- self.__participants_store.update(p)
236
- self.__store.set_participants_filled(self.pk)
237
-
238
- async def get_participants(self) -> AsyncIterator[LegacyParticipant]:
239
- assert self.pk is not None
240
- if self._participants_filled:
241
- for db_participant in self.xmpp.store.participants.get_all(
242
- self.pk, user_included=True
243
- ):
244
- participant = self.Participant.from_store(
245
- self.session, db_participant, muc=self
246
- )
247
- yield participant
244
+ stored_set = self.get_user_resources()
245
+ if resource not in stored_set:
248
246
  return
247
+ stored_set.remove(resource)
248
+ self.stored.user_resources = (
249
+ json.dumps(list(stored_set)) if stored_set else None
250
+ )
251
+ self.commit()
249
252
 
253
+ @asynccontextmanager
254
+ async def lock(self, id_: str) -> AsyncIterator[None]:
255
+ async with self.session.lock((self.legacy_id, id_)):
256
+ yield
257
+
258
+ def get_lock(self, id_: str) -> Lock | None:
259
+ return self.session.get_lock((self.legacy_id, id_))
260
+
261
+ async def __fill_participants(self) -> None:
250
262
  async with self.lock("fill participants"):
251
- self._participants_filled = True
252
- # We only fill the participants list if/when the MUC is first
253
- # joined by an XMPP client. But we may have instantiated some before.
263
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
264
+ orm.add(self.stored)
265
+ with orm.no_autoflush:
266
+ orm.refresh(self.stored, ["participants_filled"])
267
+ if self.participants_filled:
268
+ return
269
+ parts: list[Participant] = []
254
270
  resources = set[str]()
271
+ # During fill_participants(), self.get_participant*() methods may
272
+ # return a participant with a conflicting nick/resource.
255
273
  async for participant in self.fill_participants():
256
- # TODO: batch SQL update at the end of this function for perf?
257
- self.__store_participant(participant)
258
- yield participant
259
- resources.add(participant.jid.resource)
260
- for db_participant in self.xmpp.store.participants.get_all(
261
- self.pk, user_included=True
262
- ):
263
- participant = self.Participant.from_store(
264
- self.session, db_participant, muc=self
265
- )
266
- if participant.jid.resource not in resources:
267
- yield participant
268
- self.__store.set_participants_filled(self.pk)
269
- return
274
+ if participant.stored.resource in resources:
275
+ self.log.warning(
276
+ "Participant '%s' was yielded more than once by fill_participants()",
277
+ participant.stored.resource,
278
+ )
279
+ continue
280
+ parts.append(participant.stored)
281
+ resources.add(participant.stored.resource)
282
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
283
+ orm.add(self.stored)
284
+ # because self.fill_participants() is async, self.stored may be stale at
285
+ # this point, and the only thing we want to update is the participant list
286
+ # and the participant_filled attribute.
287
+ with orm.no_autoflush:
288
+ orm.refresh(self.stored)
289
+ for part in parts:
290
+ orm.merge(part)
291
+ self.stored.participants_filled = True
292
+ orm.commit()
293
+
294
+ async def get_participants(
295
+ self, affiliation: Optional[MucAffiliation] = None
296
+ ) -> AsyncIterator[LegacyParticipantType]:
297
+ await self.__fill_participants()
298
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
299
+ orm.add(self.stored)
300
+ for db_participant in self.stored.participants:
301
+ if (
302
+ affiliation is not None
303
+ and db_participant.affiliation != affiliation
304
+ ):
305
+ continue
306
+ yield self.participant_from_store(db_participant)
270
307
 
271
- async def __fill_history(self):
308
+ async def __fill_history(self) -> None:
272
309
  async with self.lock("fill history"):
273
- if self._history_filled:
274
- log.debug("History has already been fetched %s", self)
310
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
311
+ orm.add(self.stored)
312
+ with orm.no_autoflush:
313
+ orm.refresh(self.stored, ["history_filled"])
314
+ if self.stored.history_filled:
315
+ self.log.debug("History has already been fetched.")
275
316
  return
276
317
  log.debug("Fetching history for %s", self)
277
318
  try:
@@ -288,43 +329,40 @@ class LegacyMUC(
288
329
  except NotImplementedError:
289
330
  return
290
331
  except Exception as e:
291
- log.exception("Could not backfill: %s", e)
292
- assert self.pk is not None
293
- self.__store.set_history_filled(self.pk, True)
294
- self._history_filled = True
332
+ self.log.exception("Could not backfill", exc_info=e)
333
+
334
+ self.stored.history_filled = True
335
+ self.commit(merge=True)
336
+
337
+ def _get_disco_name(self) -> str | None:
338
+ return self.name
295
339
 
296
340
  @property
297
- def name(self):
298
- return self.DISCO_NAME
341
+ def name(self) -> str | None:
342
+ return self.stored.name
299
343
 
300
344
  @name.setter
301
- def name(self, n: str):
302
- if self.DISCO_NAME == n:
345
+ def name(self, n: str | None) -> None:
346
+ if self.name == n:
303
347
  return
304
- self.DISCO_NAME = n
305
- self._set_logger_name()
348
+ self.stored.name = n
349
+ self.commit()
350
+ self._set_logger()
306
351
  self.__send_configuration_change((104,))
307
- if self._updating_info:
308
- return
309
- assert self.pk is not None
310
- self.__store.update_name(self.pk, n)
311
352
 
312
353
  @property
313
354
  def description(self):
314
- return self._description
355
+ return self.stored.description or ""
315
356
 
316
357
  @description.setter
317
- def description(self, d: str):
318
- if self._description == d:
358
+ def description(self, d: str) -> None:
359
+ if self.description == d:
319
360
  return
320
- self._description = d
361
+ self.stored.description = d
362
+ self.commit()
321
363
  self.__send_configuration_change((104,))
322
- if self._updating_info:
323
- return
324
- assert self.pk is not None
325
- self.__store.update_description(self.pk, d)
326
364
 
327
- def on_presence_unavailable(self, p: Presence):
365
+ def on_presence_unavailable(self, p: Presence) -> None:
328
366
  pto = p.get_to()
329
367
  if pto.bare != self.jid.bare:
330
368
  return
@@ -332,7 +370,7 @@ class LegacyMUC(
332
370
  pfrom = p.get_from()
333
371
  if pfrom.bare != self.user_jid.bare:
334
372
  return
335
- if (resource := pfrom.resource) in self._user_resources:
373
+ if (resource := pfrom.resource) in self.get_user_resources():
336
374
  if pto.resource != self.user_nick:
337
375
  self.log.debug(
338
376
  "Received 'leave group' request but with wrong nickname. %s", p
@@ -381,7 +419,7 @@ class LegacyMUC(
381
419
  """
382
420
  raise NotImplementedError
383
421
 
384
- async def fill_participants(self) -> AsyncIterator[LegacyParticipant]:
422
+ async def fill_participants(self) -> AsyncIterator[LegacyParticipantType]:
385
423
  """
386
424
  This method should yield the list of all members of this group.
387
425
 
@@ -393,30 +431,27 @@ class LegacyMUC(
393
431
  yield
394
432
 
395
433
  @property
396
- def subject(self):
397
- return self._subject
434
+ def subject(self) -> str:
435
+ return self.stored.subject or ""
398
436
 
399
437
  @subject.setter
400
- def subject(self, s: str):
401
- if s == self._subject:
438
+ def subject(self, s: str) -> None:
439
+ if s == self.subject:
402
440
  return
441
+
442
+ self.stored.subject = s
443
+ self.commit()
403
444
  self.__get_subject_setter_participant().set_room_subject(
404
445
  s, None, self.subject_date, False
405
446
  )
406
447
 
407
- self._subject = s
408
- if self._updating_info:
409
- return
410
- assert self.pk is not None
411
- self.__store.update_subject(self.pk, s)
412
-
413
448
  @property
414
449
  def is_anonymous(self):
415
450
  return self.type == MucType.CHANNEL
416
451
 
417
452
  @property
418
453
  def subject_setter(self) -> Optional[str]:
419
- return self._subject_setter
454
+ return self.stored.subject_setter
420
455
 
421
456
  @subject_setter.setter
422
457
  def subject_setter(self, subject_setter: SubjectSetterType) -> None:
@@ -425,19 +460,16 @@ class LegacyMUC(
425
460
  elif isinstance(subject_setter, LegacyParticipant):
426
461
  subject_setter = subject_setter.nickname
427
462
 
428
- if subject_setter == self._subject_setter:
463
+ if subject_setter == self.subject_setter:
429
464
  return
430
465
  assert isinstance(subject_setter, str | None)
431
- self._subject_setter = subject_setter
432
- if self._updating_info:
433
- return
434
- assert self.pk is not None
435
- self.__store.update_subject_setter(self.pk, subject_setter)
466
+ self.stored.subject_setter = subject_setter
467
+ self.commit()
436
468
 
437
469
  def __get_subject_setter_participant(self) -> LegacyParticipant:
438
- if self._subject_setter is None:
470
+ if self.subject_setter is None:
439
471
  return self.get_system_participant()
440
- return self.Participant(self, self._subject_setter)
472
+ return self._participant_cls(self, Participant(nickname=self.subject_setter))
441
473
 
442
474
  def features(self):
443
475
  features = [
@@ -475,11 +507,13 @@ class LegacyMUC(
475
507
  form.add_field("muc#maxhistoryfetch", value=str(self.max_history_fetch))
476
508
  form.add_field("muc#roominfo_subjectmod", "boolean", value=False)
477
509
 
478
- if self._ALL_INFO_FILLED_ON_STARTUP or self._participants_filled:
479
- assert self.pk is not None
480
- n: Optional[int] = self.__participants_store.get_count(self.pk)
510
+ if self._ALL_INFO_FILLED_ON_STARTUP or self.stored.participants_filled:
511
+ with self.xmpp.store.session() as orm:
512
+ n = orm.scalar(
513
+ sa.select(sa.func.count(Participant.id)).filter_by(room=self.stored)
514
+ )
481
515
  else:
482
- n = self._n_participants
516
+ n = self.n_participants
483
517
  if n is not None:
484
518
  form.add_field("muc#roominfo_occupants", value=str(n))
485
519
 
@@ -489,14 +523,14 @@ class LegacyMUC(
489
523
  if s := self.subject:
490
524
  form.add_field("muc#roominfo_subject", value=s)
491
525
 
492
- if self._set_avatar_task:
526
+ if self._set_avatar_task is not None:
493
527
  await self._set_avatar_task
494
- avatar = self.get_avatar()
495
- if avatar and (h := avatar.id):
496
- form.add_field(
497
- "{http://modules.prosody.im/mod_vcard_muc}avatar#sha1", value=h
498
- )
499
- form.add_field("muc#roominfo_avatarhash", "text-multi", value=[h])
528
+ avatar = self.get_avatar()
529
+ if avatar and (h := avatar.id):
530
+ form.add_field(
531
+ "{http://modules.prosody.im/mod_vcard_muc}avatar#sha1", value=h
532
+ )
533
+ form.add_field("muc#roominfo_avatarhash", "text-multi", value=[h])
500
534
 
501
535
  form.add_field("muc#roomconfig_membersonly", "boolean", value=is_group)
502
536
  form.add_field(
@@ -514,7 +548,7 @@ class LegacyMUC(
514
548
 
515
549
  return r
516
550
 
517
- def shutdown(self):
551
+ def shutdown(self) -> None:
518
552
  _, user_jid = escape_nickname(self.jid, self.user_nick)
519
553
  for user_full_jid in self.user_full_jids():
520
554
  presence = self.xmpp.make_presence(
@@ -526,7 +560,7 @@ class LegacyMUC(
526
560
  presence.send()
527
561
 
528
562
  def user_full_jids(self):
529
- for r in self._user_resources:
563
+ for r in self.get_user_resources():
530
564
  j = JID(self.user_jid)
531
565
  j.resource = r
532
566
  yield j
@@ -536,14 +570,9 @@ class LegacyMUC(
536
570
  _, user_muc_jid = escape_nickname(self.jid, self.user_nick)
537
571
  return user_muc_jid
538
572
 
539
- def _legacy_to_xmpp(self, legacy_id: LegacyMessageType):
540
- return self.xmpp.store.sent.get_group_xmpp_id(
541
- self.session.user_pk, str(legacy_id)
542
- ) or self.session.legacy_to_xmpp_msg_id(legacy_id)
543
-
544
573
  async def echo(
545
574
  self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
546
- ):
575
+ ) -> None:
547
576
  origin_id = msg.get_origin_id()
548
577
 
549
578
  msg.set_from(self.user_muc_jid)
@@ -568,24 +597,11 @@ class LegacyMUC(
568
597
 
569
598
  msg.send()
570
599
 
571
- def _get_cached_avatar_id(self):
572
- if self.pk is None:
573
- return None
574
- return self.xmpp.store.rooms.get_avatar_legacy_id(self.pk)
575
-
576
- def _post_avatar_update(self) -> None:
577
- if self.pk is None:
578
- return
579
- assert self.pk is not None
580
- self.xmpp.store.rooms.set_avatar(
581
- self.pk,
582
- self._avatar_pk,
583
- None if self.avatar_id is None else str(self.avatar_id),
584
- )
600
+ def _post_avatar_update(self, cached_avatar) -> None:
585
601
  self.__send_configuration_change((104,))
586
602
  self._send_room_presence()
587
603
 
588
- def _send_room_presence(self, user_full_jid: Optional[JID] = None):
604
+ def _send_room_presence(self, user_full_jid: Optional[JID] = None) -> None:
589
605
  if user_full_jid is None:
590
606
  tos = self.user_full_jids()
591
607
  else:
@@ -599,20 +615,19 @@ class LegacyMUC(
599
615
  p.send()
600
616
 
601
617
  @timeit
602
- @with_session
603
618
  async def join(self, join_presence: Presence):
604
619
  user_full_jid = join_presence.get_from()
605
620
  requested_nickname = join_presence.get_to().resource
606
621
  client_resource = user_full_jid.resource
607
622
 
608
- if client_resource in self._user_resources:
623
+ if client_resource in self.get_user_resources():
609
624
  self.log.debug("Received join from a resource that is already joined.")
610
625
 
611
- self.add_user_resource(client_resource)
612
-
613
626
  if not requested_nickname or not client_resource:
614
627
  raise XMPPError("jid-malformed", by=self.jid)
615
628
 
629
+ self.add_user_resource(client_resource)
630
+
616
631
  self.log.debug(
617
632
  "Resource %s of %s wants to join room %s with nickname %s",
618
633
  client_resource,
@@ -631,9 +646,13 @@ class LegacyMUC(
631
646
 
632
647
  if user_participant is None:
633
648
  user_participant = await self.get_user_participant()
634
- if not user_participant.is_user: # type:ignore
649
+ with self.xmpp.store.session() as orm:
650
+ orm.add(self.stored)
651
+ with orm.no_autoflush:
652
+ orm.refresh(self.stored, ["participants"])
653
+ if not user_participant.is_user:
635
654
  self.log.warning("is_user flag not set participant on user_participant")
636
- user_participant.is_user = True # type:ignore
655
+ user_participant.is_user = True
637
656
  user_participant.send_initial_presence(
638
657
  user_full_jid,
639
658
  presence_id=join_presence["id"],
@@ -661,8 +680,12 @@ class LegacyMUC(
661
680
  maxstanzas=maxstanzas,
662
681
  since=since,
663
682
  )
683
+ if self.HAS_SUBJECT:
684
+ subject = self.subject or ""
685
+ else:
686
+ subject = self.description or self.name or ""
664
687
  self.__get_subject_setter_participant().set_room_subject(
665
- self._subject if self.HAS_SUBJECT else (self.description or self.name),
688
+ subject,
666
689
  user_full_jid,
667
690
  self.subject_date,
668
691
  )
@@ -683,22 +706,17 @@ class LegacyMUC(
683
706
  return p
684
707
 
685
708
  def __store_participant(self, p: "LegacyParticipantType") -> None:
686
- # we don't want to update the participant list when we're filling history
687
- if not self.KEEP_BACKFILLED_PARTICIPANTS and self.get_lock("fill history"):
709
+ if self.get_lock("fill participants"):
688
710
  return
689
- assert self.pk is not None
690
- p.pk = self.__participants_store.add(self.pk, p.nickname)
691
- self.__participants_store.update(p)
692
- if p._hats:
693
- self.__participants_store.set_hats(p.pk, p._hats)
711
+ p.commit(merge=True)
694
712
 
695
713
  async def get_participant(
696
714
  self,
697
715
  nickname: str,
698
- raise_if_not_found=False,
699
- fill_first=False,
700
- store=True,
701
- **kwargs,
716
+ raise_if_not_found: bool = False,
717
+ fill_first: bool = False,
718
+ store: bool = True,
719
+ is_user: bool = False,
702
720
  ) -> "LegacyParticipantType":
703
721
  """
704
722
  Get a participant by their nickname.
@@ -713,30 +731,34 @@ class LegacyMUC(
713
731
  :param fill_first: Ensure :meth:`.LegacyMUC.fill_participants()` has been called first
714
732
  (internal use by slidge, plugins should not need that)
715
733
  :param store: persistently store the user in the list of MUC participants
716
- :param kwargs: additional parameters for the :class:`.Participant`
717
- construction (optional)
718
734
  :return:
719
735
  """
720
- if fill_first and not self._participants_filled:
721
- async for _ in self.get_participants():
722
- pass
723
- if self.pk is not None:
724
- with self.xmpp.store.session():
725
- stored = self.__participants_store.get_by_nickname(
726
- self.pk, nickname
727
- ) or self.__participants_store.get_by_resource(self.pk, nickname)
728
- if stored is not None:
729
- return self.Participant.from_store(self.session, stored)
736
+ if fill_first:
737
+ await self.__fill_participants()
738
+ with self.xmpp.store.session() as orm:
739
+ stored = (
740
+ orm.query(Participant)
741
+ .filter(
742
+ Participant.room == self.stored,
743
+ (Participant.nickname == nickname)
744
+ | (Participant.resource == nickname),
745
+ )
746
+ .one_or_none()
747
+ )
748
+ if stored is not None:
749
+ return self.participant_from_store(stored)
730
750
 
731
751
  if raise_if_not_found:
732
752
  raise XMPPError("item-not-found")
733
- p = self.Participant(self, nickname, **kwargs)
734
- if store and not self._updating_info:
753
+ p = self._participant_cls(
754
+ self, Participant(room=self.stored, nickname=nickname, is_user=is_user)
755
+ )
756
+ if store:
735
757
  self.__store_participant(p)
736
758
  if (
737
759
  not self.get_lock("fill participants")
738
760
  and not self.get_lock("fill history")
739
- and self._participants_filled
761
+ and self.stored.participants_filled
740
762
  and not p.is_user
741
763
  and not p.is_system
742
764
  ):
@@ -752,11 +774,26 @@ class LegacyMUC(
752
774
  service
753
775
  :return:
754
776
  """
755
- return self.Participant(self, is_system=True)
777
+ return self._participant_cls(self, Participant(), is_system=True)
756
778
 
779
+ @overload
757
780
  async def get_participant_by_contact(
758
- self, c: "LegacyContact", **kwargs
759
- ) -> "LegacyParticipantType":
781
+ self, c: "LegacyContact"
782
+ ) -> "LegacyParticipantType": ...
783
+
784
+ @overload
785
+ async def get_participant_by_contact(
786
+ self, c: "LegacyContact", create: Literal[False]
787
+ ) -> "LegacyParticipantType | None": ...
788
+
789
+ @overload
790
+ async def get_participant_by_contact(
791
+ self, c: "LegacyContact", create: Literal[True]
792
+ ) -> "LegacyParticipantType": ...
793
+
794
+ async def get_participant_by_contact(
795
+ self, c: "LegacyContact", create: bool = True
796
+ ) -> "LegacyParticipantType | None":
760
797
  """
761
798
  Get a non-anonymous participant.
762
799
 
@@ -764,37 +801,42 @@ class LegacyMUC(
764
801
  that the Contact jid is associated to this participant
765
802
 
766
803
  :param c: The :class:`.LegacyContact` instance corresponding to this contact
767
- :param kwargs: additional parameters for the :class:`.Participant`
768
- construction (optional)
804
+ :param create: Creates the participant if it does not exist.
769
805
  :return:
770
806
  """
771
807
  await self.session.contacts.ready
772
808
 
773
- if self.pk is not None:
774
- c._LegacyContact__ensure_pk() # type: ignore
775
- assert c.contact_pk is not None
776
- with self.__store.session():
777
- stored = self.__participants_store.get_by_contact(self.pk, c.contact_pk)
778
- if stored is not None:
779
- return self.Participant.from_store(
780
- self.session, stored, muc=self, contact=c
781
- )
809
+ with self.xmpp.store.session() as orm:
810
+ self.stored = orm.merge(self.stored)
811
+ stored = (
812
+ orm.query(Participant)
813
+ .filter_by(contact=c.stored, room=self.stored)
814
+ .one_or_none()
815
+ )
816
+ if stored is None:
817
+ if not create:
818
+ return None
819
+ else:
820
+ return self.participant_from_store(stored=stored, contact=c)
782
821
 
783
- nickname = c.name or unescape_node(c.jid_username)
822
+ nickname = c.name or unescape_node(c.jid.node)
784
823
 
785
- if self.pk is None:
824
+ if self.stored.id is None:
786
825
  nick_available = True
787
826
  else:
788
- nick_available = self.__store.nickname_is_available(self.pk, nickname)
827
+ with self.xmpp.store.session() as orm:
828
+ nick_available = (
829
+ orm.query(Participant.id).filter_by(
830
+ room=self.stored, nickname=nickname
831
+ )
832
+ ).one_or_none() is None
789
833
 
790
834
  if not nick_available:
791
835
  self.log.debug("Nickname conflict")
792
- nickname = f"{nickname} ({c.jid_username})"
793
- p = self.Participant(self, nickname, **kwargs)
794
- p.contact = c
795
-
796
- if self._updating_info:
797
- return p
836
+ nickname = f"{nickname} ({c.jid.node})"
837
+ p = self._participant_cls(
838
+ self, Participant(nickname=nickname, room=self.stored), contact=c
839
+ )
798
840
 
799
841
  self.__store_participant(p)
800
842
  # FIXME: this is not great but given the current design,
@@ -803,7 +845,7 @@ class LegacyMUC(
803
845
  # and role afterwards.
804
846
  # We need a refactor of the MUC class… later™
805
847
  if (
806
- self._participants_filled
848
+ self.stored.participants_filled
807
849
  and not self.get_lock("fill participants")
808
850
  and not self.get_lock("fill history")
809
851
  ):
@@ -811,19 +853,19 @@ class LegacyMUC(
811
853
  return p
812
854
 
813
855
  async def get_participant_by_legacy_id(
814
- self, legacy_id: LegacyUserIdType, **kwargs
856
+ self, legacy_id: LegacyUserIdType
815
857
  ) -> "LegacyParticipantType":
816
858
  try:
817
859
  c = await self.session.contacts.by_legacy_id(legacy_id)
818
860
  except ContactIsUser:
819
- return await self.get_user_participant(**kwargs)
820
- return await self.get_participant_by_contact(c, **kwargs)
861
+ return await self.get_user_participant()
862
+ return await self.get_participant_by_contact(c)
821
863
 
822
864
  def remove_participant(
823
865
  self,
824
866
  p: "LegacyParticipantType",
825
- kick=False,
826
- ban=False,
867
+ kick: bool = False,
868
+ ban: bool = False,
827
869
  reason: str | None = None,
828
870
  ):
829
871
  """
@@ -836,7 +878,9 @@ class LegacyMUC(
836
878
  """
837
879
  if kick and ban:
838
880
  raise TypeError("Either kick or ban")
839
- self.__participants_store.delete(p.pk)
881
+ with self.xmpp.store.session() as orm:
882
+ orm.delete(p.stored)
883
+ orm.commit()
840
884
  if kick:
841
885
  codes = {307}
842
886
  elif ban:
@@ -844,20 +888,23 @@ class LegacyMUC(
844
888
  else:
845
889
  codes = None
846
890
  presence = p._make_presence(ptype="unavailable", status_codes=codes)
847
- p._affiliation = "outcast" if ban else "none"
848
- p._role = "none"
891
+ p.stored.affiliation = "outcast" if ban else "none"
892
+ p.stored.role = "none"
849
893
  if reason:
850
894
  presence["muc"].set_item_attr("reason", reason)
851
895
  p._send(presence)
852
896
 
853
- def rename_participant(self, old_nickname: str, new_nickname: str):
854
- assert self.pk is not None
855
- with self.xmpp.store.session():
856
- stored = self.__participants_store.get_by_nickname(self.pk, old_nickname)
897
+ def rename_participant(self, old_nickname: str, new_nickname: str) -> None:
898
+ with self.xmpp.store.session() as orm:
899
+ stored = (
900
+ orm.query(Participant)
901
+ .filter_by(room=self.stored, nickname=old_nickname)
902
+ .one_or_none()
903
+ )
857
904
  if stored is None:
858
905
  self.log.debug("Tried to rename a participant that we didn't know")
859
906
  return
860
- p = self.Participant.from_store(self.session, stored)
907
+ p = self.participant_from_store(stored)
861
908
  if p.nickname == old_nickname:
862
909
  p.nickname = new_nickname
863
910
 
@@ -868,7 +915,7 @@ class LegacyMUC(
868
915
  maxstanzas: Optional[int] = None,
869
916
  seconds: Optional[int] = None,
870
917
  since: Optional[datetime] = None,
871
- ):
918
+ ) -> None:
872
919
  """
873
920
  Old-style history join (internal slidge use)
874
921
 
@@ -895,7 +942,7 @@ class LegacyMUC(
895
942
  msg.set_to(full_jid)
896
943
  self.xmpp.send(msg, False)
897
944
 
898
- async def send_mam(self, iq: Iq):
945
+ async def send_mam(self, iq: Iq) -> None:
899
946
  await self.__fill_history()
900
947
 
901
948
  form_values = iq["mam"]["form"].get_values()
@@ -922,8 +969,13 @@ class LegacyMUC(
922
969
  after_id = after_id_rsm or after_id
923
970
 
924
971
  before_rsm = iq["mam"]["rsm"]["before"]
925
- if before_rsm is True and max_results is not None:
972
+ if before_rsm is not None and max_results is not None:
926
973
  last_page_n = max_results
974
+ # - before_rsm is True means the empty element <before />, which means
975
+ # "last page in chronological order", cf https://xmpp.org/extensions/xep-0059.html#backwards
976
+ # - before_rsm == "an ID" means <before>an ID</before>
977
+ if before_rsm is not True:
978
+ before_id = before_rsm
927
979
  else:
928
980
  last_page_n = None
929
981
 
@@ -978,11 +1030,11 @@ class LegacyMUC(
978
1030
  reply["mam_fin"]["rsm"]["count"] = str(count)
979
1031
  reply.send()
980
1032
 
981
- async def send_mam_metadata(self, iq: Iq):
1033
+ async def send_mam_metadata(self, iq: Iq) -> None:
982
1034
  await self.__fill_history()
983
1035
  await self.archive.send_metadata(iq)
984
1036
 
985
- async def kick_resource(self, r: str):
1037
+ async def kick_resource(self, r: str) -> None:
986
1038
  """
987
1039
  Kick a XMPP client of the user. (slidge internal use)
988
1040
 
@@ -1030,12 +1082,11 @@ class LegacyMUC(
1030
1082
 
1031
1083
  async def add_to_bookmarks(
1032
1084
  self,
1033
- auto_join=True,
1034
- invite=False,
1035
- preserve=True,
1085
+ auto_join: bool = True,
1086
+ preserve: bool = True,
1036
1087
  pin: bool | None = None,
1037
1088
  notify: WhenLiteral | None = None,
1038
- ):
1089
+ ) -> None:
1039
1090
  """
1040
1091
  Add the MUC to the user's XMPP bookmarks (:xep:`0402')
1041
1092
 
@@ -1046,11 +1097,6 @@ class LegacyMUC(
1046
1097
  this MUC on startup. In theory, XMPP clients will receive
1047
1098
  a "push" notification when this is called, and they will
1048
1099
  join if they are online.
1049
- :param invite: send an invitation to join this MUC emanating from
1050
- the gateway. While this should not be strictly necessary,
1051
- it can help for clients that do not support :xep:`0402`, or
1052
- that have 'do not honor bookmarks auto-join' turned on in their
1053
- settings.
1054
1100
  :param preserve: preserve auto-join and bookmarks extensions
1055
1101
  set by the user outside slidge
1056
1102
  :param pin: Pin the group chat bookmark :xep:`0469`. Requires privileged entity.
@@ -1131,20 +1177,32 @@ class LegacyMUC(
1131
1177
  "IQ privileges (XEP0356) are not set, we cannot add bookmarks for the user"
1132
1178
  )
1133
1179
  # fallback by forcing invitation
1134
- invite = True
1180
+ bookmark_add_fail = True
1135
1181
  except IqError as e:
1136
1182
  warnings.warn(
1137
1183
  f"Something went wrong while trying to set the bookmarks: {e}"
1138
1184
  )
1139
1185
  # fallback by forcing invitation
1140
- invite = True
1186
+ bookmark_add_fail = True
1187
+ else:
1188
+ bookmark_add_fail = False
1141
1189
  else:
1142
1190
  self.log.debug("Bookmark does not need updating.")
1143
1191
  return
1144
1192
 
1145
- if invite or (config.ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS and existing is None):
1193
+ if bookmark_add_fail:
1146
1194
  self.session.send_gateway_invite(
1147
- self, reason="This group could not be added automatically for you"
1195
+ self,
1196
+ reason="This group could not be added automatically for you, most"
1197
+ "likely because this gateway is not configured as a privileged entity. "
1198
+ "Contact your administrator.",
1199
+ )
1200
+ elif existing is None and self.session.user.preferences.get(
1201
+ "always_invite_when_adding_bookmarks", True
1202
+ ):
1203
+ self.session.send_gateway_invite(
1204
+ self,
1205
+ reason="The gateway is configured to always send invitations for groups.",
1148
1206
  )
1149
1207
 
1150
1208
  async def on_avatar(
@@ -1233,12 +1291,10 @@ class LegacyMUC(
1233
1291
  raise NotImplementedError
1234
1292
 
1235
1293
  async def parse_mentions(self, text: str) -> list[Mention]:
1236
- with self.__store.session():
1294
+ with self.xmpp.store.session() as orm:
1237
1295
  await self.__fill_participants()
1238
- assert self.pk is not None
1239
- participants = {
1240
- p.nickname: p for p in self.__participants_store.get_all(self.pk)
1241
- }
1296
+ orm.add(self.stored)
1297
+ participants = {p.nickname: p for p in self.stored.participants}
1242
1298
 
1243
1299
  if len(participants) == 0:
1244
1300
  return []
@@ -1259,8 +1315,8 @@ class LegacyMUC(
1259
1315
  if span[0] != 0 and text[span[0] - 1] not in _WHITESPACE_OR_PUNCTUATION:
1260
1316
  continue
1261
1317
  if span[1] == len(text) or text[span[1]] in _WHITESPACE_OR_PUNCTUATION:
1262
- participant = self.Participant.from_store(
1263
- self.session, participants[nick]
1318
+ participant = self.participant_from_store(
1319
+ stored=participants[nick],
1264
1320
  )
1265
1321
  if contact := participant.contact:
1266
1322
  result.append(
@@ -1279,45 +1335,12 @@ class LegacyMUC(
1279
1335
  """
1280
1336
  raise NotImplementedError
1281
1337
 
1282
- @classmethod
1283
- def from_store(cls, session, stored: Room, *args, **kwargs) -> Self:
1284
- muc = cls(
1285
- session,
1286
- cls.xmpp.LEGACY_ROOM_ID_TYPE(stored.legacy_id),
1287
- stored.jid,
1288
- *args, # type: ignore
1289
- **kwargs, # type: ignore
1290
- )
1291
- muc.pk = stored.id
1292
- muc.type = stored.muc_type # type: ignore
1293
- muc._user_nick = stored.user_nick
1294
- if stored.name:
1295
- muc.DISCO_NAME = stored.name
1296
- if stored.description:
1297
- muc._description = stored.description
1298
- if (data := stored.extra_attributes) is not None:
1299
- muc.deserialize_extra_attributes(data)
1300
- muc._subject = stored.subject or ""
1301
- if stored.subject_date is not None:
1302
- muc._subject_date = stored.subject_date.replace(tzinfo=timezone.utc)
1303
- muc._participants_filled = stored.participants_filled
1304
- muc._n_participants = stored.n_participants
1305
- muc._history_filled = stored.history_filled
1306
- if stored.user_resources is not None:
1307
- muc._user_resources = set(json.loads(stored.user_resources))
1308
- muc._subject_setter = stored.subject_setter
1309
- muc.archive = MessageArchive(muc.pk, session.xmpp.store.mam)
1310
- muc._set_logger_name()
1311
- muc._AvatarMixin__avatar_unique_id = ( # type:ignore
1312
- None
1313
- if stored.avatar_legacy_id is None
1314
- else session.xmpp.AVATAR_ID_TYPE(stored.avatar_legacy_id)
1315
- )
1316
- muc._avatar_pk = stored.avatar_id
1317
- return muc
1338
+ @property
1339
+ def participants_filled(self) -> bool:
1340
+ return self.stored.participants_filled
1318
1341
 
1319
1342
 
1320
- def set_origin_id(msg: Message, origin_id: str):
1343
+ def set_origin_id(msg: Message, origin_id: str) -> None:
1321
1344
  sub = ET.Element("{urn:xmpp:sid:0}origin-id")
1322
1345
  sub.attrib["id"] = origin_id
1323
1346
  msg.xml.append(sub)