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.
- slidge/__init__.py +3 -5
- slidge/__main__.py +2 -196
- slidge/__version__.py +5 -0
- slidge/command/adhoc.py +8 -1
- slidge/command/admin.py +6 -7
- slidge/command/base.py +1 -2
- slidge/command/register.py +32 -16
- slidge/command/user.py +85 -6
- slidge/contact/contact.py +165 -49
- slidge/contact/roster.py +122 -47
- slidge/core/config.py +14 -11
- slidge/core/gateway/base.py +148 -36
- slidge/core/gateway/caps.py +7 -5
- slidge/core/gateway/disco.py +2 -4
- slidge/core/gateway/mam.py +1 -4
- slidge/core/gateway/muc_admin.py +1 -1
- slidge/core/gateway/ping.py +2 -3
- slidge/core/gateway/presence.py +1 -1
- slidge/core/gateway/registration.py +32 -21
- slidge/core/gateway/search.py +3 -5
- slidge/core/gateway/session_dispatcher.py +120 -57
- slidge/core/gateway/vcard_temp.py +7 -5
- slidge/core/mixins/__init__.py +11 -1
- slidge/core/mixins/attachment.py +32 -14
- slidge/core/mixins/avatar.py +90 -25
- slidge/core/mixins/base.py +8 -2
- slidge/core/mixins/db.py +18 -0
- slidge/core/mixins/disco.py +0 -10
- slidge/core/mixins/message.py +18 -8
- slidge/core/mixins/message_maker.py +17 -9
- slidge/core/mixins/presence.py +17 -4
- slidge/core/pubsub.py +54 -220
- slidge/core/session.py +69 -34
- slidge/db/__init__.py +4 -0
- slidge/db/alembic/env.py +64 -0
- slidge/db/alembic/script.py.mako +26 -0
- slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
- slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
- slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
- slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
- slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
- slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +133 -0
- slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +85 -0
- slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
- slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +48 -0
- slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
- slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
- slidge/db/avatar.py +235 -0
- slidge/db/meta.py +65 -0
- slidge/db/models.py +375 -0
- slidge/db/store.py +1078 -0
- slidge/group/archive.py +58 -14
- slidge/group/bookmarks.py +72 -57
- slidge/group/participant.py +87 -28
- slidge/group/room.py +369 -211
- slidge/main.py +201 -0
- slidge/migration.py +30 -0
- slidge/slixfix/__init__.py +35 -2
- slidge/slixfix/roster.py +11 -4
- slidge/slixfix/xep_0292/vcard4.py +3 -0
- slidge/util/archive_msg.py +2 -1
- slidge/util/db.py +1 -47
- slidge/util/test.py +71 -4
- slidge/util/types.py +29 -4
- slidge/util/util.py +22 -0
- {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/METADATA +4 -4
- slidge-0.2.0a1.dist-info/RECORD +114 -0
- slidge/core/cache.py +0 -183
- slidge/util/schema.sql +0 -126
- slidge/util/sql.py +0 -508
- slidge-0.1.3.dist-info/RECORD +0 -96
- {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/LICENSE +0 -0
- {slidge-0.1.3.dist-info → slidge-0.2.0a1.dist-info}/WHEEL +0 -0
- {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.
|
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.
|
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.
|
144
|
-
self.get_system_participant()
|
145
|
-
)
|
144
|
+
self._subject_setter: Optional[str] = None
|
146
145
|
|
147
|
-
self.
|
146
|
+
self.pk: Optional[int] = None
|
148
147
|
self._user_nick: Optional[str] = None
|
149
148
|
|
150
|
-
self.
|
151
|
-
self.
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
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.
|
317
|
+
if pfrom.bare != self.user_jid.bare:
|
242
318
|
return
|
243
|
-
if (resource := pfrom.resource) in
|
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
|
-
|
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
|
-
|
274
|
-
|
349
|
+
after: Optional[HoleBound] = None,
|
350
|
+
before: Optional[HoleBound] = None,
|
275
351
|
):
|
276
352
|
"""
|
277
|
-
Override this if the legacy network provide server-side
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
:param
|
284
|
-
|
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
|
-
|
291
|
-
|
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
|
-
|
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.
|
304
|
-
self.
|
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
|
-
|
317
|
-
|
401
|
+
@property
|
402
|
+
def subject_setter(self) -> Optional[str]:
|
403
|
+
return self._subject_setter
|
318
404
|
|
319
|
-
|
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
|
-
|
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
|
-
|
324
|
-
|
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.
|
368
|
-
|
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.
|
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.
|
419
|
-
j = copy(self.
|
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.
|
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.
|
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.
|
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.
|
601
|
+
self.user_jid,
|
495
602
|
self.legacy_id,
|
496
603
|
requested_nickname,
|
497
604
|
)
|
498
605
|
|
499
|
-
|
500
|
-
|
501
|
-
for participant in self.
|
502
|
-
if participant.is_user:
|
503
|
-
|
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
|
-
|
509
|
-
|
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
|
-
|
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.
|
566
|
-
|
567
|
-
|
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
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
)
|
612
|
-
|
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
|
-
|
642
|
-
if
|
643
|
-
|
644
|
-
|
645
|
-
self.
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
self.
|
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
|
-
|
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
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
p.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.
|
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.
|
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.
|
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,
|
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
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1037
|
-
|
1038
|
-
|
1039
|
-
|
1040
|
-
|
1041
|
-
|
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")
|