slidge 0.1.3__py3-none-any.whl → 0.2.0a1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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")
|