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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -196
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +8 -1
  5. slidge/command/admin.py +6 -7
  6. slidge/command/base.py +1 -2
  7. slidge/command/register.py +32 -16
  8. slidge/command/user.py +85 -6
  9. slidge/contact/contact.py +165 -49
  10. slidge/contact/roster.py +122 -47
  11. slidge/core/config.py +14 -11
  12. slidge/core/gateway/base.py +148 -36
  13. slidge/core/gateway/caps.py +7 -5
  14. slidge/core/gateway/disco.py +2 -4
  15. slidge/core/gateway/mam.py +1 -4
  16. slidge/core/gateway/muc_admin.py +1 -1
  17. slidge/core/gateway/ping.py +2 -3
  18. slidge/core/gateway/presence.py +1 -1
  19. slidge/core/gateway/registration.py +32 -21
  20. slidge/core/gateway/search.py +3 -5
  21. slidge/core/gateway/session_dispatcher.py +120 -57
  22. slidge/core/gateway/vcard_temp.py +7 -5
  23. slidge/core/mixins/__init__.py +11 -1
  24. slidge/core/mixins/attachment.py +32 -14
  25. slidge/core/mixins/avatar.py +90 -25
  26. slidge/core/mixins/base.py +8 -2
  27. slidge/core/mixins/db.py +18 -0
  28. slidge/core/mixins/disco.py +0 -10
  29. slidge/core/mixins/message.py +18 -8
  30. slidge/core/mixins/message_maker.py +17 -9
  31. slidge/core/mixins/presence.py +17 -4
  32. slidge/core/pubsub.py +54 -220
  33. slidge/core/session.py +69 -34
  34. slidge/db/__init__.py +4 -0
  35. slidge/db/alembic/env.py +64 -0
  36. slidge/db/alembic/script.py.mako +26 -0
  37. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  38. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  39. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  40. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  41. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  42. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
  43. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +85 -0
  44. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  45. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
  46. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  47. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  48. slidge/db/avatar.py +235 -0
  49. slidge/db/meta.py +65 -0
  50. slidge/db/models.py +375 -0
  51. slidge/db/store.py +1078 -0
  52. slidge/group/archive.py +58 -14
  53. slidge/group/bookmarks.py +72 -57
  54. slidge/group/participant.py +87 -28
  55. slidge/group/room.py +369 -211
  56. slidge/main.py +201 -0
  57. slidge/migration.py +30 -0
  58. slidge/slixfix/__init__.py +35 -2
  59. slidge/slixfix/roster.py +11 -4
  60. slidge/slixfix/xep_0292/vcard4.py +3 -0
  61. slidge/util/archive_msg.py +2 -1
  62. slidge/util/db.py +1 -47
  63. slidge/util/test.py +71 -4
  64. slidge/util/types.py +29 -4
  65. slidge/util/util.py +22 -0
  66. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/METADATA +4 -4
  67. slidge-0.2.0a1.dist-info/RECORD +114 -0
  68. slidge/core/cache.py +0 -183
  69. slidge/util/schema.sql +0 -126
  70. slidge/util/sql.py +0 -508
  71. slidge-0.1.3.dist-info/RECORD +0 -96
  72. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/LICENSE +0 -0
  73. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/WHEEL +0 -0
  74. {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/entry_points.txt +0 -0
slidge/group/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, AsyncIterator, Generic, Optional, Self, Union
8
9
  from uuid import uuid4
9
10
 
10
11
  from slixmpp import JID, Iq, Message, Presence
@@ -15,14 +16,19 @@ 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
24
+ from ..core.mixins.db import UpdateInfoMixin
21
25
  from ..core.mixins.disco import ChatterDiscoMixin
22
26
  from ..core.mixins.lock import NamedLockMixin
23
27
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
28
+ from ..db.models import Room
24
29
  from ..util import ABCSubclassableOnceAtMost
25
30
  from ..util.types import (
31
+ HoleBound,
26
32
  LegacyGroupIdType,
27
33
  LegacyMessageType,
28
34
  LegacyParticipantType,
@@ -31,21 +37,25 @@ from ..util.types import (
31
37
  MucAffiliation,
32
38
  MucType,
33
39
  )
34
- from ..util.util import deprecated
40
+ from ..util.util import deprecated, timeit, with_session
35
41
  from .archive import MessageArchive
42
+ from .participant import LegacyParticipant
36
43
 
37
44
  if TYPE_CHECKING:
38
- from ..contact import LegacyContact
39
45
  from ..core.gateway import BaseGateway
40
46
  from ..core.session import BaseSession
41
47
 
42
48
  ADMIN_NS = "http://jabber.org/protocol/muc#admin"
43
49
 
50
+ SubjectSetterType = Union[str, None, "LegacyContact", "LegacyParticipant"]
51
+
44
52
 
45
53
  class LegacyMUC(
46
54
  Generic[
47
55
  LegacyGroupIdType, LegacyMessageType, LegacyParticipantType, LegacyUserIdType
48
56
  ],
57
+ UpdateInfoMixin,
58
+ StoredAttributeMixin,
49
59
  AvatarMixin,
50
60
  NamedLockMixin,
51
61
  ChatterDiscoMixin,
@@ -60,8 +70,6 @@ class LegacyMUC(
60
70
  on the user's :py:class:`slidge.core.session.BaseSession`.
61
71
  """
62
72
 
63
- subject_date: Optional[datetime] = None
64
- n_participants: Optional[int] = None
65
73
  max_history_fetch = 100
66
74
 
67
75
  type = MucType.CHANNEL
@@ -117,98 +125,158 @@ class LegacyMUC(
117
125
  tries to set the room subject.
118
126
  """
119
127
 
120
- _avatar_pubsub_broadcast = False
121
128
  _avatar_bare_jid = True
129
+ archive: MessageArchive
122
130
 
123
131
  def __init__(self, session: "BaseSession", legacy_id: LegacyGroupIdType, jid: JID):
124
- from .participant import LegacyParticipant
125
-
126
132
  self.session = session
127
133
  self.xmpp: "BaseGateway" = session.xmpp
128
- self.user = session.user
129
- self.log = logging.getLogger(f"{self.user.bare_jid}:muc:{jid}")
134
+ self.log = logging.getLogger(f"{self.user_jid.bare}:muc:{jid}")
130
135
 
131
136
  self.legacy_id = legacy_id
132
137
  self.jid = jid
133
138
 
134
- self.user_resources = set[str]()
139
+ self._user_resources = set[str]()
135
140
 
136
141
  self.Participant = LegacyParticipant.get_self_or_unique_subclass()
137
142
 
138
- self.xmpp.add_event_handler(
139
- "presence_unavailable", self._on_presence_unavailable
140
- )
141
-
142
143
  self._subject = ""
143
- self.subject_setter: Union[str, "LegacyContact", "LegacyParticipant"] = (
144
- self.get_system_participant()
145
- )
144
+ self._subject_setter: Optional[str] = None
146
145
 
147
- self.archive: MessageArchive = MessageArchive(str(self.jid), self.user)
146
+ self.pk: Optional[int] = None
148
147
  self._user_nick: Optional[str] = None
149
148
 
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
155
- self.__history_filled = False
149
+ self._participants_filled = False
150
+ self._history_filled = False
156
151
  self._description = ""
152
+ self._subject_date: Optional[datetime] = None
153
+
154
+ self.__participants_store = self.xmpp.store.participants
155
+ self.__store = self.xmpp.store.rooms
156
+
157
+ self._n_participants: Optional[int] = None
158
+
157
159
  super().__init__()
158
160
 
161
+ @property
162
+ def n_participants(self):
163
+ return self._n_participants
164
+
165
+ @n_participants.setter
166
+ def n_participants(self, n_participants: Optional[int]):
167
+ if self._n_participants == n_participants:
168
+ return
169
+ self._n_participants = n_participants
170
+ if self._updating_info:
171
+ return
172
+ assert self.pk is not None
173
+ self.__store.update_n_participants(self.pk, n_participants)
174
+
175
+ @property
176
+ def user_jid(self):
177
+ return self.session.user_jid
178
+
159
179
  def __repr__(self):
160
180
  return f"<MUC {self.legacy_id}/{self.jid}/{self.name}>"
161
181
 
182
+ @property
183
+ def subject_date(self) -> Optional[datetime]:
184
+ return self._subject_date
185
+
186
+ @subject_date.setter
187
+ def subject_date(self, when: Optional[datetime]) -> None:
188
+ self._subject_date = when
189
+ if self._updating_info:
190
+ return
191
+ assert self.pk is not None
192
+ self.__store.update_subject_date(self.pk, when)
193
+
162
194
  def __send_configuration_change(self, codes):
163
195
  part = self.get_system_participant()
164
196
  part.send_configuration_change(codes)
165
197
 
166
198
  @property
167
199
  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
- )
200
+ return self._user_nick or self.session.bookmarks.user_nick or self.user_jid.node
173
201
 
174
202
  @user_nick.setter
175
203
  def user_nick(self, nick: str):
176
204
  self._user_nick = nick
177
205
 
206
+ def add_user_resource(self, resource: str) -> None:
207
+ self._user_resources.add(resource)
208
+ assert self.pk is not None
209
+ self.__store.set_resource(self.pk, self._user_resources)
210
+
211
+ def get_user_resources(self) -> set[str]:
212
+ return self._user_resources
213
+
214
+ def remove_user_resource(self, resource: str) -> None:
215
+ self._user_resources.remove(resource)
216
+ assert self.pk is not None
217
+ self.__store.set_resource(self.pk, self._user_resources)
218
+
178
219
  async def __fill_participants(self):
220
+ if self._participants_filled:
221
+ return
222
+ assert self.pk is not None
179
223
  async with self.lock("fill participants"):
180
- if self.__participants_filled:
181
- return
182
- self.__participants_filled = True
183
- try:
184
- await self.fill_participants()
185
- except NotImplementedError:
186
- pass
224
+ self._participants_filled = True
225
+ async for p in self.fill_participants():
226
+ self.__participants_store.update(p)
227
+ self.__store.set_participants_filled(self.pk)
228
+
229
+ async def get_participants(self) -> AsyncIterator[LegacyParticipant]:
230
+ assert self.pk is not None
231
+ if self._participants_filled:
232
+ for db_participant in self.xmpp.store.participants.get_all(
233
+ self.pk, user_included=True
234
+ ):
235
+ participant = self.Participant.from_store(self.session, db_participant)
236
+ yield participant
237
+ return
238
+
239
+ async with self.lock("fill participants"):
240
+ self._participants_filled = True
241
+ # We only fill the participants list if/when the MUC is first
242
+ # joined by an XMPP client. But we may have instantiated
243
+ resources = set[str]()
244
+ for db_participant in self.xmpp.store.participants.get_all(
245
+ self.pk, user_included=True
246
+ ):
247
+ participant = self.Participant.from_store(self.session, db_participant)
248
+ resources.add(participant.jid.resource)
249
+ yield participant
250
+ async for p in self.fill_participants():
251
+ if p.jid.resource not in resources:
252
+ yield p
253
+ self.__store.set_participants_filled(self.pk)
254
+ return
187
255
 
188
256
  async def __fill_history(self):
189
257
  async with self.lock("fill history"):
190
- if self.__history_filled:
258
+ if self._history_filled:
191
259
  log.debug("History has already been fetched %s", self)
192
260
  return
193
261
  log.debug("Fetching history for %s", self)
194
- for msg in self.archive:
195
- try:
196
- legacy_id = self.session.xmpp_to_legacy_msg_id(msg.id)
197
- oldest_date = msg.when
198
- except Exception as e:
199
- # not all archived stanzas have a valid legacy msg ID, eg
200
- # reactions, corrections, message with multiple attachments…
201
- self.log.debug(f"Could not convert during history back-filling {e}")
202
- else:
203
- break
204
- else:
205
- legacy_id = None
206
- oldest_date = None
207
262
  try:
208
- await self.backfill(legacy_id, oldest_date)
263
+ before, after = self.archive.get_hole_bounds()
264
+ if before is not None:
265
+ before = before._replace(
266
+ id=self.xmpp.LEGACY_MSG_ID_TYPE(before.id) # type:ignore
267
+ )
268
+ if after is not None:
269
+ after = after._replace(
270
+ id=self.xmpp.LEGACY_MSG_ID_TYPE(after.id) # type:ignore
271
+ )
272
+ await self.backfill(before, after)
209
273
  except NotImplementedError:
210
274
  return
211
- self.__history_filled = True
275
+ except Exception as e:
276
+ log.exception("Could not backfill: %s", e)
277
+ assert self.pk is not None
278
+ self.__store.set_history_filled(self.pk, True)
279
+ self._history_filled = True
212
280
 
213
281
  @property
214
282
  def name(self):
@@ -220,6 +288,10 @@ class LegacyMUC(
220
288
  return
221
289
  self.DISCO_NAME = n
222
290
  self.__send_configuration_change((104,))
291
+ if self._updating_info:
292
+ return
293
+ assert self.pk is not None
294
+ self.__store.update_name(self.pk, n)
223
295
 
224
296
  @property
225
297
  def description(self):
@@ -231,21 +303,25 @@ class LegacyMUC(
231
303
  return
232
304
  self._description = d
233
305
  self.__send_configuration_change((104,))
306
+ if self._updating_info:
307
+ return
308
+ assert self.pk is not None
309
+ self.__store.update_description(self.pk, d)
234
310
 
235
- def _on_presence_unavailable(self, p: Presence):
311
+ def on_presence_unavailable(self, p: Presence):
236
312
  pto = p.get_to()
237
313
  if pto.bare != self.jid.bare:
238
314
  return
239
315
 
240
316
  pfrom = p.get_from()
241
- if pfrom.bare != self.user.bare_jid:
317
+ if pfrom.bare != self.user_jid.bare:
242
318
  return
243
- if (resource := pfrom.resource) in (resources := self.user_resources):
319
+ if (resource := pfrom.resource) in self._user_resources:
244
320
  if pto.resource != self.user_nick:
245
321
  self.log.debug(
246
322
  "Received 'leave group' request but with wrong nickname. %s", p
247
323
  )
248
- resources.remove(resource)
324
+ self.remove_user_resource(resource)
249
325
  else:
250
326
  self.log.debug(
251
327
  "Received 'leave group' request but resource was not listed. %s", p
@@ -270,27 +346,35 @@ class LegacyMUC(
270
346
 
271
347
  async def backfill(
272
348
  self,
273
- oldest_message_id: Optional[LegacyMessageType] = None,
274
- oldest_message_date: Optional[datetime] = None,
349
+ after: Optional[HoleBound] = None,
350
+ before: Optional[HoleBound] = None,
275
351
  ):
276
352
  """
277
- Override this if the legacy network provide server-side archive.
278
- In it, send history messages using ``self.get_participant().send*``,
279
- with the ``archive_only=True`` kwarg.
280
-
281
- You only need to fetch messages older than ``oldest_message_id``.
282
-
283
- :param oldest_message_id: The oldest message ID already present in the archive
284
- :param oldest_message_date: The oldest message date already present in the archive
353
+ Override this if the legacy network provide server-side group archives.
354
+
355
+ In it, send history messages using ``self.get_participant(xxx).send_xxxx``,
356
+ with the ``archive_only=True`` kwarg. This is only called once per slidge
357
+ run for a given group.
358
+
359
+ :param after: Fetch messages after this one. If ``None``, it's up to you
360
+ to decide how far you want to go in the archive. If it's not ``None``,
361
+ it means slidge has some messages in this archive and you should really try
362
+ to complete it to avoid "holes" in the history of this group.
363
+ :param before: Fetch messages before this one. If ``None``, fetch all messages
364
+ up to the most recent one
285
365
  """
286
366
  raise NotImplementedError
287
367
 
288
- async def fill_participants(self):
368
+ async def fill_participants(self) -> AsyncIterator[LegacyParticipant]:
289
369
  """
290
- In here, call self.get_participant(), self.get_participant_by_contact(),
291
- of self.get_user_participant() to make an initial list of participants.
370
+ This method should yield the list of all members of this group.
371
+
372
+ Typically, use ``participant = self.get_participant()``, self.get_participant_by_contact(),
373
+ of self.get_user_participant(), and update their affiliation, hats, etc.
374
+ before yielding them.
292
375
  """
293
- raise NotImplementedError
376
+ return
377
+ yield
294
378
 
295
379
  @property
296
380
  def subject(self):
@@ -300,34 +384,44 @@ class LegacyMUC(
300
384
  def subject(self, s: str):
301
385
  if s == self._subject:
302
386
  return
303
- self.xmpp.loop.create_task(
304
- self.__get_subject_setter_participant()
305
- ).add_done_callback(
306
- lambda task: task.result().set_room_subject(
307
- s, None, self.subject_date, False
308
- )
387
+ self.__get_subject_setter_participant().set_room_subject(
388
+ s, None, self.subject_date, False
309
389
  )
390
+
310
391
  self._subject = s
392
+ if self._updating_info:
393
+ return
394
+ assert self.pk is not None
395
+ self.__store.update_subject(self.pk, s)
311
396
 
312
397
  @property
313
398
  def is_anonymous(self):
314
399
  return self.type == MucType.CHANNEL
315
400
 
316
- async def __get_subject_setter_participant(self):
317
- from slidge.contact import LegacyContact
401
+ @property
402
+ def subject_setter(self) -> Optional[str]:
403
+ return self._subject_setter
318
404
 
319
- from .participant import LegacyParticipant
405
+ @subject_setter.setter
406
+ def subject_setter(self, subject_setter: SubjectSetterType) -> None:
407
+ if isinstance(subject_setter, LegacyContact):
408
+ subject_setter = subject_setter.name
409
+ elif isinstance(subject_setter, LegacyParticipant):
410
+ subject_setter = subject_setter.nickname
320
411
 
321
- who = self.subject_setter
412
+ if subject_setter == self._subject_setter:
413
+ return
414
+ assert isinstance(subject_setter, str)
415
+ self._subject_setter = subject_setter
416
+ if self._updating_info:
417
+ return
418
+ assert self.pk is not None
419
+ self.__store.update_subject_setter(self.pk, subject_setter)
322
420
 
323
- if isinstance(who, LegacyParticipant):
324
- return who
325
- elif isinstance(who, str):
326
- return await self.get_participant(who, store=False)
327
- elif isinstance(self.subject_setter, LegacyContact):
328
- return await self.get_participant_by_contact(who)
329
- else:
421
+ def __get_subject_setter_participant(self) -> LegacyParticipant:
422
+ if self._subject_setter is None:
330
423
  return self.get_system_participant()
424
+ return self.Participant(self, self._subject_setter)
331
425
 
332
426
  def features(self):
333
427
  features = [
@@ -341,6 +435,7 @@ class LegacyMUC(
341
435
  "vcard-temp",
342
436
  "urn:xmpp:ping",
343
437
  "urn:xmpp:occupant-id:0",
438
+ "jabber:iq:register",
344
439
  self.xmpp.plugin["xep_0425"].stanza.NS,
345
440
  ]
346
441
  if self.type == MucType.GROUP:
@@ -364,10 +459,11 @@ class LegacyMUC(
364
459
  form.add_field("muc#maxhistoryfetch", value=str(self.max_history_fetch))
365
460
  form.add_field("muc#roominfo_subjectmod", "boolean", value=False)
366
461
 
367
- if self._ALL_INFO_FILLED_ON_STARTUP or self.__participants_filled:
368
- n: Optional[int] = len(await self.get_participants())
462
+ if self._ALL_INFO_FILLED_ON_STARTUP or self._participants_filled:
463
+ assert self.pk is not None
464
+ n: Optional[int] = self.__participants_store.get_count(self.pk)
369
465
  else:
370
- n = self.n_participants
466
+ n = self._n_participants
371
467
  if n is not None:
372
468
  form.add_field("muc#roominfo_occupants", value=str(n))
373
469
 
@@ -415,8 +511,8 @@ class LegacyMUC(
415
511
  presence.send()
416
512
 
417
513
  def user_full_jids(self):
418
- for r in self.user_resources:
419
- j = copy(self.user.jid)
514
+ for r in self._user_resources:
515
+ j = copy(self.user_jid)
420
516
  j.resource = r
421
517
  yield j
422
518
 
@@ -427,9 +523,9 @@ class LegacyMUC(
427
523
  return user_muc_jid
428
524
 
429
525
  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
- )
526
+ return self.xmpp.store.sent.get_group_xmpp_id(
527
+ self.session.user_pk, str(legacy_id)
528
+ ) or self.session.legacy_to_xmpp_msg_id(legacy_id)
433
529
 
434
530
  async def echo(
435
531
  self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
@@ -458,7 +554,16 @@ class LegacyMUC(
458
554
 
459
555
  msg.send()
460
556
 
557
+ def _get_cached_avatar_id(self):
558
+ if self.pk is None:
559
+ return None
560
+ return self.xmpp.store.rooms.get_avatar_legacy_id(self.pk)
561
+
461
562
  def _post_avatar_update(self) -> None:
563
+ if self.pk is None:
564
+ return
565
+ assert self.pk is not None
566
+ self.xmpp.store.rooms.set_avatar(self.pk, self._avatar_pk)
462
567
  self.__send_configuration_change((104,))
463
568
  self._send_room_presence()
464
569
 
@@ -475,15 +580,17 @@ class LegacyMUC(
475
580
  p["vcard_temp_update"]["photo"] = ""
476
581
  p.send()
477
582
 
583
+ @timeit
584
+ @with_session
478
585
  async def join(self, join_presence: Presence):
479
586
  user_full_jid = join_presence.get_from()
480
587
  requested_nickname = join_presence.get_to().resource
481
588
  client_resource = user_full_jid.resource
482
589
 
483
- if client_resource in self.user_resources:
590
+ if client_resource in self._user_resources:
484
591
  self.log.debug("Received join from a resource that is already joined.")
485
592
 
486
- self.user_resources.add(client_resource)
593
+ self.add_user_resource(client_resource)
487
594
 
488
595
  if not requested_nickname or not client_resource:
489
596
  raise XMPPError("jid-malformed", by=self.jid)
@@ -491,22 +598,21 @@ class LegacyMUC(
491
598
  self.log.debug(
492
599
  "Resource %s of %s wants to join room %s with nickname %s",
493
600
  client_resource,
494
- self.user,
601
+ self.user_jid,
495
602
  self.legacy_id,
496
603
  requested_nickname,
497
604
  )
498
605
 
499
- await self.__fill_participants()
500
-
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
606
+ user_nick = self.user_nick
607
+ user_participant = None
608
+ async for participant in self.get_participants():
609
+ if participant.is_user:
610
+ user_participant = participant
505
611
  continue
506
612
  participant.send_initial_presence(full_jid=user_full_jid)
507
613
 
508
- user_nick = self.user_nick
509
- user_participant = await self.get_user_participant()
614
+ if user_participant is None:
615
+ user_participant = await self.get_user_participant()
510
616
  if not user_participant.is_user: # type:ignore
511
617
  self.log.warning("is_user flag not set participant on user_participant")
512
618
  user_participant.is_user = True # type:ignore
@@ -537,7 +643,7 @@ class LegacyMUC(
537
643
  maxstanzas=maxstanzas,
538
644
  since=since,
539
645
  )
540
- (await self.__get_subject_setter_participant()).set_room_subject(
646
+ self.__get_subject_setter_participant().set_room_subject(
541
647
  self._subject if self.HAS_SUBJECT else (self.description or self.name),
542
648
  user_full_jid,
543
649
  self.subject_date,
@@ -558,13 +664,13 @@ class LegacyMUC(
558
664
  self.__store_participant(p)
559
665
  return p
560
666
 
561
- def __store_participant(self, p: "LegacyParticipantType"):
667
+ def __store_participant(self, p: "LegacyParticipantType") -> None:
562
668
  # we don't want to update the participant list when we're filling history
563
669
  if not self.KEEP_BACKFILLED_PARTICIPANTS and self.get_lock("fill history"):
564
670
  return
565
- self._participants_by_nicknames[p.nickname] = p # type:ignore
566
- if p.contact:
567
- self._participants_by_contacts[p.contact] = p
671
+ assert self.pk is not None
672
+ p.pk = self.__participants_store.add(self.pk, p.nickname)
673
+ self.__participants_store.update(p)
568
674
 
569
675
  async def get_participant(
570
676
  self,
@@ -591,25 +697,30 @@ class LegacyMUC(
591
697
  construction (optional)
592
698
  :return:
593
699
  """
594
- if fill_first:
595
- 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()
700
+ if fill_first and not self._participants_filled:
701
+ async for _ in self.get_participants():
702
+ pass
703
+ if self.pk is not None:
704
+ with self.xmpp.store.session():
705
+ stored = self.__participants_store.get_by_nickname(
706
+ self.pk, nickname
707
+ ) or self.__participants_store.get_by_resource(self.pk, nickname)
708
+ if stored is not None:
709
+ return self.Participant.from_store(self.session, stored)
710
+
711
+ if raise_if_not_found:
712
+ raise XMPPError("item-not-found")
713
+ p = self.Participant(self, nickname, **kwargs)
714
+ if store and not self._updating_info:
715
+ self.__store_participant(p)
716
+ if (
717
+ not self.get_lock("fill participants")
718
+ and not self.get_lock("fill history")
719
+ and self._participants_filled
720
+ and not p.is_user
721
+ and not p.is_system
722
+ ):
723
+ p.send_affiliation_change()
613
724
  return p
614
725
 
615
726
  def get_system_participant(self) -> "LegacyParticipantType":
@@ -638,25 +749,44 @@ class LegacyMUC(
638
749
  :return:
639
750
  """
640
751
  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)
752
+
753
+ if self.pk is not None:
754
+ assert c.contact_pk is not None
755
+ with self.__store.session():
756
+ stored = self.__participants_store.get_by_contact(self.pk, c.contact_pk)
757
+ if stored is not None:
758
+ return self.Participant.from_store(
759
+ self.session, stored, muc=self, contact=c
760
+ )
761
+
762
+ nickname = c.name or _unescape_node(c.jid_username)
763
+
764
+ if self.pk is None:
765
+ nick_available = True
766
+ else:
767
+ nick_available = self.__store.nickname_is_available(self.pk, nickname)
768
+
769
+ if not nick_available:
770
+ self.log.debug("Nickname conflict")
771
+ nickname = f"{nickname} ({c.jid_username})"
772
+ p = self.Participant(self, nickname, **kwargs)
773
+ p.contact = c
774
+
775
+ if self._updating_info:
776
+ return p
777
+
778
+ self.__store_participant(p)
779
+ # FIXME: this is not great but given the current design,
780
+ # during participants fill and history backfill we do not
781
+ # want to send presence, because we might :update affiliation
782
+ # and role afterwards.
783
+ # We need a refactor of the MUC class… later™
784
+ if (
785
+ self._participants_filled
786
+ and not self.get_lock("fill participants")
787
+ and not self.get_lock("fill history")
788
+ ):
789
+ p.send_last_presence(force=True, no_cache_online=True)
660
790
  return p
661
791
 
662
792
  async def get_participant_by_legacy_id(
@@ -668,16 +798,6 @@ class LegacyMUC(
668
798
  return await self.get_user_participant(**kwargs)
669
799
  return await self.get_participant_by_contact(c, **kwargs)
670
800
 
671
- async def get_participants(self):
672
- """
673
- Get all known participants of the group, ensure :meth:`.LegacyMUC.fill_participants`
674
- has been awaited once before. Plugins should not use that, internal
675
- slidge use only.
676
- :return:
677
- """
678
- await self.__fill_participants()
679
- return list(self._participants_by_nicknames.values())
680
-
681
801
  def remove_participant(self, p: "LegacyParticipantType", kick=False, ban=False):
682
802
  """
683
803
  Call this when a participant leaves the room
@@ -688,19 +808,7 @@ class LegacyMUC(
688
808
  """
689
809
  if kick and ban:
690
810
  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)
811
+ self.__participants_store.delete(p.pk)
704
812
  if kick:
705
813
  codes = {307}
706
814
  elif ban:
@@ -713,14 +821,15 @@ class LegacyMUC(
713
821
  p._send(presence)
714
822
 
715
823
  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
824
+ assert self.pk is not None
825
+ with self.xmpp.store.session():
826
+ stored = self.__participants_store.get_by_nickname(self.pk, old_nickname)
827
+ if stored is None:
828
+ self.log.debug("Tried to rename a participant that we didn't know")
829
+ return
830
+ p = self.Participant.from_store(self.session, stored)
831
+ if p.nickname == old_nickname:
832
+ p.nickname = new_nickname
724
833
 
725
834
  async def __old_school_history(
726
835
  self,
@@ -849,7 +958,7 @@ class LegacyMUC(
849
958
 
850
959
  :param r: The resource to kick
851
960
  """
852
- pto = self.user.jid
961
+ pto = self.user_jid
853
962
  pto.resource = r
854
963
  p = self.xmpp.make_presence(
855
964
  pfrom=(await self.get_user_participant()).jid, pto=pto
@@ -882,7 +991,7 @@ class LegacyMUC(
882
991
  item = Item()
883
992
  item["id"] = self.jid
884
993
 
885
- iq = Iq(stype="get", sfrom=self.user.jid, sto=self.user.jid)
994
+ iq = Iq(stype="get", sfrom=self.user_jid, sto=self.user_jid)
886
995
  iq["pubsub"]["items"]["node"] = self.xmpp["xep_0402"].stanza.NS
887
996
  iq["pubsub"]["items"].append(item)
888
997
 
@@ -912,7 +1021,7 @@ class LegacyMUC(
912
1021
  item["conference"]["autojoin"] = auto_join
913
1022
 
914
1023
  item["conference"]["nick"] = self.user_nick
915
- iq = Iq(stype="set", sfrom=self.user.jid, sto=self.user.jid)
1024
+ iq = Iq(stype="set", sfrom=self.user_jid, sto=self.user_jid)
916
1025
  iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0402"].stanza.NS
917
1026
  iq["pubsub"]["publish"].append(item)
918
1027
 
@@ -974,10 +1083,9 @@ class LegacyMUC(
974
1083
  ):
975
1084
  """
976
1085
  Triggered when the user requests changing the affiliation of a contact
977
- for this group,
1086
+ for this group.
978
1087
 
979
- Examples: promotion them to moderator, kick (affiliation=none),
980
- ban (affiliation=outcast).
1088
+ Examples: promotion them to moderator, ban (affiliation=outcast).
981
1089
 
982
1090
  :param contact: The contact whose affiliation change is requested
983
1091
  :param affiliation: The new affiliation
@@ -986,6 +1094,16 @@ class LegacyMUC(
986
1094
  """
987
1095
  raise NotImplementedError
988
1096
 
1097
+ async def on_kick(self, contact: "LegacyContact", reason: Optional[str]):
1098
+ """
1099
+ Triggered when the user requests changing the role of a contact
1100
+ to "none" for this group. Action commonly known as "kick".
1101
+
1102
+ :param contact: Contact to be kicked
1103
+ :param reason: A reason for this kick
1104
+ """
1105
+ raise NotImplementedError
1106
+
989
1107
  async def on_set_config(
990
1108
  self,
991
1109
  name: Optional[str],
@@ -1015,30 +1133,39 @@ class LegacyMUC(
1015
1133
  raise NotImplementedError
1016
1134
 
1017
1135
  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]))
1136
+ with self.__store.session():
1137
+ await self.__fill_participants()
1138
+ assert self.pk is not None
1139
+ participants = {
1140
+ p.nickname: p for p in self.__participants_store.get_all(self.pk)
1141
+ }
1142
+
1143
+ if len(participants) == 0:
1144
+ return []
1145
+
1146
+ result = []
1147
+ for match in re.finditer(
1148
+ "|".join(
1149
+ sorted(
1150
+ [re.escape(nick) for nick in participants.keys()],
1151
+ key=lambda nick: len(nick),
1152
+ reverse=True,
1153
+ )
1154
+ ),
1155
+ text,
1156
+ ):
1157
+ span = match.span()
1158
+ nick = match.group()
1159
+ if span[0] != 0 and text[span[0] - 1] not in _WHITESPACE_OR_PUNCTUATION:
1160
+ continue
1161
+ if span[1] == len(text) or text[span[1]] in _WHITESPACE_OR_PUNCTUATION:
1162
+ participant = self.Participant.from_store(
1163
+ self.session, participants[nick]
1164
+ )
1165
+ if contact := participant.contact:
1166
+ result.append(
1167
+ Mention(contact=contact, start=span[0], end=span[1])
1168
+ )
1042
1169
  return result
1043
1170
 
1044
1171
  async def on_set_subject(self, subject: str) -> None:
@@ -1052,6 +1179,37 @@ class LegacyMUC(
1052
1179
  """
1053
1180
  raise NotImplementedError
1054
1181
 
1182
+ @classmethod
1183
+ def from_store(cls, session, stored: Room, *args, **kwargs) -> Self:
1184
+ muc = cls(
1185
+ session,
1186
+ cls.xmpp.LEGACY_ROOM_ID_TYPE(stored.legacy_id),
1187
+ stored.jid,
1188
+ *args, # type: ignore
1189
+ **kwargs, # type: ignore
1190
+ )
1191
+ muc.pk = stored.id
1192
+ muc.type = stored.muc_type # type: ignore
1193
+ muc.user_nick = stored.user_nick
1194
+ if stored.name:
1195
+ muc.DISCO_NAME = stored.name
1196
+ if stored.description:
1197
+ muc._description = stored.description
1198
+ if (data := stored.extra_attributes) is not None:
1199
+ muc.deserialize_extra_attributes(data)
1200
+ muc._subject = stored.subject or ""
1201
+ if stored.subject_date is not None:
1202
+ muc._subject_date = stored.subject_date.replace(tzinfo=timezone.utc)
1203
+ muc._participants_filled = stored.participants_filled
1204
+ muc._n_participants = stored.n_participants
1205
+ muc._history_filled = stored.history_filled
1206
+ if stored.user_resources is not None:
1207
+ muc._user_resources = set(json.loads(stored.user_resources))
1208
+ muc._subject_setter = stored.subject_setter
1209
+ muc.archive = MessageArchive(muc.pk, session.xmpp.store.mam)
1210
+ muc._set_avatar_from_store(stored)
1211
+ return muc
1212
+
1055
1213
 
1056
1214
  def set_origin_id(msg: Message, origin_id: str):
1057
1215
  sub = ET.Element("{urn:xmpp:sid:0}origin-id")