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