slidge 0.1.3__py3-none-any.whl → 0.2.0a1__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 (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")