slidge 0.1.3__py3-none-any.whl → 0.2.0a0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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 +100 -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 +77 -25
  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.3.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.3.dist-info/RECORD +0 -96
  61. {slidge-0.1.3.dist-info → slidge-0.2.0a0.dist-info}/LICENSE +0 -0
  62. {slidge-0.1.3.dist-info → slidge-0.2.0a0.dist-info}/WHEEL +0 -0
  63. {slidge-0.1.3.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")