slidge 0.1.2__py3-none-any.whl → 0.2.0a0__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 (63) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -196
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +8 -1
  5. slidge/command/admin.py +5 -6
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -5
  9. slidge/contact/contact.py +93 -31
  10. slidge/contact/roster.py +54 -39
  11. slidge/core/config.py +13 -7
  12. slidge/core/gateway/base.py +139 -34
  13. slidge/core/gateway/disco.py +2 -4
  14. slidge/core/gateway/mam.py +1 -4
  15. slidge/core/gateway/ping.py +2 -3
  16. slidge/core/gateway/presence.py +1 -1
  17. slidge/core/gateway/registration.py +32 -21
  18. slidge/core/gateway/search.py +3 -5
  19. slidge/core/gateway/session_dispatcher.py +109 -51
  20. slidge/core/gateway/vcard_temp.py +6 -4
  21. slidge/core/mixins/__init__.py +11 -1
  22. slidge/core/mixins/attachment.py +15 -10
  23. slidge/core/mixins/avatar.py +66 -18
  24. slidge/core/mixins/base.py +8 -2
  25. slidge/core/mixins/message.py +11 -7
  26. slidge/core/mixins/message_maker.py +17 -9
  27. slidge/core/mixins/presence.py +14 -4
  28. slidge/core/pubsub.py +54 -212
  29. slidge/core/session.py +65 -33
  30. slidge/db/__init__.py +4 -0
  31. slidge/db/alembic/env.py +64 -0
  32. slidge/db/alembic/script.py.mako +26 -0
  33. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  34. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  35. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  36. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +76 -0
  37. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  38. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  39. slidge/db/avatar.py +224 -0
  40. slidge/db/meta.py +65 -0
  41. slidge/db/models.py +365 -0
  42. slidge/db/store.py +976 -0
  43. slidge/group/archive.py +13 -14
  44. slidge/group/bookmarks.py +59 -56
  45. slidge/group/participant.py +81 -29
  46. slidge/group/room.py +242 -142
  47. slidge/main.py +201 -0
  48. slidge/migration.py +30 -0
  49. slidge/slixfix/__init__.py +35 -2
  50. slidge/slixfix/roster.py +11 -4
  51. slidge/slixfix/xep_0292/vcard4.py +1 -0
  52. slidge/util/db.py +1 -47
  53. slidge/util/test.py +21 -4
  54. slidge/util/types.py +24 -4
  55. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/METADATA +3 -1
  56. slidge-0.2.0a0.dist-info/RECORD +108 -0
  57. slidge/core/cache.py +0 -183
  58. slidge/util/schema.sql +0 -126
  59. slidge/util/sql.py +0 -508
  60. slidge-0.1.2.dist-info/RECORD +0 -96
  61. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/LICENSE +0 -0
  62. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/WHEEL +0 -0
  63. {slidge-0.1.2.dist-info → slidge-0.2.0a0.dist-info}/entry_points.txt +0 -0
slidge/group/room.py CHANGED
@@ -1,10 +1,11 @@
1
+ import json
1
2
  import logging
2
3
  import re
3
4
  import string
4
5
  import warnings
5
6
  from copy import copy
6
7
  from datetime import datetime, timedelta, timezone
7
- from typing import TYPE_CHECKING, Generic, Optional, Union
8
+ from typing import TYPE_CHECKING, Generic, Optional, Self, Union
8
9
  from uuid import uuid4
9
10
 
10
11
  from slixmpp import JID, Iq, Message, Presence
@@ -15,12 +16,15 @@ from slixmpp.plugins.xep_0060.stanza import Item
15
16
  from slixmpp.plugins.xep_0082 import parse as str_to_datetime
16
17
  from slixmpp.xmlstream import ET
17
18
 
19
+ from ..contact.contact import LegacyContact
18
20
  from ..contact.roster import ContactIsUser
19
21
  from ..core import config
22
+ from ..core.mixins import StoredAttributeMixin
20
23
  from ..core.mixins.avatar import AvatarMixin
21
24
  from ..core.mixins.disco import ChatterDiscoMixin
22
25
  from ..core.mixins.lock import NamedLockMixin
23
26
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
27
+ from ..db.models import Room
24
28
  from ..util import ABCSubclassableOnceAtMost
25
29
  from ..util.types import (
26
30
  LegacyGroupIdType,
@@ -33,9 +37,9 @@ from ..util.types import (
33
37
  )
34
38
  from ..util.util import deprecated
35
39
  from .archive import MessageArchive
40
+ from .participant import LegacyParticipant
36
41
 
37
42
  if TYPE_CHECKING:
38
- from ..contact import LegacyContact
39
43
  from ..core.gateway import BaseGateway
40
44
  from ..core.session import BaseSession
41
45
 
@@ -46,6 +50,7 @@ class LegacyMUC(
46
50
  Generic[
47
51
  LegacyGroupIdType, LegacyMessageType, LegacyParticipantType, LegacyUserIdType
48
52
  ],
53
+ StoredAttributeMixin,
49
54
  AvatarMixin,
50
55
  NamedLockMixin,
51
56
  ChatterDiscoMixin,
@@ -60,8 +65,6 @@ class LegacyMUC(
60
65
  on the user's :py:class:`slidge.core.session.BaseSession`.
61
66
  """
62
67
 
63
- subject_date: Optional[datetime] = None
64
- n_participants: Optional[int] = None
65
68
  max_history_fetch = 100
66
69
 
67
70
  type = MucType.CHANNEL
@@ -119,67 +122,99 @@ class LegacyMUC(
119
122
 
120
123
  _avatar_pubsub_broadcast = False
121
124
  _avatar_bare_jid = True
125
+ archive: MessageArchive
122
126
 
123
127
  def __init__(self, session: "BaseSession", legacy_id: LegacyGroupIdType, jid: JID):
124
- from .participant import LegacyParticipant
125
-
126
128
  self.session = session
127
129
  self.xmpp: "BaseGateway" = session.xmpp
128
- self.user = session.user
129
- self.log = logging.getLogger(f"{self.user.bare_jid}:muc:{jid}")
130
+ self.log = logging.getLogger(f"{self.user_jid.bare}:muc:{jid}")
130
131
 
131
132
  self.legacy_id = legacy_id
132
133
  self.jid = jid
133
134
 
134
- self.user_resources = set[str]()
135
+ self._user_resources = set[str]()
135
136
 
136
137
  self.Participant = LegacyParticipant.get_self_or_unique_subclass()
137
138
 
138
- self.xmpp.add_event_handler(
139
- "presence_unavailable", self._on_presence_unavailable
140
- )
141
-
142
139
  self._subject = ""
143
- self.subject_setter: Union[str, "LegacyContact", "LegacyParticipant"] = (
144
- self.get_system_participant()
140
+ self._subject_setter: Union[str, None, "LegacyContact", "LegacyParticipant"] = (
141
+ None
145
142
  )
146
143
 
147
- self.archive: MessageArchive = MessageArchive(str(self.jid), self.user)
144
+ self.pk: Optional[int] = None
148
145
  self._user_nick: Optional[str] = None
149
146
 
150
- self._participants_by_nicknames = dict[str, LegacyParticipantType]()
151
- self._participants_by_escaped_nicknames = dict[str, LegacyParticipantType]()
152
- self._participants_by_contacts = dict["LegacyContact", LegacyParticipantType]()
153
-
154
- self.__participants_filled = False
147
+ self._participants_filled = False
155
148
  self.__history_filled = False
156
149
  self._description = ""
150
+ self._subject_date: Optional[datetime] = None
151
+
152
+ self.__participants_store = self.xmpp.store.participants
153
+ self.__store = self.xmpp.store.rooms
154
+
155
+ self._n_participants: Optional[int] = None
156
+
157
157
  super().__init__()
158
158
 
159
+ @property
160
+ def n_participants(self):
161
+ return self._n_participants
162
+
163
+ @n_participants.setter
164
+ def n_participants(self, n_participants: Optional[int]):
165
+ if self._n_participants == n_participants:
166
+ return
167
+ self._n_participants = n_participants
168
+ assert self.pk is not None
169
+ self.__store.update_n_participants(self.pk, n_participants)
170
+
171
+ @property
172
+ def user_jid(self):
173
+ return self.session.user_jid
174
+
159
175
  def __repr__(self):
160
176
  return f"<MUC {self.legacy_id}/{self.jid}/{self.name}>"
161
177
 
178
+ @property
179
+ def subject_date(self) -> Optional[datetime]:
180
+ return self._subject_date
181
+
182
+ @subject_date.setter
183
+ def subject_date(self, when: Optional[datetime]) -> None:
184
+ self._subject_date = when
185
+ assert self.pk is not None
186
+ self.__store.update_subject_date(self.pk, when)
187
+
162
188
  def __send_configuration_change(self, codes):
163
189
  part = self.get_system_participant()
164
190
  part.send_configuration_change(codes)
165
191
 
166
192
  @property
167
193
  def user_nick(self):
168
- return (
169
- self._user_nick
170
- or self.session.bookmarks.user_nick
171
- or self.session.user.jid.node
172
- )
194
+ return self._user_nick or self.session.bookmarks.user_nick or self.user_jid.node
173
195
 
174
196
  @user_nick.setter
175
197
  def user_nick(self, nick: str):
176
198
  self._user_nick = nick
177
199
 
200
+ def add_user_resource(self, resource: str) -> None:
201
+ self._user_resources.add(resource)
202
+ assert self.pk is not None
203
+ self.__store.set_resource(self.pk, self._user_resources)
204
+
205
+ def get_user_resources(self) -> set[str]:
206
+ return self._user_resources
207
+
208
+ def remove_user_resource(self, resource: str) -> None:
209
+ self._user_resources.remove(resource)
210
+ assert self.pk is not None
211
+ self.__store.set_resource(self.pk, self._user_resources)
212
+
178
213
  async def __fill_participants(self):
179
214
  async with self.lock("fill participants"):
180
- if self.__participants_filled:
215
+ if self._participants_filled:
181
216
  return
182
- self.__participants_filled = True
217
+ self._participants_filled = True
183
218
  try:
184
219
  await self.fill_participants()
185
220
  except NotImplementedError:
@@ -230,22 +265,24 @@ class LegacyMUC(
230
265
  if self._description == d:
231
266
  return
232
267
  self._description = d
268
+ assert self.pk is not None
269
+ self.__store.update_description(self.pk, d)
233
270
  self.__send_configuration_change((104,))
234
271
 
235
- def _on_presence_unavailable(self, p: Presence):
272
+ def on_presence_unavailable(self, p: Presence):
236
273
  pto = p.get_to()
237
274
  if pto.bare != self.jid.bare:
238
275
  return
239
276
 
240
277
  pfrom = p.get_from()
241
- if pfrom.bare != self.user.bare_jid:
278
+ if pfrom.bare != self.user_jid.bare:
242
279
  return
243
- if (resource := pfrom.resource) in (resources := self.user_resources):
280
+ if (resource := pfrom.resource) in self._user_resources:
244
281
  if pto.resource != self.user_nick:
245
282
  self.log.debug(
246
283
  "Received 'leave group' request but with wrong nickname. %s", p
247
284
  )
248
- resources.remove(resource)
285
+ self.remove_user_resource(resource)
249
286
  else:
250
287
  self.log.debug(
251
288
  "Received 'leave group' request but resource was not listed. %s", p
@@ -300,7 +337,7 @@ class LegacyMUC(
300
337
  def subject(self, s: str):
301
338
  if s == self._subject:
302
339
  return
303
- self.xmpp.loop.create_task(
340
+ self.session.create_task(
304
341
  self.__get_subject_setter_participant()
305
342
  ).add_done_callback(
306
343
  lambda task: task.result().set_room_subject(
@@ -308,16 +345,23 @@ class LegacyMUC(
308
345
  )
309
346
  )
310
347
  self._subject = s
348
+ assert self.pk is not None
349
+ self.__store.update_subject(self.pk, s)
311
350
 
312
351
  @property
313
352
  def is_anonymous(self):
314
353
  return self.type == MucType.CHANNEL
315
354
 
316
- async def __get_subject_setter_participant(self):
317
- from slidge.contact import LegacyContact
355
+ @property
356
+ def subject_setter(self):
357
+ return self._subject_setter
318
358
 
319
- from .participant import LegacyParticipant
359
+ @subject_setter.setter
360
+ def subject_setter(self, subject_setter):
361
+ self._subject_setter = subject_setter
362
+ self.__store.update(self)
320
363
 
364
+ async def __get_subject_setter_participant(self):
321
365
  who = self.subject_setter
322
366
 
323
367
  if isinstance(who, LegacyParticipant):
@@ -341,6 +385,7 @@ class LegacyMUC(
341
385
  "vcard-temp",
342
386
  "urn:xmpp:ping",
343
387
  "urn:xmpp:occupant-id:0",
388
+ "jabber:iq:register",
344
389
  self.xmpp.plugin["xep_0425"].stanza.NS,
345
390
  ]
346
391
  if self.type == MucType.GROUP:
@@ -364,10 +409,10 @@ class LegacyMUC(
364
409
  form.add_field("muc#maxhistoryfetch", value=str(self.max_history_fetch))
365
410
  form.add_field("muc#roominfo_subjectmod", "boolean", value=False)
366
411
 
367
- if self._ALL_INFO_FILLED_ON_STARTUP or self.__participants_filled:
412
+ if self._ALL_INFO_FILLED_ON_STARTUP or self._participants_filled:
368
413
  n: Optional[int] = len(await self.get_participants())
369
414
  else:
370
- n = self.n_participants
415
+ n = self._n_participants
371
416
  if n is not None:
372
417
  form.add_field("muc#roominfo_occupants", value=str(n))
373
418
 
@@ -415,8 +460,8 @@ class LegacyMUC(
415
460
  presence.send()
416
461
 
417
462
  def user_full_jids(self):
418
- for r in self.user_resources:
419
- j = copy(self.user.jid)
463
+ for r in self._user_resources:
464
+ j = copy(self.user_jid)
420
465
  j.resource = r
421
466
  yield j
422
467
 
@@ -427,9 +472,9 @@ class LegacyMUC(
427
472
  return user_muc_jid
428
473
 
429
474
  def _legacy_to_xmpp(self, legacy_id: LegacyMessageType):
430
- return self.session.sent.get(legacy_id) or self.session.legacy_to_xmpp_msg_id(
431
- legacy_id
432
- )
475
+ return self.xmpp.store.sent.get_group_xmpp_id(
476
+ self.session.user_pk, str(legacy_id)
477
+ ) or self.session.legacy_to_xmpp_msg_id(legacy_id)
433
478
 
434
479
  async def echo(
435
480
  self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
@@ -458,7 +503,14 @@ class LegacyMUC(
458
503
 
459
504
  msg.send()
460
505
 
506
+ def _get_cached_avatar_id(self):
507
+ assert self.pk is not None
508
+ return self.xmpp.store.rooms.get_avatar_legacy_id(self.pk)
509
+
461
510
  def _post_avatar_update(self) -> None:
511
+ if self.pk is None or self._avatar_pk is None:
512
+ return
513
+ self.xmpp.store.rooms.set_avatar(self.pk, self._avatar_pk)
462
514
  self.__send_configuration_change((104,))
463
515
  self._send_room_presence()
464
516
 
@@ -480,10 +532,10 @@ class LegacyMUC(
480
532
  requested_nickname = join_presence.get_to().resource
481
533
  client_resource = user_full_jid.resource
482
534
 
483
- if client_resource in self.user_resources:
535
+ if client_resource in self._user_resources:
484
536
  self.log.debug("Received join from a resource that is already joined.")
485
537
 
486
- self.user_resources.add(client_resource)
538
+ self.add_user_resource(client_resource)
487
539
 
488
540
  if not requested_nickname or not client_resource:
489
541
  raise XMPPError("jid-malformed", by=self.jid)
@@ -491,18 +543,18 @@ class LegacyMUC(
491
543
  self.log.debug(
492
544
  "Resource %s of %s wants to join room %s with nickname %s",
493
545
  client_resource,
494
- self.user,
546
+ self.user_jid,
495
547
  self.legacy_id,
496
548
  requested_nickname,
497
549
  )
498
550
 
499
551
  await self.__fill_participants()
500
552
 
501
- for participant in self._participants_by_nicknames.values():
502
- if participant.is_user: # type:ignore
503
- continue
504
- if participant.is_system: # type:ignore
505
- continue
553
+ assert self.pk is not None
554
+ for db_participant in self.xmpp.store.participants.get_all(
555
+ self.pk, user_included=False
556
+ ):
557
+ participant = self.Participant.from_store(self.session, db_participant)
506
558
  participant.send_initial_presence(full_jid=user_full_jid)
507
559
 
508
560
  user_nick = self.user_nick
@@ -558,13 +610,13 @@ class LegacyMUC(
558
610
  self.__store_participant(p)
559
611
  return p
560
612
 
561
- def __store_participant(self, p: "LegacyParticipantType"):
613
+ def __store_participant(self, p: "LegacyParticipantType") -> None:
562
614
  # we don't want to update the participant list when we're filling history
563
615
  if not self.KEEP_BACKFILLED_PARTICIPANTS and self.get_lock("fill history"):
564
616
  return
565
- self._participants_by_nicknames[p.nickname] = p # type:ignore
566
- if p.contact:
567
- self._participants_by_contacts[p.contact] = p
617
+ assert self.pk is not None
618
+ p.pk = self.__participants_store.add(self.pk, p.nickname)
619
+ self.__participants_store.update(p)
568
620
 
569
621
  async def get_participant(
570
622
  self,
@@ -593,23 +645,27 @@ class LegacyMUC(
593
645
  """
594
646
  if fill_first:
595
647
  await self.__fill_participants()
596
- p = self._participants_by_nicknames.get(
597
- nickname
598
- ) or self._participants_by_escaped_nicknames.get(nickname)
599
- if p is None:
600
- if raise_if_not_found:
601
- raise XMPPError("item-not-found")
602
- p = self.Participant(self, nickname, **kwargs)
603
- if store:
604
- self.__store_participant(p)
605
- if (
606
- not self.get_lock("fill participants")
607
- and not self.get_lock("fill history")
608
- and self.__participants_filled
609
- and not p.is_user
610
- and not p.is_system
611
- ):
612
- p.send_affiliation_change()
648
+ assert self.pk is not None
649
+ with self.xmpp.store.session():
650
+ stored = self.__participants_store.get_by_nickname(
651
+ self.pk, nickname
652
+ ) or self.__participants_store.get_by_resource(self.pk, nickname)
653
+ if stored is not None:
654
+ return self.Participant.from_store(self.session, stored)
655
+
656
+ if raise_if_not_found:
657
+ raise XMPPError("item-not-found")
658
+ p = self.Participant(self, nickname, **kwargs)
659
+ if store:
660
+ self.__store_participant(p)
661
+ if (
662
+ not self.get_lock("fill participants")
663
+ and not self.get_lock("fill history")
664
+ and self._participants_filled
665
+ and not p.is_user
666
+ and not p.is_system
667
+ ):
668
+ p.send_affiliation_change()
613
669
  return p
614
670
 
615
671
  def get_system_participant(self) -> "LegacyParticipantType":
@@ -638,25 +694,30 @@ class LegacyMUC(
638
694
  :return:
639
695
  """
640
696
  await self.session.contacts.ready
641
- p = self._participants_by_contacts.get(c)
642
- if p is None:
643
- nickname = c.name or _unescape_node(c.jid_username)
644
- if nickname in self._participants_by_nicknames:
645
- self.log.debug("Nickname conflict")
646
- nickname = f"{nickname} ({c.jid_username})"
647
- p = self.Participant(self, nickname, **kwargs)
648
- p.contact = c
649
- c.participants.add(p)
650
- # FIXME: this is not great but given the current design,
651
- # during participants fill and history backfill we do not
652
- # want to send presence, because we might update affiliation
653
- # and role afterwards.
654
- # We need a refactor of the MUC class… later™
655
- if not self.get_lock("fill participants") and not self.get_lock(
656
- "fill history"
657
- ):
658
- p.send_last_presence(force=True, no_cache_online=True)
659
- self.__store_participant(p)
697
+ assert self.pk is not None
698
+ assert c.contact_pk is not None
699
+ with self.__store.session():
700
+ stored = self.__participants_store.get_by_contact(self.pk, c.contact_pk)
701
+ if stored is not None:
702
+ return self.Participant.from_store(
703
+ self.session, stored, muc=self, contact=c
704
+ )
705
+
706
+ nickname = c.name or _unescape_node(c.jid_username)
707
+ if not self.__store.nickname_is_available(self.pk, nickname):
708
+ self.log.debug("Nickname conflict")
709
+ nickname = f"{nickname} ({c.jid_username})"
710
+ p = self.Participant(self, nickname, **kwargs)
711
+ p.contact = c
712
+
713
+ # FIXME: this is not great but given the current design,
714
+ # during participants fill and history backfill we do not
715
+ # want to send presence, because we might update affiliation
716
+ # and role afterwards.
717
+ # We need a refactor of the MUC class… later™
718
+ self.__store_participant(p)
719
+ if not self.get_lock("fill participants") and not self.get_lock("fill history"):
720
+ p.send_last_presence(force=True, no_cache_online=True)
660
721
  return p
661
722
 
662
723
  async def get_participant_by_legacy_id(
@@ -668,15 +729,20 @@ class LegacyMUC(
668
729
  return await self.get_user_participant(**kwargs)
669
730
  return await self.get_participant_by_contact(c, **kwargs)
670
731
 
671
- async def get_participants(self):
732
+ async def get_participants(self, fill_first=True):
672
733
  """
673
734
  Get all known participants of the group, ensure :meth:`.LegacyMUC.fill_participants`
674
735
  has been awaited once before. Plugins should not use that, internal
675
736
  slidge use only.
676
737
  :return:
677
738
  """
678
- await self.__fill_participants()
679
- return list(self._participants_by_nicknames.values())
739
+ if fill_first:
740
+ await self.__fill_participants()
741
+ assert self.pk is not None
742
+ return [
743
+ self.Participant.from_store(self.session, s)
744
+ for s in self.__participants_store.get_all(self.pk)
745
+ ]
680
746
 
681
747
  def remove_participant(self, p: "LegacyParticipantType", kick=False, ban=False):
682
748
  """
@@ -688,19 +754,7 @@ class LegacyMUC(
688
754
  """
689
755
  if kick and ban:
690
756
  raise TypeError("Either kick or ban")
691
- if p.contact is not None:
692
- try:
693
- del self._participants_by_contacts[p.contact]
694
- except KeyError:
695
- self.log.warning(
696
- "Removed a participant we didn't know was here?, %s", p
697
- )
698
- else:
699
- p.contact.participants.remove(p)
700
- try:
701
- del self._participants_by_nicknames[p.nickname] # type:ignore
702
- except KeyError:
703
- self.log.warning("Removed a participant we didn't know was here?, %s", p)
757
+ self.__participants_store.delete(p.pk)
704
758
  if kick:
705
759
  codes = {307}
706
760
  elif ban:
@@ -713,14 +767,15 @@ class LegacyMUC(
713
767
  p._send(presence)
714
768
 
715
769
  def rename_participant(self, old_nickname: str, new_nickname: str):
716
- try:
717
- p = self._participants_by_nicknames.pop(old_nickname)
718
- except KeyError:
719
- # when called by participant.nickname.setter
720
- return
721
- self._participants_by_nicknames[new_nickname] = p
722
- if p.nickname == old_nickname:
723
- p.nickname = new_nickname
770
+ assert self.pk is not None
771
+ with self.xmpp.store.session():
772
+ stored = self.__participants_store.get_by_nickname(self.pk, old_nickname)
773
+ if stored is None:
774
+ self.log.debug("Tried to rename a participant that we didn't know")
775
+ return
776
+ p = self.Participant.from_store(self.session, stored)
777
+ if p.nickname == old_nickname:
778
+ p.nickname = new_nickname
724
779
 
725
780
  async def __old_school_history(
726
781
  self,
@@ -849,7 +904,7 @@ class LegacyMUC(
849
904
 
850
905
  :param r: The resource to kick
851
906
  """
852
- pto = self.user.jid
907
+ pto = self.user_jid
853
908
  pto.resource = r
854
909
  p = self.xmpp.make_presence(
855
910
  pfrom=(await self.get_user_participant()).jid, pto=pto
@@ -882,7 +937,7 @@ class LegacyMUC(
882
937
  item = Item()
883
938
  item["id"] = self.jid
884
939
 
885
- iq = Iq(stype="get", sfrom=self.user.jid, sto=self.user.jid)
940
+ iq = Iq(stype="get", sfrom=self.user_jid, sto=self.user_jid)
886
941
  iq["pubsub"]["items"]["node"] = self.xmpp["xep_0402"].stanza.NS
887
942
  iq["pubsub"]["items"].append(item)
888
943
 
@@ -912,7 +967,7 @@ class LegacyMUC(
912
967
  item["conference"]["autojoin"] = auto_join
913
968
 
914
969
  item["conference"]["nick"] = self.user_nick
915
- iq = Iq(stype="set", sfrom=self.user.jid, sto=self.user.jid)
970
+ iq = Iq(stype="set", sfrom=self.user_jid, sto=self.user_jid)
916
971
  iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0402"].stanza.NS
917
972
  iq["pubsub"]["publish"].append(item)
918
973
 
@@ -1015,30 +1070,39 @@ class LegacyMUC(
1015
1070
  raise NotImplementedError
1016
1071
 
1017
1072
  async def parse_mentions(self, text: str) -> list[Mention]:
1018
- await self.__fill_participants()
1019
-
1020
- if len(self._participants_by_nicknames) == 0:
1021
- return []
1022
-
1023
- result = []
1024
- for match in re.finditer(
1025
- "|".join(
1026
- sorted(
1027
- [re.escape(nick) for nick in self._participants_by_nicknames],
1028
- key=lambda nick: len(nick),
1029
- reverse=True,
1030
- )
1031
- ),
1032
- text,
1033
- ):
1034
- span = match.span()
1035
- nick = match.group()
1036
- if span[0] != 0 and text[span[0] - 1] not in _WHITESPACE_OR_PUNCTUATION:
1037
- continue
1038
- if span[1] == len(text) or text[span[1]] in _WHITESPACE_OR_PUNCTUATION:
1039
- participant = self._participants_by_nicknames[nick]
1040
- if contact := participant.contact:
1041
- result.append(Mention(contact=contact, start=span[0], end=span[1]))
1073
+ with self.__store.session():
1074
+ await self.__fill_participants()
1075
+ assert self.pk is not None
1076
+ participants = {
1077
+ p.nickname: p for p in self.__participants_store.get_all(self.pk)
1078
+ }
1079
+
1080
+ if len(participants) == 0:
1081
+ return []
1082
+
1083
+ result = []
1084
+ for match in re.finditer(
1085
+ "|".join(
1086
+ sorted(
1087
+ [re.escape(nick) for nick in participants.keys()],
1088
+ key=lambda nick: len(nick),
1089
+ reverse=True,
1090
+ )
1091
+ ),
1092
+ text,
1093
+ ):
1094
+ span = match.span()
1095
+ nick = match.group()
1096
+ if span[0] != 0 and text[span[0] - 1] not in _WHITESPACE_OR_PUNCTUATION:
1097
+ continue
1098
+ if span[1] == len(text) or text[span[1]] in _WHITESPACE_OR_PUNCTUATION:
1099
+ participant = self.Participant.from_store(
1100
+ self.session, participants[nick]
1101
+ )
1102
+ if contact := participant.contact:
1103
+ result.append(
1104
+ Mention(contact=contact, start=span[0], end=span[1])
1105
+ )
1042
1106
  return result
1043
1107
 
1044
1108
  async def on_set_subject(self, subject: str) -> None:
@@ -1052,6 +1116,42 @@ class LegacyMUC(
1052
1116
  """
1053
1117
  raise NotImplementedError
1054
1118
 
1119
+ @classmethod
1120
+ def from_store(cls, session, stored: Room, *args, **kwargs) -> Self:
1121
+ muc = cls(
1122
+ session,
1123
+ cls.xmpp.LEGACY_ROOM_ID_TYPE(stored.legacy_id),
1124
+ stored.jid,
1125
+ *args, # type: ignore
1126
+ **kwargs, # type: ignore
1127
+ )
1128
+ muc.pk = stored.id
1129
+ muc.type = stored.muc_type # type: ignore
1130
+ if stored.name:
1131
+ muc.DISCO_NAME = stored.name
1132
+ if stored.description:
1133
+ muc._description = stored.description
1134
+ if (data := stored.extra_attributes) is not None:
1135
+ muc.deserialize_extra_attributes(data)
1136
+ muc._subject = stored.subject or ""
1137
+ if stored.subject_date is not None:
1138
+ muc._subject_date = stored.subject_date.replace(tzinfo=timezone.utc)
1139
+ muc._participants_filled = stored.participants_filled
1140
+ muc.__history_filled = True
1141
+ if stored.user_resources is not None:
1142
+ muc._user_resources = set(json.loads(stored.user_resources))
1143
+ if stored.subject_setter is not None:
1144
+ muc.subject_setter = (
1145
+ LegacyParticipant.get_self_or_unique_subclass().from_store(
1146
+ session,
1147
+ stored.subject_setter,
1148
+ muc=muc,
1149
+ )
1150
+ )
1151
+ muc.archive = MessageArchive(muc.pk, session.xmpp.store.mam)
1152
+ muc._set_avatar_from_store(stored)
1153
+ return muc
1154
+
1055
1155
 
1056
1156
  def set_origin_id(msg: Message, origin_id: str):
1057
1157
  sub = ET.Element("{urn:xmpp:sid:0}origin-id")