slidge 0.2.12__py3-none-any.whl → 0.3.0a0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. slidge/__init__.py +5 -2
  2. slidge/command/adhoc.py +9 -3
  3. slidge/command/admin.py +16 -12
  4. slidge/command/base.py +16 -12
  5. slidge/command/chat_command.py +25 -16
  6. slidge/command/user.py +7 -8
  7. slidge/contact/contact.py +119 -209
  8. slidge/contact/roster.py +106 -105
  9. slidge/core/config.py +2 -43
  10. slidge/core/dispatcher/caps.py +9 -2
  11. slidge/core/dispatcher/disco.py +13 -3
  12. slidge/core/dispatcher/message/__init__.py +1 -1
  13. slidge/core/dispatcher/message/chat_state.py +17 -8
  14. slidge/core/dispatcher/message/marker.py +7 -5
  15. slidge/core/dispatcher/message/message.py +117 -92
  16. slidge/core/dispatcher/muc/__init__.py +1 -1
  17. slidge/core/dispatcher/muc/admin.py +4 -4
  18. slidge/core/dispatcher/muc/mam.py +10 -6
  19. slidge/core/dispatcher/muc/misc.py +4 -2
  20. slidge/core/dispatcher/muc/owner.py +5 -3
  21. slidge/core/dispatcher/muc/ping.py +3 -1
  22. slidge/core/dispatcher/presence.py +21 -15
  23. slidge/core/dispatcher/registration.py +20 -12
  24. slidge/core/dispatcher/search.py +7 -3
  25. slidge/core/dispatcher/session_dispatcher.py +13 -5
  26. slidge/core/dispatcher/util.py +37 -27
  27. slidge/core/dispatcher/vcard.py +7 -4
  28. slidge/core/gateway.py +168 -84
  29. slidge/core/mixins/__init__.py +1 -11
  30. slidge/core/mixins/attachment.py +163 -148
  31. slidge/core/mixins/avatar.py +100 -177
  32. slidge/core/mixins/db.py +50 -2
  33. slidge/core/mixins/message.py +19 -17
  34. slidge/core/mixins/message_maker.py +29 -15
  35. slidge/core/mixins/message_text.py +38 -30
  36. slidge/core/mixins/presence.py +91 -35
  37. slidge/core/pubsub.py +42 -47
  38. slidge/core/session.py +88 -57
  39. slidge/db/alembic/versions/0337c90c0b96_unify_legacy_xmpp_id_mappings.py +183 -0
  40. slidge/db/alembic/versions/4dbd23a3f868_new_avatar_store.py +56 -0
  41. slidge/db/alembic/versions/54ce3cde350c_use_hash_for_avatar_filenames.py +50 -0
  42. slidge/db/alembic/versions/58b98dacf819_refactor.py +118 -0
  43. slidge/db/alembic/versions/75a62b74b239_ditch_hats_table.py +74 -0
  44. slidge/db/avatar.py +150 -119
  45. slidge/db/meta.py +33 -22
  46. slidge/db/models.py +68 -117
  47. slidge/db/store.py +412 -1094
  48. slidge/group/archive.py +61 -54
  49. slidge/group/bookmarks.py +74 -55
  50. slidge/group/participant.py +135 -142
  51. slidge/group/room.py +315 -312
  52. slidge/main.py +28 -18
  53. slidge/migration.py +2 -12
  54. slidge/slixfix/__init__.py +20 -4
  55. slidge/slixfix/delivery_receipt.py +6 -4
  56. slidge/slixfix/link_preview/link_preview.py +1 -1
  57. slidge/slixfix/link_preview/stanza.py +1 -1
  58. slidge/slixfix/roster.py +5 -7
  59. slidge/slixfix/xep_0077/register.py +8 -8
  60. slidge/slixfix/xep_0077/stanza.py +7 -7
  61. slidge/slixfix/xep_0100/gateway.py +12 -13
  62. slidge/slixfix/xep_0153/vcard_avatar.py +1 -1
  63. slidge/slixfix/xep_0292/vcard4.py +1 -1
  64. slidge/util/archive_msg.py +11 -5
  65. slidge/util/conf.py +23 -20
  66. slidge/util/jid_escaping.py +1 -1
  67. slidge/{core/mixins → util}/lock.py +6 -6
  68. slidge/util/test.py +30 -29
  69. slidge/util/types.py +22 -18
  70. slidge/util/util.py +19 -22
  71. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/METADATA +1 -1
  72. slidge-0.3.0a0.dist-info/RECORD +117 -0
  73. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/WHEEL +1 -1
  74. slidge-0.2.12.dist-info/RECORD +0 -112
  75. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/entry_points.txt +0 -0
  76. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
  77. {slidge-0.2.12.dist-info → slidge-0.3.0a0.dist-info}/top_level.txt +0 -0
slidge/group/room.py CHANGED
@@ -5,9 +5,10 @@ import string
5
5
  import warnings
6
6
  from copy import copy
7
7
  from datetime import datetime, timedelta, timezone
8
- from typing import TYPE_CHECKING, AsyncIterator, Generic, Optional, Self, Union
8
+ from typing import TYPE_CHECKING, AsyncIterator, Generic, Optional, Type, Union
9
9
  from uuid import uuid4
10
10
 
11
+ import sqlalchemy as sa
11
12
  from slixmpp import JID, Iq, Message, Presence
12
13
  from slixmpp.exceptions import IqError, IqTimeout, XMPPError
13
14
  from slixmpp.plugins.xep_0004 import Form
@@ -17,19 +18,16 @@ from slixmpp.plugins.xep_0469.stanza import NS as PINNING_NS
17
18
  from slixmpp.plugins.xep_0492.stanza import NS as NOTIFY_NS
18
19
  from slixmpp.plugins.xep_0492.stanza import WhenLiteral
19
20
  from slixmpp.xmlstream import ET
21
+ from sqlalchemy.orm import Session as OrmSession
20
22
 
21
23
  from ..contact.contact import LegacyContact
22
24
  from ..contact.roster import ContactIsUser
23
- from ..core import config
24
- from ..core.mixins import StoredAttributeMixin
25
25
  from ..core.mixins.avatar import AvatarMixin
26
- from ..core.mixins.db import UpdateInfoMixin
27
26
  from ..core.mixins.disco import ChatterDiscoMixin
28
- from ..core.mixins.lock import NamedLockMixin
29
27
  from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
30
- from ..db.models import Room
31
- from ..util import ABCSubclassableOnceAtMost
28
+ from ..db.models import Participant, Room
32
29
  from ..util.jid_escaping import unescape_node
30
+ from ..util.lock import NamedLockMixin
33
31
  from ..util.types import (
34
32
  HoleBound,
35
33
  LegacyGroupIdType,
@@ -40,12 +38,11 @@ from ..util.types import (
40
38
  MucAffiliation,
41
39
  MucType,
42
40
  )
43
- from ..util.util import deprecated, timeit, with_session
41
+ from ..util.util import SubclassableOnce, deprecated, timeit
44
42
  from .archive import MessageArchive
45
43
  from .participant import LegacyParticipant, escape_nickname
46
44
 
47
45
  if TYPE_CHECKING:
48
- from ..core.gateway import BaseGateway
49
46
  from ..core.session import BaseSession
50
47
 
51
48
  ADMIN_NS = "http://jabber.org/protocol/muc#admin"
@@ -57,14 +54,12 @@ class LegacyMUC(
57
54
  Generic[
58
55
  LegacyGroupIdType, LegacyMessageType, LegacyParticipantType, LegacyUserIdType
59
56
  ],
60
- UpdateInfoMixin,
61
- StoredAttributeMixin,
62
57
  AvatarMixin,
63
58
  NamedLockMixin,
64
59
  ChatterDiscoMixin,
65
60
  ReactionRecipientMixin,
66
61
  ThreadRecipientMixin,
67
- metaclass=ABCSubclassableOnceAtMost,
62
+ metaclass=SubclassableOnce,
68
63
  ):
69
64
  """
70
65
  A room, a.k.a. a Multi-User Chat.
@@ -75,12 +70,10 @@ class LegacyMUC(
75
70
 
76
71
  max_history_fetch = 100
77
72
 
78
- type = MucType.CHANNEL
79
73
  is_group = True
80
74
 
81
75
  DISCO_TYPE = "text"
82
76
  DISCO_CATEGORY = "conference"
83
- DISCO_NAME = "unnamed-room"
84
77
 
85
78
  STABLE_ARCHIVE = False
86
79
  """
@@ -128,152 +121,209 @@ class LegacyMUC(
128
121
  tries to set the room subject.
129
122
  """
130
123
 
131
- _avatar_bare_jid = True
132
124
  archive: MessageArchive
125
+ session: "BaseSession"
133
126
 
134
- def __init__(self, session: "BaseSession", legacy_id: LegacyGroupIdType, jid: JID):
135
- self.session = session
136
- self.xmpp: "BaseGateway" = session.xmpp
127
+ stored: Room
137
128
 
138
- self.legacy_id = legacy_id
139
- self.jid = jid
129
+ _participant_cls: Type[LegacyParticipantType]
140
130
 
141
- self._user_resources = set[str]()
131
+ def __init__(self, session: "BaseSession", stored: Room) -> None:
132
+ self.session = session
133
+ self.xmpp = session.xmpp
134
+ self.stored = stored
135
+ self._set_logger()
136
+ super().__init__()
142
137
 
143
- self.Participant = LegacyParticipant.get_self_or_unique_subclass()
138
+ self.archive = MessageArchive(stored, self.xmpp.store.mam)
144
139
 
145
- self._subject = ""
146
- self._subject_setter: Optional[str] = None
140
+ def participant_from_store(
141
+ self, stored: Participant, contact: LegacyContact | None = None
142
+ ) -> LegacyParticipantType:
143
+ if contact is None and stored.contact is not None:
144
+ contact = self.session.contacts.from_store(stored.contact)
145
+ return self._participant_cls(self, stored=stored, contact=contact)
146
+
147
+ @property
148
+ def jid(self) -> JID:
149
+ return self.stored.jid
147
150
 
148
- self.pk: Optional[int] = None
149
- self._user_nick: Optional[str] = None
151
+ @jid.setter
152
+ def jid(self, x: JID):
153
+ # FIXME: without this, mypy yields
154
+ # "Cannot override writeable attribute with read-only property"
155
+ # But it does not happen for LegacyContact. WTF?
156
+ raise RuntimeError
150
157
 
151
- self._participants_filled = False
152
- self._history_filled = False
153
- self._description = ""
154
- self._subject_date: Optional[datetime] = None
158
+ @property
159
+ def legacy_id(self):
160
+ return self.xmpp.LEGACY_ROOM_ID_TYPE(self.stored.legacy_id)
155
161
 
156
- self.__participants_store = self.xmpp.store.participants
157
- self.__store = self.xmpp.store.rooms
162
+ def orm(self) -> OrmSession:
163
+ return self.xmpp.store.session()
158
164
 
159
- self._n_participants: Optional[int] = None
165
+ @property
166
+ def type(self) -> MucType:
167
+ return self.stored.muc_type
160
168
 
161
- self.log = logging.getLogger(self.jid.bare)
162
- self._set_logger_name()
163
- super().__init__()
169
+ @type.setter
170
+ def type(self, type_: MucType) -> None:
171
+ if self.type == type_:
172
+ return
173
+ self.stored.muc_type = type_
174
+ self.commit()
164
175
 
165
176
  @property
166
177
  def n_participants(self):
167
- return self._n_participants
178
+ return self.stored.n_participants
168
179
 
169
180
  @n_participants.setter
170
- def n_participants(self, n_participants: Optional[int]):
171
- if self._n_participants == n_participants:
172
- return
173
- self._n_participants = n_participants
174
- if self._updating_info:
181
+ def n_participants(self, n_participants: Optional[int]) -> None:
182
+ if self.stored.n_participants == n_participants:
175
183
  return
176
- assert self.pk is not None
177
- self.__store.update_n_participants(self.pk, n_participants)
184
+ self.stored.n_participants = n_participants
185
+ self.commit()
178
186
 
179
187
  @property
180
188
  def user_jid(self):
181
189
  return self.session.user_jid
182
190
 
183
- def _set_logger_name(self):
191
+ def _set_logger(self) -> None:
184
192
  self.log = logging.getLogger(f"{self.user_jid}:muc:{self}")
185
193
 
186
- def __repr__(self):
187
- return f"<MUC #{self.pk} '{self.name}' ({self.legacy_id} - {self.jid.user})'>"
194
+ def __repr__(self) -> str:
195
+ return f"<MUC #{self.stored.id} '{self.name}' ({self.stored.legacy_id} - {self.jid.user})'>"
188
196
 
189
197
  @property
190
198
  def subject_date(self) -> Optional[datetime]:
191
- return self._subject_date
199
+ if self.stored.subject_date is None:
200
+ return None
201
+ return self.stored.subject_date.replace(tzinfo=timezone.utc)
192
202
 
193
203
  @subject_date.setter
194
204
  def subject_date(self, when: Optional[datetime]) -> None:
195
- self._subject_date = when
196
- if self._updating_info:
205
+ if self.subject_date == when:
197
206
  return
198
- assert self.pk is not None
199
- self.__store.update_subject_date(self.pk, when)
207
+ self.stored.subject_date = when
208
+ self.commit()
200
209
 
201
- def __send_configuration_change(self, codes):
210
+ def __send_configuration_change(self, codes) -> None:
202
211
  part = self.get_system_participant()
203
212
  part.send_configuration_change(codes)
204
213
 
205
214
  @property
206
215
  def user_nick(self):
207
- return self._user_nick or self.session.bookmarks.user_nick or self.user_jid.node
216
+ return (
217
+ self.stored.user_nick
218
+ or self.session.bookmarks.user_nick
219
+ or self.user_jid.node
220
+ )
208
221
 
209
222
  @user_nick.setter
210
- def user_nick(self, nick: str):
211
- self._user_nick = nick
212
- if not self._updating_info:
213
- self.__store.update_user_nick(self.pk, nick)
223
+ def user_nick(self, nick: str) -> None:
224
+ if nick == self.user_nick:
225
+ return
226
+ self.stored.user_nick = nick
227
+ self.commit()
214
228
 
215
229
  def add_user_resource(self, resource: str) -> None:
216
- self._user_resources.add(resource)
217
- assert self.pk is not None
218
- self.__store.set_resource(self.pk, self._user_resources)
230
+ stored_set = self.get_user_resources()
231
+ if resource in stored_set:
232
+ return
233
+ stored_set.add(resource)
234
+ self.stored.user_resources = (
235
+ json.dumps(list(stored_set)) if stored_set else None
236
+ )
237
+ self.commit()
219
238
 
220
239
  def get_user_resources(self) -> set[str]:
221
- return self._user_resources
240
+ stored_str = self.stored.user_resources
241
+ if stored_str is None:
242
+ return set()
243
+ return set(json.loads(stored_str))
222
244
 
223
245
  def remove_user_resource(self, resource: str) -> None:
224
- self._user_resources.remove(resource)
225
- assert self.pk is not None
226
- self.__store.set_resource(self.pk, self._user_resources)
227
-
228
- async def __fill_participants(self):
229
- if self._participants_filled:
230
- return
231
- assert self.pk is not None
232
- async with self.lock("fill participants"):
233
- self._participants_filled = True
234
- async for p in self.fill_participants():
235
- self.__participants_store.update(p)
236
- self.__store.set_participants_filled(self.pk)
237
-
238
- async def get_participants(self) -> AsyncIterator[LegacyParticipant]:
239
- assert self.pk is not None
240
- if self._participants_filled:
241
- for db_participant in self.xmpp.store.participants.get_all(
242
- self.pk, user_included=True
243
- ):
244
- participant = self.Participant.from_store(
245
- self.session, db_participant, muc=self
246
- )
247
- yield participant
246
+ stored_set = self.get_user_resources()
247
+ if resource not in stored_set:
248
248
  return
249
+ stored_set.remove(resource)
250
+ self.stored.user_resources = (
251
+ json.dumps(list(stored_set)) if stored_set else None
252
+ )
253
+ self.commit()
249
254
 
255
+ async def __fill_participants(self) -> None:
256
+ if self.participants_filled:
257
+ return
250
258
  async with self.lock("fill participants"):
251
- self._participants_filled = True
252
- # We only fill the participants list if/when the MUC is first
253
- # joined by an XMPP client. But we may have instantiated some before.
254
- resources = set[str]()
259
+ parts: list[Participant] = []
260
+ resources: set[str] = set()
255
261
  async for participant in self.fill_participants():
256
- # TODO: batch SQL update at the end of this function for perf?
257
- self.__store_participant(participant)
258
- yield participant
262
+ if participant.stored.id is not None:
263
+ continue
264
+ # During fill_participants(), self.get_participant*() methods may
265
+ # return a participant with a conflicting nick/resource. There is
266
+ # a better way to fix this than the logic below, but this better way
267
+ # has not been found yet.
268
+ if participant.jid.resource in resources:
269
+ if participant.contact is None:
270
+ self.log.warning(
271
+ "Ditching participant %s", participant.nickname
272
+ )
273
+ del participant
274
+ continue
275
+ else:
276
+ nickname = (
277
+ f"{participant.nickname} ({participant.contact.jid.node})"
278
+ )
279
+ participant = self._participant_cls(
280
+ self,
281
+ Participant(nickname=nickname, room=self.stored),
282
+ contact=participant.contact,
283
+ )
259
284
  resources.add(participant.jid.resource)
260
- for db_participant in self.xmpp.store.participants.get_all(
261
- self.pk, user_included=True
262
- ):
263
- participant = self.Participant.from_store(
264
- self.session, db_participant, muc=self
265
- )
266
- if participant.jid.resource not in resources:
267
- yield participant
268
- self.__store.set_participants_filled(self.pk)
269
- return
285
+ parts.append(participant.stored)
286
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
287
+ # FIXME: something must be wrong with all these refreshes and merge,
288
+ # but I did not manage to get rid of them without getting various
289
+ # sqlalchemy exceptions raised everywhere
290
+ orm.add(self.stored)
291
+ orm.refresh(self.stored)
292
+ known = {p.resource for p in self.stored.participants}
293
+ self.stored.participants_filled = True
294
+ for part in parts:
295
+ if part.resource in known:
296
+ continue
297
+ part = orm.merge(part)
298
+ orm.add(part)
299
+ self.stored.participants.append(part)
300
+ orm.commit()
301
+ orm.refresh(self.stored)
302
+
303
+ async def get_participants(
304
+ self, affiliation: Optional[MucAffiliation] = None
305
+ ) -> AsyncIterator[LegacyParticipantType]:
306
+ await self.__fill_participants()
307
+ with self.xmpp.store.session(expire_on_commit=False) as orm:
308
+ orm.add(self.stored)
309
+ for db_participant in self.stored.participants:
310
+ if (
311
+ affiliation is not None
312
+ and db_participant.affiliation != affiliation
313
+ ):
314
+ continue
315
+ yield self.participant_from_store(db_participant)
270
316
 
271
- async def __fill_history(self):
317
+ async def __fill_history(self) -> None:
318
+ if self.stored.history_filled:
319
+ self.log.debug("History has already been fetched.")
320
+ return
272
321
  async with self.lock("fill history"):
273
- if self._history_filled:
274
- log.debug("History has already been fetched %s", self)
275
- return
276
322
  log.debug("Fetching history for %s", self)
323
+ if not self.KEEP_BACKFILLED_PARTICIPANTS:
324
+ with self.xmpp.store.session() as orm:
325
+ orm.add(self.stored)
326
+ participants = list(self.stored.participants)
277
327
  try:
278
328
  before, after = self.archive.get_hole_bounds()
279
329
  if before is not None:
@@ -288,43 +338,42 @@ class LegacyMUC(
288
338
  except NotImplementedError:
289
339
  return
290
340
  except Exception as e:
291
- log.exception("Could not backfill: %s", e)
292
- assert self.pk is not None
293
- self.__store.set_history_filled(self.pk, True)
294
- self._history_filled = True
341
+ self.log.exception("Could not backfill", exc_info=e)
342
+ if not self.KEEP_BACKFILLED_PARTICIPANTS:
343
+ self.stored.participants = participants
344
+ self.stored.history_filled = True
345
+ self.commit(merge=True)
346
+
347
+ @property
348
+ def DISCO_NAME(self) -> str: # type:ignore
349
+ return self.name or "unnamed-room"
295
350
 
296
351
  @property
297
- def name(self):
298
- return self.DISCO_NAME
352
+ def name(self) -> str:
353
+ return self.stored.name or "unnamed-room"
299
354
 
300
355
  @name.setter
301
- def name(self, n: str):
302
- if self.DISCO_NAME == n:
356
+ def name(self, n: str) -> None:
357
+ if self.name == n:
303
358
  return
304
- self.DISCO_NAME = n
305
- self._set_logger_name()
359
+ self.stored.name = n
360
+ self.commit()
361
+ self._set_logger()
306
362
  self.__send_configuration_change((104,))
307
- if self._updating_info:
308
- return
309
- assert self.pk is not None
310
- self.__store.update_name(self.pk, n)
311
363
 
312
364
  @property
313
365
  def description(self):
314
- return self._description
366
+ return self.stored.description or ""
315
367
 
316
368
  @description.setter
317
- def description(self, d: str):
318
- if self._description == d:
369
+ def description(self, d: str) -> None:
370
+ if self.description == d:
319
371
  return
320
- self._description = d
372
+ self.stored.description = d
373
+ self.commit()
321
374
  self.__send_configuration_change((104,))
322
- if self._updating_info:
323
- return
324
- assert self.pk is not None
325
- self.__store.update_description(self.pk, d)
326
375
 
327
- def on_presence_unavailable(self, p: Presence):
376
+ def on_presence_unavailable(self, p: Presence) -> None:
328
377
  pto = p.get_to()
329
378
  if pto.bare != self.jid.bare:
330
379
  return
@@ -332,7 +381,7 @@ class LegacyMUC(
332
381
  pfrom = p.get_from()
333
382
  if pfrom.bare != self.user_jid.bare:
334
383
  return
335
- if (resource := pfrom.resource) in self._user_resources:
384
+ if (resource := pfrom.resource) in self.get_user_resources():
336
385
  if pto.resource != self.user_nick:
337
386
  self.log.debug(
338
387
  "Received 'leave group' request but with wrong nickname. %s", p
@@ -381,7 +430,7 @@ class LegacyMUC(
381
430
  """
382
431
  raise NotImplementedError
383
432
 
384
- async def fill_participants(self) -> AsyncIterator[LegacyParticipant]:
433
+ async def fill_participants(self) -> AsyncIterator[LegacyParticipantType]:
385
434
  """
386
435
  This method should yield the list of all members of this group.
387
436
 
@@ -394,29 +443,26 @@ class LegacyMUC(
394
443
 
395
444
  @property
396
445
  def subject(self):
397
- return self._subject
446
+ return self.stored.subject
398
447
 
399
448
  @subject.setter
400
- def subject(self, s: str):
401
- if s == self._subject:
449
+ def subject(self, s: str) -> None:
450
+ if s == self.subject:
402
451
  return
452
+
453
+ self.stored.subject = s
454
+ self.commit()
403
455
  self.__get_subject_setter_participant().set_room_subject(
404
456
  s, None, self.subject_date, False
405
457
  )
406
458
 
407
- self._subject = s
408
- if self._updating_info:
409
- return
410
- assert self.pk is not None
411
- self.__store.update_subject(self.pk, s)
412
-
413
459
  @property
414
460
  def is_anonymous(self):
415
461
  return self.type == MucType.CHANNEL
416
462
 
417
463
  @property
418
464
  def subject_setter(self) -> Optional[str]:
419
- return self._subject_setter
465
+ return self.stored.subject_setter
420
466
 
421
467
  @subject_setter.setter
422
468
  def subject_setter(self, subject_setter: SubjectSetterType) -> None:
@@ -425,19 +471,16 @@ class LegacyMUC(
425
471
  elif isinstance(subject_setter, LegacyParticipant):
426
472
  subject_setter = subject_setter.nickname
427
473
 
428
- if subject_setter == self._subject_setter:
474
+ if subject_setter == self.subject_setter:
429
475
  return
430
476
  assert isinstance(subject_setter, str | None)
431
- self._subject_setter = subject_setter
432
- if self._updating_info:
433
- return
434
- assert self.pk is not None
435
- self.__store.update_subject_setter(self.pk, subject_setter)
477
+ self.stored.subject_setter = subject_setter
478
+ self.commit()
436
479
 
437
480
  def __get_subject_setter_participant(self) -> LegacyParticipant:
438
- if self._subject_setter is None:
481
+ if self.subject_setter is None:
439
482
  return self.get_system_participant()
440
- return self.Participant(self, self._subject_setter)
483
+ return self._participant_cls(self, Participant(nickname=self.subject_setter))
441
484
 
442
485
  def features(self):
443
486
  features = [
@@ -475,11 +518,13 @@ class LegacyMUC(
475
518
  form.add_field("muc#maxhistoryfetch", value=str(self.max_history_fetch))
476
519
  form.add_field("muc#roominfo_subjectmod", "boolean", value=False)
477
520
 
478
- if self._ALL_INFO_FILLED_ON_STARTUP or self._participants_filled:
479
- assert self.pk is not None
480
- n: Optional[int] = self.__participants_store.get_count(self.pk)
521
+ if self._ALL_INFO_FILLED_ON_STARTUP or self.stored.participants_filled:
522
+ with self.xmpp.store.session() as orm:
523
+ n = orm.scalar(
524
+ sa.select(sa.func.count(Participant.id)).filter_by(room=self.stored)
525
+ )
481
526
  else:
482
- n = self._n_participants
527
+ n = self.n_participants
483
528
  if n is not None:
484
529
  form.add_field("muc#roominfo_occupants", value=str(n))
485
530
 
@@ -489,14 +534,14 @@ class LegacyMUC(
489
534
  if s := self.subject:
490
535
  form.add_field("muc#roominfo_subject", value=s)
491
536
 
492
- if self._set_avatar_task:
537
+ if self._set_avatar_task is not None:
493
538
  await self._set_avatar_task
494
- avatar = self.get_avatar()
495
- if avatar and (h := avatar.id):
496
- form.add_field(
497
- "{http://modules.prosody.im/mod_vcard_muc}avatar#sha1", value=h
498
- )
499
- form.add_field("muc#roominfo_avatarhash", "text-multi", value=[h])
539
+ avatar = self.get_avatar()
540
+ if avatar and (h := avatar.id):
541
+ form.add_field(
542
+ "{http://modules.prosody.im/mod_vcard_muc}avatar#sha1", value=h
543
+ )
544
+ form.add_field("muc#roominfo_avatarhash", "text-multi", value=[h])
500
545
 
501
546
  form.add_field("muc#roomconfig_membersonly", "boolean", value=is_group)
502
547
  form.add_field(
@@ -514,7 +559,7 @@ class LegacyMUC(
514
559
 
515
560
  return r
516
561
 
517
- def shutdown(self):
562
+ def shutdown(self) -> None:
518
563
  _, user_jid = escape_nickname(self.jid, self.user_nick)
519
564
  for user_full_jid in self.user_full_jids():
520
565
  presence = self.xmpp.make_presence(
@@ -526,7 +571,7 @@ class LegacyMUC(
526
571
  presence.send()
527
572
 
528
573
  def user_full_jids(self):
529
- for r in self._user_resources:
574
+ for r in self.get_user_resources():
530
575
  j = JID(self.user_jid)
531
576
  j.resource = r
532
577
  yield j
@@ -536,14 +581,9 @@ class LegacyMUC(
536
581
  _, user_muc_jid = escape_nickname(self.jid, self.user_nick)
537
582
  return user_muc_jid
538
583
 
539
- def _legacy_to_xmpp(self, legacy_id: LegacyMessageType):
540
- return self.xmpp.store.sent.get_group_xmpp_id(
541
- self.session.user_pk, str(legacy_id)
542
- ) or self.session.legacy_to_xmpp_msg_id(legacy_id)
543
-
544
584
  async def echo(
545
585
  self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
546
- ):
586
+ ) -> None:
547
587
  origin_id = msg.get_origin_id()
548
588
 
549
589
  msg.set_from(self.user_muc_jid)
@@ -568,24 +608,11 @@ class LegacyMUC(
568
608
 
569
609
  msg.send()
570
610
 
571
- def _get_cached_avatar_id(self):
572
- if self.pk is None:
573
- return None
574
- return self.xmpp.store.rooms.get_avatar_legacy_id(self.pk)
575
-
576
- def _post_avatar_update(self) -> None:
577
- if self.pk is None:
578
- return
579
- assert self.pk is not None
580
- self.xmpp.store.rooms.set_avatar(
581
- self.pk,
582
- self._avatar_pk,
583
- None if self.avatar_id is None else str(self.avatar_id),
584
- )
611
+ def _post_avatar_update(self, cached_avatar) -> None:
585
612
  self.__send_configuration_change((104,))
586
613
  self._send_room_presence()
587
614
 
588
- def _send_room_presence(self, user_full_jid: Optional[JID] = None):
615
+ def _send_room_presence(self, user_full_jid: Optional[JID] = None) -> None:
589
616
  if user_full_jid is None:
590
617
  tos = self.user_full_jids()
591
618
  else:
@@ -599,20 +626,19 @@ class LegacyMUC(
599
626
  p.send()
600
627
 
601
628
  @timeit
602
- @with_session
603
629
  async def join(self, join_presence: Presence):
604
630
  user_full_jid = join_presence.get_from()
605
631
  requested_nickname = join_presence.get_to().resource
606
632
  client_resource = user_full_jid.resource
607
633
 
608
- if client_resource in self._user_resources:
634
+ if client_resource in self.get_user_resources():
609
635
  self.log.debug("Received join from a resource that is already joined.")
610
636
 
611
- self.add_user_resource(client_resource)
612
-
613
637
  if not requested_nickname or not client_resource:
614
638
  raise XMPPError("jid-malformed", by=self.jid)
615
639
 
640
+ self.add_user_resource(client_resource)
641
+
616
642
  self.log.debug(
617
643
  "Resource %s of %s wants to join room %s with nickname %s",
618
644
  client_resource,
@@ -631,7 +657,7 @@ class LegacyMUC(
631
657
 
632
658
  if user_participant is None:
633
659
  user_participant = await self.get_user_participant()
634
- if not user_participant.is_user: # type:ignore
660
+ if not user_participant.is_user:
635
661
  self.log.warning("is_user flag not set participant on user_participant")
636
662
  user_participant.is_user = True # type:ignore
637
663
  user_participant.send_initial_presence(
@@ -662,7 +688,7 @@ class LegacyMUC(
662
688
  since=since,
663
689
  )
664
690
  self.__get_subject_setter_participant().set_room_subject(
665
- self._subject if self.HAS_SUBJECT else (self.description or self.name),
691
+ self.subject if self.HAS_SUBJECT else (self.description or self.name),
666
692
  user_full_jid,
667
693
  self.subject_date,
668
694
  )
@@ -683,22 +709,17 @@ class LegacyMUC(
683
709
  return p
684
710
 
685
711
  def __store_participant(self, p: "LegacyParticipantType") -> None:
686
- # we don't want to update the participant list when we're filling history
687
- if not self.KEEP_BACKFILLED_PARTICIPANTS and self.get_lock("fill history"):
712
+ if self.get_lock("fill participants"):
688
713
  return
689
- assert self.pk is not None
690
- p.pk = self.__participants_store.add(self.pk, p.nickname)
691
- self.__participants_store.update(p)
692
- if p._hats:
693
- self.__participants_store.set_hats(p.pk, p._hats)
714
+ p.commit(merge=True)
694
715
 
695
716
  async def get_participant(
696
717
  self,
697
718
  nickname: str,
698
- raise_if_not_found=False,
699
- fill_first=False,
700
- store=True,
701
- **kwargs,
719
+ raise_if_not_found: bool = False,
720
+ fill_first: bool = False,
721
+ store: bool = True,
722
+ is_user: bool = False,
702
723
  ) -> "LegacyParticipantType":
703
724
  """
704
725
  Get a participant by their nickname.
@@ -713,30 +734,34 @@ class LegacyMUC(
713
734
  :param fill_first: Ensure :meth:`.LegacyMUC.fill_participants()` has been called first
714
735
  (internal use by slidge, plugins should not need that)
715
736
  :param store: persistently store the user in the list of MUC participants
716
- :param kwargs: additional parameters for the :class:`.Participant`
717
- construction (optional)
718
737
  :return:
719
738
  """
720
- if fill_first and not self._participants_filled:
721
- async for _ in self.get_participants():
722
- pass
723
- if self.pk is not None:
724
- with self.xmpp.store.session():
725
- stored = self.__participants_store.get_by_nickname(
726
- self.pk, nickname
727
- ) or self.__participants_store.get_by_resource(self.pk, nickname)
728
- if stored is not None:
729
- return self.Participant.from_store(self.session, stored)
739
+ if fill_first:
740
+ await self.__fill_participants()
741
+ with self.xmpp.store.session() as orm:
742
+ stored = (
743
+ orm.query(Participant)
744
+ .filter(
745
+ Participant.room == self.stored,
746
+ (Participant.nickname == nickname)
747
+ | (Participant.resource == nickname),
748
+ )
749
+ .one_or_none()
750
+ )
751
+ if stored is not None:
752
+ return self.participant_from_store(stored)
730
753
 
731
754
  if raise_if_not_found:
732
755
  raise XMPPError("item-not-found")
733
- p = self.Participant(self, nickname, **kwargs)
734
- if store and not self._updating_info:
756
+ p = self._participant_cls(
757
+ self, Participant(room=self.stored, nickname=nickname, is_user=is_user)
758
+ )
759
+ if store:
735
760
  self.__store_participant(p)
736
761
  if (
737
762
  not self.get_lock("fill participants")
738
763
  and not self.get_lock("fill history")
739
- and self._participants_filled
764
+ and self.stored.participants_filled
740
765
  and not p.is_user
741
766
  and not p.is_system
742
767
  ):
@@ -752,10 +777,10 @@ class LegacyMUC(
752
777
  service
753
778
  :return:
754
779
  """
755
- return self.Participant(self, is_system=True)
780
+ return self._participant_cls(self, Participant(), is_system=True)
756
781
 
757
782
  async def get_participant_by_contact(
758
- self, c: "LegacyContact", **kwargs
783
+ self, c: "LegacyContact"
759
784
  ) -> "LegacyParticipantType":
760
785
  """
761
786
  Get a non-anonymous participant.
@@ -764,37 +789,39 @@ class LegacyMUC(
764
789
  that the Contact jid is associated to this participant
765
790
 
766
791
  :param c: The :class:`.LegacyContact` instance corresponding to this contact
767
- :param kwargs: additional parameters for the :class:`.Participant`
768
- construction (optional)
769
792
  :return:
770
793
  """
771
794
  await self.session.contacts.ready
772
795
 
773
- if self.pk is not None:
774
- c._LegacyContact__ensure_pk() # type: ignore
775
- assert c.contact_pk is not None
776
- with self.__store.session():
777
- stored = self.__participants_store.get_by_contact(self.pk, c.contact_pk)
796
+ if not self.get_lock("fill participants"):
797
+ with self.xmpp.store.session() as orm:
798
+ self.stored = orm.merge(self.stored)
799
+ stored = (
800
+ orm.query(Participant)
801
+ .filter_by(contact=c.stored, room=self.stored)
802
+ .one_or_none()
803
+ )
778
804
  if stored is not None:
779
- return self.Participant.from_store(
780
- self.session, stored, muc=self, contact=c
781
- )
805
+ return self.participant_from_store(stored=stored, contact=c)
782
806
 
783
- nickname = c.name or unescape_node(c.jid_username)
807
+ nickname = c.name or unescape_node(c.jid.node)
784
808
 
785
- if self.pk is None:
809
+ if self.stored.id is None:
786
810
  nick_available = True
787
811
  else:
788
- nick_available = self.__store.nickname_is_available(self.pk, nickname)
812
+ with self.xmpp.store.session() as orm:
813
+ nick_available = (
814
+ orm.query(Participant.id).filter_by(
815
+ room=self.stored, nickname=nickname
816
+ )
817
+ ).one_or_none() is None
789
818
 
790
819
  if not nick_available:
791
820
  self.log.debug("Nickname conflict")
792
- nickname = f"{nickname} ({c.jid_username})"
793
- p = self.Participant(self, nickname, **kwargs)
794
- p.contact = c
795
-
796
- if self._updating_info:
797
- return p
821
+ nickname = f"{nickname} ({c.jid.node})"
822
+ p = self._participant_cls(
823
+ self, Participant(nickname=nickname, room=self.stored), contact=c
824
+ )
798
825
 
799
826
  self.__store_participant(p)
800
827
  # FIXME: this is not great but given the current design,
@@ -803,7 +830,7 @@ class LegacyMUC(
803
830
  # and role afterwards.
804
831
  # We need a refactor of the MUC class… later™
805
832
  if (
806
- self._participants_filled
833
+ self.stored.participants_filled
807
834
  and not self.get_lock("fill participants")
808
835
  and not self.get_lock("fill history")
809
836
  ):
@@ -811,19 +838,19 @@ class LegacyMUC(
811
838
  return p
812
839
 
813
840
  async def get_participant_by_legacy_id(
814
- self, legacy_id: LegacyUserIdType, **kwargs
841
+ self, legacy_id: LegacyUserIdType
815
842
  ) -> "LegacyParticipantType":
816
843
  try:
817
844
  c = await self.session.contacts.by_legacy_id(legacy_id)
818
845
  except ContactIsUser:
819
- return await self.get_user_participant(**kwargs)
820
- return await self.get_participant_by_contact(c, **kwargs)
846
+ return await self.get_user_participant()
847
+ return await self.get_participant_by_contact(c)
821
848
 
822
849
  def remove_participant(
823
850
  self,
824
851
  p: "LegacyParticipantType",
825
- kick=False,
826
- ban=False,
852
+ kick: bool = False,
853
+ ban: bool = False,
827
854
  reason: str | None = None,
828
855
  ):
829
856
  """
@@ -836,7 +863,9 @@ class LegacyMUC(
836
863
  """
837
864
  if kick and ban:
838
865
  raise TypeError("Either kick or ban")
839
- self.__participants_store.delete(p.pk)
866
+ with self.xmpp.store.session() as orm:
867
+ orm.delete(p.stored)
868
+ orm.commit()
840
869
  if kick:
841
870
  codes = {307}
842
871
  elif ban:
@@ -844,20 +873,23 @@ class LegacyMUC(
844
873
  else:
845
874
  codes = None
846
875
  presence = p._make_presence(ptype="unavailable", status_codes=codes)
847
- p._affiliation = "outcast" if ban else "none"
848
- p._role = "none"
876
+ p.stored.affiliation = "outcast" if ban else "none"
877
+ p.stored.role = "none"
849
878
  if reason:
850
879
  presence["muc"].set_item_attr("reason", reason)
851
880
  p._send(presence)
852
881
 
853
- def rename_participant(self, old_nickname: str, new_nickname: str):
854
- assert self.pk is not None
855
- with self.xmpp.store.session():
856
- stored = self.__participants_store.get_by_nickname(self.pk, old_nickname)
882
+ def rename_participant(self, old_nickname: str, new_nickname: str) -> None:
883
+ with self.xmpp.store.session() as orm:
884
+ stored = (
885
+ orm.query(Participant)
886
+ .filter_by(room=self.stored, nickname=old_nickname)
887
+ .one_or_none()
888
+ )
857
889
  if stored is None:
858
890
  self.log.debug("Tried to rename a participant that we didn't know")
859
891
  return
860
- p = self.Participant.from_store(self.session, stored)
892
+ p = self.participant_from_store(stored)
861
893
  if p.nickname == old_nickname:
862
894
  p.nickname = new_nickname
863
895
 
@@ -868,7 +900,7 @@ class LegacyMUC(
868
900
  maxstanzas: Optional[int] = None,
869
901
  seconds: Optional[int] = None,
870
902
  since: Optional[datetime] = None,
871
- ):
903
+ ) -> None:
872
904
  """
873
905
  Old-style history join (internal slidge use)
874
906
 
@@ -895,7 +927,7 @@ class LegacyMUC(
895
927
  msg.set_to(full_jid)
896
928
  self.xmpp.send(msg, False)
897
929
 
898
- async def send_mam(self, iq: Iq):
930
+ async def send_mam(self, iq: Iq) -> None:
899
931
  await self.__fill_history()
900
932
 
901
933
  form_values = iq["mam"]["form"].get_values()
@@ -983,11 +1015,11 @@ class LegacyMUC(
983
1015
  reply["mam_fin"]["rsm"]["count"] = str(count)
984
1016
  reply.send()
985
1017
 
986
- async def send_mam_metadata(self, iq: Iq):
1018
+ async def send_mam_metadata(self, iq: Iq) -> None:
987
1019
  await self.__fill_history()
988
1020
  await self.archive.send_metadata(iq)
989
1021
 
990
- async def kick_resource(self, r: str):
1022
+ async def kick_resource(self, r: str) -> None:
991
1023
  """
992
1024
  Kick a XMPP client of the user. (slidge internal use)
993
1025
 
@@ -1035,12 +1067,11 @@ class LegacyMUC(
1035
1067
 
1036
1068
  async def add_to_bookmarks(
1037
1069
  self,
1038
- auto_join=True,
1039
- invite=False,
1040
- preserve=True,
1070
+ auto_join: bool = True,
1071
+ preserve: bool = True,
1041
1072
  pin: bool | None = None,
1042
1073
  notify: WhenLiteral | None = None,
1043
- ):
1074
+ ) -> None:
1044
1075
  """
1045
1076
  Add the MUC to the user's XMPP bookmarks (:xep:`0402')
1046
1077
 
@@ -1051,11 +1082,6 @@ class LegacyMUC(
1051
1082
  this MUC on startup. In theory, XMPP clients will receive
1052
1083
  a "push" notification when this is called, and they will
1053
1084
  join if they are online.
1054
- :param invite: send an invitation to join this MUC emanating from
1055
- the gateway. While this should not be strictly necessary,
1056
- it can help for clients that do not support :xep:`0402`, or
1057
- that have 'do not honor bookmarks auto-join' turned on in their
1058
- settings.
1059
1085
  :param preserve: preserve auto-join and bookmarks extensions
1060
1086
  set by the user outside slidge
1061
1087
  :param pin: Pin the group chat bookmark :xep:`0469`. Requires privileged entity.
@@ -1136,20 +1162,32 @@ class LegacyMUC(
1136
1162
  "IQ privileges (XEP0356) are not set, we cannot add bookmarks for the user"
1137
1163
  )
1138
1164
  # fallback by forcing invitation
1139
- invite = True
1165
+ bookmark_add_fail = True
1140
1166
  except IqError as e:
1141
1167
  warnings.warn(
1142
1168
  f"Something went wrong while trying to set the bookmarks: {e}"
1143
1169
  )
1144
1170
  # fallback by forcing invitation
1145
- invite = True
1171
+ bookmark_add_fail = True
1172
+ else:
1173
+ bookmark_add_fail = False
1146
1174
  else:
1147
1175
  self.log.debug("Bookmark does not need updating.")
1148
1176
  return
1149
1177
 
1150
- if invite or (config.ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS and existing is None):
1178
+ if bookmark_add_fail:
1151
1179
  self.session.send_gateway_invite(
1152
- self, reason="This group could not be added automatically for you"
1180
+ self,
1181
+ reason="This group could not be added automatically for you, most"
1182
+ "likely because this gateway is not configured as a privileged entity. "
1183
+ "Contact your administrator.",
1184
+ )
1185
+ elif existing is None and self.session.user.preferences.get(
1186
+ "always_invite_when_adding_bookmarks", True
1187
+ ):
1188
+ self.session.send_gateway_invite(
1189
+ self,
1190
+ reason="The gateway is configured to always send invitations for groups.",
1153
1191
  )
1154
1192
 
1155
1193
  async def on_avatar(
@@ -1238,12 +1276,10 @@ class LegacyMUC(
1238
1276
  raise NotImplementedError
1239
1277
 
1240
1278
  async def parse_mentions(self, text: str) -> list[Mention]:
1241
- with self.__store.session():
1279
+ with self.xmpp.store.session() as orm:
1242
1280
  await self.__fill_participants()
1243
- assert self.pk is not None
1244
- participants = {
1245
- p.nickname: p for p in self.__participants_store.get_all(self.pk)
1246
- }
1281
+ orm.add(self.stored)
1282
+ participants = {p.nickname: p for p in self.stored.participants}
1247
1283
 
1248
1284
  if len(participants) == 0:
1249
1285
  return []
@@ -1264,8 +1300,8 @@ class LegacyMUC(
1264
1300
  if span[0] != 0 and text[span[0] - 1] not in _WHITESPACE_OR_PUNCTUATION:
1265
1301
  continue
1266
1302
  if span[1] == len(text) or text[span[1]] in _WHITESPACE_OR_PUNCTUATION:
1267
- participant = self.Participant.from_store(
1268
- self.session, participants[nick]
1303
+ participant = self.participant_from_store(
1304
+ stored=participants[nick],
1269
1305
  )
1270
1306
  if contact := participant.contact:
1271
1307
  result.append(
@@ -1284,45 +1320,12 @@ class LegacyMUC(
1284
1320
  """
1285
1321
  raise NotImplementedError
1286
1322
 
1287
- @classmethod
1288
- def from_store(cls, session, stored: Room, *args, **kwargs) -> Self:
1289
- muc = cls(
1290
- session,
1291
- cls.xmpp.LEGACY_ROOM_ID_TYPE(stored.legacy_id),
1292
- stored.jid,
1293
- *args, # type: ignore
1294
- **kwargs, # type: ignore
1295
- )
1296
- muc.pk = stored.id
1297
- muc.type = stored.muc_type # type: ignore
1298
- muc._user_nick = stored.user_nick
1299
- if stored.name:
1300
- muc.DISCO_NAME = stored.name
1301
- if stored.description:
1302
- muc._description = stored.description
1303
- if (data := stored.extra_attributes) is not None:
1304
- muc.deserialize_extra_attributes(data)
1305
- muc._subject = stored.subject or ""
1306
- if stored.subject_date is not None:
1307
- muc._subject_date = stored.subject_date.replace(tzinfo=timezone.utc)
1308
- muc._participants_filled = stored.participants_filled
1309
- muc._n_participants = stored.n_participants
1310
- muc._history_filled = stored.history_filled
1311
- if stored.user_resources is not None:
1312
- muc._user_resources = set(json.loads(stored.user_resources))
1313
- muc._subject_setter = stored.subject_setter
1314
- muc.archive = MessageArchive(muc.pk, session.xmpp.store.mam)
1315
- muc._set_logger_name()
1316
- muc._AvatarMixin__avatar_unique_id = ( # type:ignore
1317
- None
1318
- if stored.avatar_legacy_id is None
1319
- else session.xmpp.AVATAR_ID_TYPE(stored.avatar_legacy_id)
1320
- )
1321
- muc._avatar_pk = stored.avatar_id
1322
- return muc
1323
+ @property
1324
+ def participants_filled(self) -> bool:
1325
+ return self.stored.participants_filled
1323
1326
 
1324
1327
 
1325
- def set_origin_id(msg: Message, origin_id: str):
1328
+ def set_origin_id(msg: Message, origin_id: str) -> None:
1326
1329
  sub = ET.Element("{urn:xmpp:sid:0}origin-id")
1327
1330
  sub.attrib["id"] = origin_id
1328
1331
  msg.xml.append(sub)