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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +107 -40
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -804
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.3.dist-info/RECORD +0 -96
  100. slidge-0.1.3.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/WHEEL +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,47 +125,75 @@ 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}")
130
134
 
131
135
  self.legacy_id = legacy_id
132
136
  self.jid = jid
133
137
 
134
- self.user_resources = set[str]()
138
+ self._user_resources = set[str]()
135
139
 
136
140
  self.Participant = LegacyParticipant.get_self_or_unique_subclass()
137
141
 
138
- self.xmpp.add_event_handler(
139
- "presence_unavailable", self._on_presence_unavailable
140
- )
141
-
142
142
  self._subject = ""
143
- self.subject_setter: Union[str, "LegacyContact", "LegacyParticipant"] = (
144
- self.get_system_participant()
145
- )
143
+ self._subject_setter: Optional[str] = None
146
144
 
147
- self.archive: MessageArchive = MessageArchive(str(self.jid), self.user)
145
+ self.pk: Optional[int] = None
148
146
  self._user_nick: Optional[str] = None
149
147
 
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
148
+ self._participants_filled = False
149
+ self._history_filled = False
156
150
  self._description = ""
151
+ self._subject_date: Optional[datetime] = None
152
+
153
+ self.__participants_store = self.xmpp.store.participants
154
+ self.__store = self.xmpp.store.rooms
155
+
156
+ self._n_participants: Optional[int] = None
157
+
158
+ self.log = logging.getLogger(self.jid.bare)
159
+ self._set_logger_name()
157
160
  super().__init__()
158
161
 
162
+ @property
163
+ def n_participants(self):
164
+ return self._n_participants
165
+
166
+ @n_participants.setter
167
+ def n_participants(self, n_participants: Optional[int]):
168
+ if self._n_participants == n_participants:
169
+ return
170
+ self._n_participants = n_participants
171
+ if self._updating_info:
172
+ return
173
+ assert self.pk is not None
174
+ self.__store.update_n_participants(self.pk, n_participants)
175
+
176
+ @property
177
+ def user_jid(self):
178
+ return self.session.user_jid
179
+
180
+ def _set_logger_name(self):
181
+ self.log = logging.getLogger(f"{self.user_jid}:muc:{self}")
182
+
159
183
  def __repr__(self):
160
- return f"<MUC {self.legacy_id}/{self.jid}/{self.name}>"
184
+ return f"<MUC #{self.pk} '{self.name}' ({self.legacy_id} - {self.jid.local})'>"
185
+
186
+ @property
187
+ def subject_date(self) -> Optional[datetime]:
188
+ return self._subject_date
189
+
190
+ @subject_date.setter
191
+ def subject_date(self, when: Optional[datetime]) -> None:
192
+ self._subject_date = when
193
+ if self._updating_info:
194
+ return
195
+ assert self.pk is not None
196
+ self.__store.update_subject_date(self.pk, when)
161
197
 
162
198
  def __send_configuration_change(self, codes):
163
199
  part = self.get_system_participant()
@@ -165,50 +201,92 @@ class LegacyMUC(
165
201
 
166
202
  @property
167
203
  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
- )
204
+ return self._user_nick or self.session.bookmarks.user_nick or self.user_jid.node
173
205
 
174
206
  @user_nick.setter
175
207
  def user_nick(self, nick: str):
176
208
  self._user_nick = nick
209
+ if not self._updating_info:
210
+ self.__store.update_user_nick(self.pk, nick)
211
+
212
+ def add_user_resource(self, resource: str) -> None:
213
+ self._user_resources.add(resource)
214
+ assert self.pk is not None
215
+ self.__store.set_resource(self.pk, self._user_resources)
216
+
217
+ def get_user_resources(self) -> set[str]:
218
+ return self._user_resources
219
+
220
+ def remove_user_resource(self, resource: str) -> None:
221
+ self._user_resources.remove(resource)
222
+ assert self.pk is not None
223
+ self.__store.set_resource(self.pk, self._user_resources)
177
224
 
178
225
  async def __fill_participants(self):
226
+ if self._participants_filled:
227
+ return
228
+ assert self.pk is not None
179
229
  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
230
+ self._participants_filled = True
231
+ async for p in self.fill_participants():
232
+ self.__participants_store.update(p)
233
+ self.__store.set_participants_filled(self.pk)
234
+
235
+ async def get_participants(self) -> AsyncIterator[LegacyParticipant]:
236
+ assert self.pk is not None
237
+ if self._participants_filled:
238
+ for db_participant in self.xmpp.store.participants.get_all(
239
+ self.pk, user_included=True
240
+ ):
241
+ participant = self.Participant.from_store(
242
+ self.session, db_participant, muc=self
243
+ )
244
+ yield participant
245
+ return
246
+
247
+ async with self.lock("fill participants"):
248
+ self._participants_filled = True
249
+ # We only fill the participants list if/when the MUC is first
250
+ # joined by an XMPP client. But we may have instantiated
251
+ resources = set[str]()
252
+ for db_participant in self.xmpp.store.participants.get_all(
253
+ self.pk, user_included=True
254
+ ):
255
+ participant = self.Participant.from_store(
256
+ self.session, db_participant, muc=self
257
+ )
258
+ resources.add(participant.jid.resource)
259
+ yield participant
260
+ async for p in self.fill_participants():
261
+ if p.jid.resource not in resources:
262
+ yield p
263
+ self.__store.set_participants_filled(self.pk)
264
+ return
187
265
 
188
266
  async def __fill_history(self):
189
267
  async with self.lock("fill history"):
190
- if self.__history_filled:
268
+ if self._history_filled:
191
269
  log.debug("History has already been fetched %s", self)
192
270
  return
193
271
  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
272
  try:
208
- await self.backfill(legacy_id, oldest_date)
273
+ before, after = self.archive.get_hole_bounds()
274
+ if before is not None:
275
+ before = before._replace(
276
+ id=self.xmpp.LEGACY_MSG_ID_TYPE(before.id) # type:ignore
277
+ )
278
+ if after is not None:
279
+ after = after._replace(
280
+ id=self.xmpp.LEGACY_MSG_ID_TYPE(after.id) # type:ignore
281
+ )
282
+ await self.backfill(before, after)
209
283
  except NotImplementedError:
210
284
  return
211
- self.__history_filled = True
285
+ except Exception as e:
286
+ log.exception("Could not backfill: %s", e)
287
+ assert self.pk is not None
288
+ self.__store.set_history_filled(self.pk, True)
289
+ self._history_filled = True
212
290
 
213
291
  @property
214
292
  def name(self):
@@ -219,7 +297,12 @@ class LegacyMUC(
219
297
  if self.DISCO_NAME == n:
220
298
  return
221
299
  self.DISCO_NAME = n
300
+ self._set_logger_name()
222
301
  self.__send_configuration_change((104,))
302
+ if self._updating_info:
303
+ return
304
+ assert self.pk is not None
305
+ self.__store.update_name(self.pk, n)
223
306
 
224
307
  @property
225
308
  def description(self):
@@ -231,21 +314,25 @@ class LegacyMUC(
231
314
  return
232
315
  self._description = d
233
316
  self.__send_configuration_change((104,))
317
+ if self._updating_info:
318
+ return
319
+ assert self.pk is not None
320
+ self.__store.update_description(self.pk, d)
234
321
 
235
- def _on_presence_unavailable(self, p: Presence):
322
+ def on_presence_unavailable(self, p: Presence):
236
323
  pto = p.get_to()
237
324
  if pto.bare != self.jid.bare:
238
325
  return
239
326
 
240
327
  pfrom = p.get_from()
241
- if pfrom.bare != self.user.bare_jid:
328
+ if pfrom.bare != self.user_jid.bare:
242
329
  return
243
- if (resource := pfrom.resource) in (resources := self.user_resources):
330
+ if (resource := pfrom.resource) in self._user_resources:
244
331
  if pto.resource != self.user_nick:
245
332
  self.log.debug(
246
333
  "Received 'leave group' request but with wrong nickname. %s", p
247
334
  )
248
- resources.remove(resource)
335
+ self.remove_user_resource(resource)
249
336
  else:
250
337
  self.log.debug(
251
338
  "Received 'leave group' request but resource was not listed. %s", p
@@ -270,27 +357,35 @@ class LegacyMUC(
270
357
 
271
358
  async def backfill(
272
359
  self,
273
- oldest_message_id: Optional[LegacyMessageType] = None,
274
- oldest_message_date: Optional[datetime] = None,
360
+ after: Optional[HoleBound] = None,
361
+ before: Optional[HoleBound] = None,
275
362
  ):
276
363
  """
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
364
+ Override this if the legacy network provide server-side group archives.
365
+
366
+ In it, send history messages using ``self.get_participant(xxx).send_xxxx``,
367
+ with the ``archive_only=True`` kwarg. This is only called once per slidge
368
+ run for a given group.
369
+
370
+ :param after: Fetch messages after this one. If ``None``, it's up to you
371
+ to decide how far you want to go in the archive. If it's not ``None``,
372
+ it means slidge has some messages in this archive and you should really try
373
+ to complete it to avoid "holes" in the history of this group.
374
+ :param before: Fetch messages before this one. If ``None``, fetch all messages
375
+ up to the most recent one
285
376
  """
286
377
  raise NotImplementedError
287
378
 
288
- async def fill_participants(self):
379
+ async def fill_participants(self) -> AsyncIterator[LegacyParticipant]:
289
380
  """
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.
381
+ This method should yield the list of all members of this group.
382
+
383
+ Typically, use ``participant = self.get_participant()``, self.get_participant_by_contact(),
384
+ of self.get_user_participant(), and update their affiliation, hats, etc.
385
+ before yielding them.
292
386
  """
293
- raise NotImplementedError
387
+ return
388
+ yield
294
389
 
295
390
  @property
296
391
  def subject(self):
@@ -300,34 +395,44 @@ class LegacyMUC(
300
395
  def subject(self, s: str):
301
396
  if s == self._subject:
302
397
  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
- )
398
+ self.__get_subject_setter_participant().set_room_subject(
399
+ s, None, self.subject_date, False
309
400
  )
401
+
310
402
  self._subject = s
403
+ if self._updating_info:
404
+ return
405
+ assert self.pk is not None
406
+ self.__store.update_subject(self.pk, s)
311
407
 
312
408
  @property
313
409
  def is_anonymous(self):
314
410
  return self.type == MucType.CHANNEL
315
411
 
316
- async def __get_subject_setter_participant(self):
317
- from slidge.contact import LegacyContact
412
+ @property
413
+ def subject_setter(self) -> Optional[str]:
414
+ return self._subject_setter
318
415
 
319
- from .participant import LegacyParticipant
416
+ @subject_setter.setter
417
+ def subject_setter(self, subject_setter: SubjectSetterType) -> None:
418
+ if isinstance(subject_setter, LegacyContact):
419
+ subject_setter = subject_setter.name
420
+ elif isinstance(subject_setter, LegacyParticipant):
421
+ subject_setter = subject_setter.nickname
320
422
 
321
- who = self.subject_setter
423
+ if subject_setter == self._subject_setter:
424
+ return
425
+ assert isinstance(subject_setter, str)
426
+ self._subject_setter = subject_setter
427
+ if self._updating_info:
428
+ return
429
+ assert self.pk is not None
430
+ self.__store.update_subject_setter(self.pk, subject_setter)
322
431
 
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:
432
+ def __get_subject_setter_participant(self) -> LegacyParticipant:
433
+ if self._subject_setter is None:
330
434
  return self.get_system_participant()
435
+ return self.Participant(self, self._subject_setter)
331
436
 
332
437
  def features(self):
333
438
  features = [
@@ -341,6 +446,7 @@ class LegacyMUC(
341
446
  "vcard-temp",
342
447
  "urn:xmpp:ping",
343
448
  "urn:xmpp:occupant-id:0",
449
+ "jabber:iq:register",
344
450
  self.xmpp.plugin["xep_0425"].stanza.NS,
345
451
  ]
346
452
  if self.type == MucType.GROUP:
@@ -364,10 +470,11 @@ class LegacyMUC(
364
470
  form.add_field("muc#maxhistoryfetch", value=str(self.max_history_fetch))
365
471
  form.add_field("muc#roominfo_subjectmod", "boolean", value=False)
366
472
 
367
- if self._ALL_INFO_FILLED_ON_STARTUP or self.__participants_filled:
368
- n: Optional[int] = len(await self.get_participants())
473
+ if self._ALL_INFO_FILLED_ON_STARTUP or self._participants_filled:
474
+ assert self.pk is not None
475
+ n: Optional[int] = self.__participants_store.get_count(self.pk)
369
476
  else:
370
- n = self.n_participants
477
+ n = self._n_participants
371
478
  if n is not None:
372
479
  form.add_field("muc#roominfo_occupants", value=str(n))
373
480
 
@@ -415,8 +522,8 @@ class LegacyMUC(
415
522
  presence.send()
416
523
 
417
524
  def user_full_jids(self):
418
- for r in self.user_resources:
419
- j = copy(self.user.jid)
525
+ for r in self._user_resources:
526
+ j = copy(self.user_jid)
420
527
  j.resource = r
421
528
  yield j
422
529
 
@@ -427,9 +534,9 @@ class LegacyMUC(
427
534
  return user_muc_jid
428
535
 
429
536
  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
- )
537
+ return self.xmpp.store.sent.get_group_xmpp_id(
538
+ self.session.user_pk, str(legacy_id)
539
+ ) or self.session.legacy_to_xmpp_msg_id(legacy_id)
433
540
 
434
541
  async def echo(
435
542
  self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
@@ -458,7 +565,20 @@ class LegacyMUC(
458
565
 
459
566
  msg.send()
460
567
 
568
+ def _get_cached_avatar_id(self):
569
+ if self.pk is None:
570
+ return None
571
+ return self.xmpp.store.rooms.get_avatar_legacy_id(self.pk)
572
+
461
573
  def _post_avatar_update(self) -> None:
574
+ if self.pk is None:
575
+ return
576
+ assert self.pk is not None
577
+ self.xmpp.store.rooms.set_avatar(
578
+ self.pk,
579
+ self._avatar_pk,
580
+ None if self.avatar_id is None else str(self.avatar_id),
581
+ )
462
582
  self.__send_configuration_change((104,))
463
583
  self._send_room_presence()
464
584
 
@@ -475,15 +595,17 @@ class LegacyMUC(
475
595
  p["vcard_temp_update"]["photo"] = ""
476
596
  p.send()
477
597
 
598
+ @timeit
599
+ @with_session
478
600
  async def join(self, join_presence: Presence):
479
601
  user_full_jid = join_presence.get_from()
480
602
  requested_nickname = join_presence.get_to().resource
481
603
  client_resource = user_full_jid.resource
482
604
 
483
- if client_resource in self.user_resources:
605
+ if client_resource in self._user_resources:
484
606
  self.log.debug("Received join from a resource that is already joined.")
485
607
 
486
- self.user_resources.add(client_resource)
608
+ self.add_user_resource(client_resource)
487
609
 
488
610
  if not requested_nickname or not client_resource:
489
611
  raise XMPPError("jid-malformed", by=self.jid)
@@ -491,22 +613,21 @@ class LegacyMUC(
491
613
  self.log.debug(
492
614
  "Resource %s of %s wants to join room %s with nickname %s",
493
615
  client_resource,
494
- self.user,
616
+ self.user_jid,
495
617
  self.legacy_id,
496
618
  requested_nickname,
497
619
  )
498
620
 
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
621
+ user_nick = self.user_nick
622
+ user_participant = None
623
+ async for participant in self.get_participants():
624
+ if participant.is_user:
625
+ user_participant = participant
505
626
  continue
506
627
  participant.send_initial_presence(full_jid=user_full_jid)
507
628
 
508
- user_nick = self.user_nick
509
- user_participant = await self.get_user_participant()
629
+ if user_participant is None:
630
+ user_participant = await self.get_user_participant()
510
631
  if not user_participant.is_user: # type:ignore
511
632
  self.log.warning("is_user flag not set participant on user_participant")
512
633
  user_participant.is_user = True # type:ignore
@@ -537,7 +658,7 @@ class LegacyMUC(
537
658
  maxstanzas=maxstanzas,
538
659
  since=since,
539
660
  )
540
- (await self.__get_subject_setter_participant()).set_room_subject(
661
+ self.__get_subject_setter_participant().set_room_subject(
541
662
  self._subject if self.HAS_SUBJECT else (self.description or self.name),
542
663
  user_full_jid,
543
664
  self.subject_date,
@@ -558,13 +679,13 @@ class LegacyMUC(
558
679
  self.__store_participant(p)
559
680
  return p
560
681
 
561
- def __store_participant(self, p: "LegacyParticipantType"):
682
+ def __store_participant(self, p: "LegacyParticipantType") -> None:
562
683
  # we don't want to update the participant list when we're filling history
563
684
  if not self.KEEP_BACKFILLED_PARTICIPANTS and self.get_lock("fill history"):
564
685
  return
565
- self._participants_by_nicknames[p.nickname] = p # type:ignore
566
- if p.contact:
567
- self._participants_by_contacts[p.contact] = p
686
+ assert self.pk is not None
687
+ p.pk = self.__participants_store.add(self.pk, p.nickname)
688
+ self.__participants_store.update(p)
568
689
 
569
690
  async def get_participant(
570
691
  self,
@@ -591,25 +712,30 @@ class LegacyMUC(
591
712
  construction (optional)
592
713
  :return:
593
714
  """
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()
715
+ if fill_first and not self._participants_filled:
716
+ async for _ in self.get_participants():
717
+ pass
718
+ if self.pk is not None:
719
+ with self.xmpp.store.session():
720
+ stored = self.__participants_store.get_by_nickname(
721
+ self.pk, nickname
722
+ ) or self.__participants_store.get_by_resource(self.pk, nickname)
723
+ if stored is not None:
724
+ return self.Participant.from_store(self.session, stored)
725
+
726
+ if raise_if_not_found:
727
+ raise XMPPError("item-not-found")
728
+ p = self.Participant(self, nickname, **kwargs)
729
+ if store and not self._updating_info:
730
+ self.__store_participant(p)
731
+ if (
732
+ not self.get_lock("fill participants")
733
+ and not self.get_lock("fill history")
734
+ and self._participants_filled
735
+ and not p.is_user
736
+ and not p.is_system
737
+ ):
738
+ p.send_affiliation_change()
613
739
  return p
614
740
 
615
741
  def get_system_participant(self) -> "LegacyParticipantType":
@@ -638,25 +764,45 @@ class LegacyMUC(
638
764
  :return:
639
765
  """
640
766
  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)
767
+
768
+ if self.pk is not None:
769
+ c._LegacyContact__ensure_pk() # type: ignore
770
+ assert c.contact_pk is not None
771
+ with self.__store.session():
772
+ stored = self.__participants_store.get_by_contact(self.pk, c.contact_pk)
773
+ if stored is not None:
774
+ return self.Participant.from_store(
775
+ self.session, stored, muc=self, contact=c
776
+ )
777
+
778
+ nickname = c.name or _unescape_node(c.jid_username)
779
+
780
+ if self.pk is None:
781
+ nick_available = True
782
+ else:
783
+ nick_available = self.__store.nickname_is_available(self.pk, nickname)
784
+
785
+ if not nick_available:
786
+ self.log.debug("Nickname conflict")
787
+ nickname = f"{nickname} ({c.jid_username})"
788
+ p = self.Participant(self, nickname, **kwargs)
789
+ p.contact = c
790
+
791
+ if self._updating_info:
792
+ return p
793
+
794
+ self.__store_participant(p)
795
+ # FIXME: this is not great but given the current design,
796
+ # during participants fill and history backfill we do not
797
+ # want to send presence, because we might :update affiliation
798
+ # and role afterwards.
799
+ # We need a refactor of the MUC class… later™
800
+ if (
801
+ self._participants_filled
802
+ and not self.get_lock("fill participants")
803
+ and not self.get_lock("fill history")
804
+ ):
805
+ p.send_last_presence(force=True, no_cache_online=True)
660
806
  return p
661
807
 
662
808
  async def get_participant_by_legacy_id(
@@ -668,39 +814,24 @@ class LegacyMUC(
668
814
  return await self.get_user_participant(**kwargs)
669
815
  return await self.get_participant_by_contact(c, **kwargs)
670
816
 
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
- def remove_participant(self, p: "LegacyParticipantType", kick=False, ban=False):
817
+ def remove_participant(
818
+ self,
819
+ p: "LegacyParticipantType",
820
+ kick=False,
821
+ ban=False,
822
+ reason: str | None = None,
823
+ ):
682
824
  """
683
825
  Call this when a participant leaves the room
684
826
 
685
827
  :param p: The participant
686
828
  :param kick: Whether the participant left because they were kicked
687
829
  :param ban: Whether the participant left because they were banned
830
+ :param reason: Optionally, a reason why the participant was removed.
688
831
  """
689
832
  if kick and ban:
690
833
  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)
834
+ self.__participants_store.delete(p.pk)
704
835
  if kick:
705
836
  codes = {307}
706
837
  elif ban:
@@ -710,17 +841,20 @@ class LegacyMUC(
710
841
  presence = p._make_presence(ptype="unavailable", status_codes=codes)
711
842
  p._affiliation = "outcast" if ban else "none"
712
843
  p._role = "none"
844
+ if reason:
845
+ presence["muc"].set_item_attr("reason", reason)
713
846
  p._send(presence)
714
847
 
715
848
  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
849
+ assert self.pk is not None
850
+ with self.xmpp.store.session():
851
+ stored = self.__participants_store.get_by_nickname(self.pk, old_nickname)
852
+ if stored is None:
853
+ self.log.debug("Tried to rename a participant that we didn't know")
854
+ return
855
+ p = self.Participant.from_store(self.session, stored)
856
+ if p.nickname == old_nickname:
857
+ p.nickname = new_nickname
724
858
 
725
859
  async def __old_school_history(
726
860
  self,
@@ -849,7 +983,7 @@ class LegacyMUC(
849
983
 
850
984
  :param r: The resource to kick
851
985
  """
852
- pto = self.user.jid
986
+ pto = self.user_jid
853
987
  pto.resource = r
854
988
  p = self.xmpp.make_presence(
855
989
  pfrom=(await self.get_user_participant()).jid, pto=pto
@@ -882,7 +1016,7 @@ class LegacyMUC(
882
1016
  item = Item()
883
1017
  item["id"] = self.jid
884
1018
 
885
- iq = Iq(stype="get", sfrom=self.user.jid, sto=self.user.jid)
1019
+ iq = Iq(stype="get", sfrom=self.user_jid, sto=self.user_jid)
886
1020
  iq["pubsub"]["items"]["node"] = self.xmpp["xep_0402"].stanza.NS
887
1021
  iq["pubsub"]["items"].append(item)
888
1022
 
@@ -912,7 +1046,7 @@ class LegacyMUC(
912
1046
  item["conference"]["autojoin"] = auto_join
913
1047
 
914
1048
  item["conference"]["nick"] = self.user_nick
915
- iq = Iq(stype="set", sfrom=self.user.jid, sto=self.user.jid)
1049
+ iq = Iq(stype="set", sfrom=self.user_jid, sto=self.user_jid)
916
1050
  iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0402"].stanza.NS
917
1051
  iq["pubsub"]["publish"].append(item)
918
1052
 
@@ -974,10 +1108,9 @@ class LegacyMUC(
974
1108
  ):
975
1109
  """
976
1110
  Triggered when the user requests changing the affiliation of a contact
977
- for this group,
1111
+ for this group.
978
1112
 
979
- Examples: promotion them to moderator, kick (affiliation=none),
980
- ban (affiliation=outcast).
1113
+ Examples: promotion them to moderator, ban (affiliation=outcast).
981
1114
 
982
1115
  :param contact: The contact whose affiliation change is requested
983
1116
  :param affiliation: The new affiliation
@@ -986,6 +1119,16 @@ class LegacyMUC(
986
1119
  """
987
1120
  raise NotImplementedError
988
1121
 
1122
+ async def on_kick(self, contact: "LegacyContact", reason: Optional[str]):
1123
+ """
1124
+ Triggered when the user requests changing the role of a contact
1125
+ to "none" for this group. Action commonly known as "kick".
1126
+
1127
+ :param contact: Contact to be kicked
1128
+ :param reason: A reason for this kick
1129
+ """
1130
+ raise NotImplementedError
1131
+
989
1132
  async def on_set_config(
990
1133
  self,
991
1134
  name: Optional[str],
@@ -1015,30 +1158,39 @@ class LegacyMUC(
1015
1158
  raise NotImplementedError
1016
1159
 
1017
1160
  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]))
1161
+ with self.__store.session():
1162
+ await self.__fill_participants()
1163
+ assert self.pk is not None
1164
+ participants = {
1165
+ p.nickname: p for p in self.__participants_store.get_all(self.pk)
1166
+ }
1167
+
1168
+ if len(participants) == 0:
1169
+ return []
1170
+
1171
+ result = []
1172
+ for match in re.finditer(
1173
+ "|".join(
1174
+ sorted(
1175
+ [re.escape(nick) for nick in participants.keys()],
1176
+ key=lambda nick: len(nick),
1177
+ reverse=True,
1178
+ )
1179
+ ),
1180
+ text,
1181
+ ):
1182
+ span = match.span()
1183
+ nick = match.group()
1184
+ if span[0] != 0 and text[span[0] - 1] not in _WHITESPACE_OR_PUNCTUATION:
1185
+ continue
1186
+ if span[1] == len(text) or text[span[1]] in _WHITESPACE_OR_PUNCTUATION:
1187
+ participant = self.Participant.from_store(
1188
+ self.session, participants[nick]
1189
+ )
1190
+ if contact := participant.contact:
1191
+ result.append(
1192
+ Mention(contact=contact, start=span[0], end=span[1])
1193
+ )
1042
1194
  return result
1043
1195
 
1044
1196
  async def on_set_subject(self, subject: str) -> None:
@@ -1052,6 +1204,43 @@ class LegacyMUC(
1052
1204
  """
1053
1205
  raise NotImplementedError
1054
1206
 
1207
+ @classmethod
1208
+ def from_store(cls, session, stored: Room, *args, **kwargs) -> Self:
1209
+ muc = cls(
1210
+ session,
1211
+ cls.xmpp.LEGACY_ROOM_ID_TYPE(stored.legacy_id),
1212
+ stored.jid,
1213
+ *args, # type: ignore
1214
+ **kwargs, # type: ignore
1215
+ )
1216
+ muc.pk = stored.id
1217
+ muc.type = stored.muc_type # type: ignore
1218
+ muc._user_nick = stored.user_nick
1219
+ if stored.name:
1220
+ muc.DISCO_NAME = stored.name
1221
+ if stored.description:
1222
+ muc._description = stored.description
1223
+ if (data := stored.extra_attributes) is not None:
1224
+ muc.deserialize_extra_attributes(data)
1225
+ muc._subject = stored.subject or ""
1226
+ if stored.subject_date is not None:
1227
+ muc._subject_date = stored.subject_date.replace(tzinfo=timezone.utc)
1228
+ muc._participants_filled = stored.participants_filled
1229
+ muc._n_participants = stored.n_participants
1230
+ muc._history_filled = stored.history_filled
1231
+ if stored.user_resources is not None:
1232
+ muc._user_resources = set(json.loads(stored.user_resources))
1233
+ muc._subject_setter = stored.subject_setter
1234
+ muc.archive = MessageArchive(muc.pk, session.xmpp.store.mam)
1235
+ muc._set_logger_name()
1236
+ muc._AvatarMixin__avatar_unique_id = ( # type:ignore
1237
+ None
1238
+ if stored.avatar_legacy_id is None
1239
+ else session.xmpp.AVATAR_ID_TYPE(stored.avatar_legacy_id)
1240
+ )
1241
+ muc._avatar_pk = stored.avatar_id
1242
+ return muc
1243
+
1055
1244
 
1056
1245
  def set_origin_id(msg: Message, origin_id: str):
1057
1246
  sub = ET.Element("{urn:xmpp:sid:0}origin-id")