slidge 0.1.0__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 +61 -0
- slidge/__main__.py +192 -0
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +3 -0
- slidge/core/cache.py +183 -0
- slidge/core/config.py +209 -0
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +892 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +757 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +19 -0
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +31 -0
- slidge/core/mixins/disco.py +130 -0
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +398 -0
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +217 -0
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +525 -0
- slidge/core/session.py +752 -0
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +440 -0
- slidge/group/room.py +1095 -0
- slidge/migration.py +18 -0
- slidge/py.typed +0 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/slixfix/link_preview/__init__.py +10 -0
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/slixfix/xep_0077/__init__.py +10 -0
- slidge/slixfix/xep_0077/register.py +289 -0
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/slixfix/xep_0100/__init__.py +5 -0
- slidge/slixfix/xep_0100/gateway.py +121 -0
- slidge/slixfix/xep_0100/stanza.py +9 -0
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/slixfix/xep_0356_old/__init__.py +7 -0
- slidge/slixfix/xep_0356_old/privilege.py +167 -0
- slidge/slixfix/xep_0356_old/stanza.py +44 -0
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +15 -0
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +206 -0
- slidge/util/db.py +229 -0
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +295 -0
- slidge/util/types.py +180 -0
- slidge/util/util.py +295 -0
- slidge-0.1.0.dist-info/LICENSE +661 -0
- slidge-0.1.0.dist-info/METADATA +109 -0
- slidge-0.1.0.dist-info/RECORD +96 -0
- slidge-0.1.0.dist-info/WHEEL +4 -0
- slidge-0.1.0.dist-info/entry_points.txt +3 -0
slidge/group/room.py
ADDED
@@ -0,0 +1,1095 @@
|
|
1
|
+
import logging
|
2
|
+
import re
|
3
|
+
import string
|
4
|
+
import warnings
|
5
|
+
from copy import copy
|
6
|
+
from datetime import datetime, timedelta, timezone
|
7
|
+
from typing import TYPE_CHECKING, Generic, Optional, Union
|
8
|
+
from uuid import uuid4
|
9
|
+
|
10
|
+
from slixmpp import JID, Iq, Message, Presence
|
11
|
+
from slixmpp.exceptions import IqError, XMPPError
|
12
|
+
from slixmpp.jid import _unescape_node
|
13
|
+
from slixmpp.plugins.xep_0004 import Form
|
14
|
+
from slixmpp.plugins.xep_0060.stanza import Item
|
15
|
+
from slixmpp.plugins.xep_0082 import parse as str_to_datetime
|
16
|
+
from slixmpp.xmlstream import ET
|
17
|
+
|
18
|
+
from ..contact.roster import ContactIsUser
|
19
|
+
from ..core import config
|
20
|
+
from ..core.mixins.avatar import AvatarMixin
|
21
|
+
from ..core.mixins.disco import ChatterDiscoMixin
|
22
|
+
from ..core.mixins.lock import NamedLockMixin
|
23
|
+
from ..core.mixins.recipient import ReactionRecipientMixin, ThreadRecipientMixin
|
24
|
+
from ..util import ABCSubclassableOnceAtMost
|
25
|
+
from ..util.types import (
|
26
|
+
LegacyGroupIdType,
|
27
|
+
LegacyMessageType,
|
28
|
+
LegacyParticipantType,
|
29
|
+
LegacyUserIdType,
|
30
|
+
Mention,
|
31
|
+
MucAffiliation,
|
32
|
+
MucType,
|
33
|
+
)
|
34
|
+
from ..util.util import deprecated
|
35
|
+
from .archive import MessageArchive
|
36
|
+
|
37
|
+
if TYPE_CHECKING:
|
38
|
+
from ..contact import LegacyContact
|
39
|
+
from ..core.gateway import BaseGateway
|
40
|
+
from ..core.session import BaseSession
|
41
|
+
|
42
|
+
ADMIN_NS = "http://jabber.org/protocol/muc#admin"
|
43
|
+
|
44
|
+
|
45
|
+
class LegacyMUC(
|
46
|
+
Generic[
|
47
|
+
LegacyGroupIdType, LegacyMessageType, LegacyParticipantType, LegacyUserIdType
|
48
|
+
],
|
49
|
+
AvatarMixin,
|
50
|
+
NamedLockMixin,
|
51
|
+
ChatterDiscoMixin,
|
52
|
+
ReactionRecipientMixin,
|
53
|
+
ThreadRecipientMixin,
|
54
|
+
metaclass=ABCSubclassableOnceAtMost,
|
55
|
+
):
|
56
|
+
"""
|
57
|
+
A room, a.k.a. a Multi-User Chat.
|
58
|
+
|
59
|
+
MUC instances are obtained by calling :py:meth:`slidge.group.bookmarks.LegacyBookmarks`
|
60
|
+
on the user's :py:class:`slidge.core.session.BaseSession`.
|
61
|
+
"""
|
62
|
+
|
63
|
+
subject_date: Optional[datetime] = None
|
64
|
+
n_participants: Optional[int] = None
|
65
|
+
max_history_fetch = 100
|
66
|
+
|
67
|
+
type = MucType.CHANNEL
|
68
|
+
is_group = True
|
69
|
+
|
70
|
+
DISCO_TYPE = "text"
|
71
|
+
DISCO_CATEGORY = "conference"
|
72
|
+
DISCO_NAME = "unnamed-room"
|
73
|
+
|
74
|
+
STABLE_ARCHIVE = False
|
75
|
+
"""
|
76
|
+
Because legacy events like reactions, editions, etc. don't all map to a stanza
|
77
|
+
with a proper legacy ID, slidge usually cannot guarantee the stability of the archive
|
78
|
+
across restarts.
|
79
|
+
|
80
|
+
Set this to True if you know what you're doing, but realistically, this can't
|
81
|
+
be set to True until archive is permanently stored on disk by slidge.
|
82
|
+
|
83
|
+
This is just a flag on archive responses that most clients ignore anyway.
|
84
|
+
"""
|
85
|
+
|
86
|
+
KEEP_BACKFILLED_PARTICIPANTS = False
|
87
|
+
"""
|
88
|
+
Set this to ``True`` if the participant list is not full after calling
|
89
|
+
``fill_participants()``. This is a workaround for networks with huge
|
90
|
+
participant lists which do not map really well the MUCs where all presences
|
91
|
+
are sent on join.
|
92
|
+
It allows to ensure that the participants that last spoke (within the
|
93
|
+
``fill_history()`` method are effectively participants, thus making possible
|
94
|
+
for XMPP clients to fetch their avatars.
|
95
|
+
"""
|
96
|
+
|
97
|
+
_ALL_INFO_FILLED_ON_STARTUP = False
|
98
|
+
"""
|
99
|
+
Set this to true if the fill_participants() / fill_participants() design does not
|
100
|
+
fit the legacy API, ie, no lazy loading of the participant list and history.
|
101
|
+
"""
|
102
|
+
|
103
|
+
HAS_DESCRIPTION = True
|
104
|
+
"""
|
105
|
+
Set this to false if the legacy network does not allow setting a description
|
106
|
+
for the group. In this case the description field will not be present in the
|
107
|
+
room configuration form.
|
108
|
+
"""
|
109
|
+
|
110
|
+
HAS_SUBJECT = True
|
111
|
+
"""
|
112
|
+
Set this to false if the legacy network does not allow setting a subject
|
113
|
+
(sometimes also called topic) for the group. In this case, as a subject is
|
114
|
+
recommended by :xep:`0045` ("SHALL"), the description (or the group name as
|
115
|
+
ultimate fallback) will be used as the room subject.
|
116
|
+
By setting this to false, an error will be returned when the :term:`User`
|
117
|
+
tries to set the room subject.
|
118
|
+
"""
|
119
|
+
|
120
|
+
_avatar_pubsub_broadcast = False
|
121
|
+
_avatar_bare_jid = True
|
122
|
+
|
123
|
+
def __init__(self, session: "BaseSession", legacy_id: LegacyGroupIdType, jid: JID):
|
124
|
+
from .participant import LegacyParticipant
|
125
|
+
|
126
|
+
self.session = session
|
127
|
+
self.xmpp: "BaseGateway" = session.xmpp
|
128
|
+
self.user = session.user
|
129
|
+
self.log = logging.getLogger(f"{self.user.bare_jid}:muc:{jid}")
|
130
|
+
|
131
|
+
self.legacy_id = legacy_id
|
132
|
+
self.jid = jid
|
133
|
+
|
134
|
+
self.user_resources = set[str]()
|
135
|
+
|
136
|
+
self.Participant = LegacyParticipant.get_self_or_unique_subclass()
|
137
|
+
|
138
|
+
self.xmpp.add_event_handler(
|
139
|
+
"presence_unavailable", self._on_presence_unavailable
|
140
|
+
)
|
141
|
+
|
142
|
+
self._subject = ""
|
143
|
+
self.subject_setter: Union[str, "LegacyContact", "LegacyParticipant"] = (
|
144
|
+
"unknown"
|
145
|
+
)
|
146
|
+
|
147
|
+
self.archive: MessageArchive = MessageArchive(str(self.jid), self.user)
|
148
|
+
self._user_nick: Optional[str] = None
|
149
|
+
|
150
|
+
self._participants_by_nicknames = dict[str, LegacyParticipantType]()
|
151
|
+
self._participants_by_escaped_nicknames = dict[str, LegacyParticipantType]()
|
152
|
+
self._participants_by_contacts = dict["LegacyContact", LegacyParticipantType]()
|
153
|
+
|
154
|
+
self.__participants_filled = False
|
155
|
+
self.__history_filled = False
|
156
|
+
self._description = ""
|
157
|
+
super().__init__()
|
158
|
+
|
159
|
+
def __repr__(self):
|
160
|
+
return f"<MUC {self.legacy_id}/{self.jid}/{self.name}>"
|
161
|
+
|
162
|
+
def __send_configuration_change(self, codes):
|
163
|
+
part = self.get_system_participant()
|
164
|
+
part.send_configuration_change(codes)
|
165
|
+
|
166
|
+
@property
|
167
|
+
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
|
+
)
|
173
|
+
|
174
|
+
@user_nick.setter
|
175
|
+
def user_nick(self, nick: str):
|
176
|
+
self._user_nick = nick
|
177
|
+
|
178
|
+
async def __fill_participants(self):
|
179
|
+
async with self.lock("fill participants"):
|
180
|
+
if self.__participants_filled:
|
181
|
+
return
|
182
|
+
self.__participants_filled = True
|
183
|
+
try:
|
184
|
+
await self.fill_participants()
|
185
|
+
except NotImplementedError:
|
186
|
+
pass
|
187
|
+
|
188
|
+
async def __fill_history(self):
|
189
|
+
async with self.lock("fill history"):
|
190
|
+
if self.__history_filled:
|
191
|
+
log.debug("History has already been fetched %s", self)
|
192
|
+
return
|
193
|
+
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
|
+
try:
|
208
|
+
await self.backfill(legacy_id, oldest_date)
|
209
|
+
except NotImplementedError:
|
210
|
+
return
|
211
|
+
self.__history_filled = True
|
212
|
+
|
213
|
+
@property
|
214
|
+
def name(self):
|
215
|
+
return self.DISCO_NAME
|
216
|
+
|
217
|
+
@name.setter
|
218
|
+
def name(self, n: str):
|
219
|
+
if self.DISCO_NAME == n:
|
220
|
+
return
|
221
|
+
self.DISCO_NAME = n
|
222
|
+
self.__send_configuration_change((104,))
|
223
|
+
|
224
|
+
@property
|
225
|
+
def description(self):
|
226
|
+
return self._description
|
227
|
+
|
228
|
+
@description.setter
|
229
|
+
def description(self, d: str):
|
230
|
+
if self._description == d:
|
231
|
+
return
|
232
|
+
self._description = d
|
233
|
+
self.__send_configuration_change((104,))
|
234
|
+
|
235
|
+
def _on_presence_unavailable(self, p: Presence):
|
236
|
+
pto = p.get_to()
|
237
|
+
if pto.bare != self.jid.bare:
|
238
|
+
return
|
239
|
+
|
240
|
+
pfrom = p.get_from()
|
241
|
+
if pfrom.bare != self.user.bare_jid:
|
242
|
+
return
|
243
|
+
if (resource := pfrom.resource) in (resources := self.user_resources):
|
244
|
+
if pto.resource != self.user_nick:
|
245
|
+
self.log.debug(
|
246
|
+
"Received 'leave group' request but with wrong nickname. %s", p
|
247
|
+
)
|
248
|
+
resources.remove(resource)
|
249
|
+
else:
|
250
|
+
self.log.debug(
|
251
|
+
"Received 'leave group' request but resource was not listed. %s", p
|
252
|
+
)
|
253
|
+
|
254
|
+
async def update_info(self):
|
255
|
+
"""
|
256
|
+
Fetch information about this group from the legacy network
|
257
|
+
|
258
|
+
This is awaited on MUC instantiation, and should be overridden to
|
259
|
+
update the attributes of the group chat, like title, subject, number
|
260
|
+
of participants etc.
|
261
|
+
|
262
|
+
To take advantage of the slidge avatar cache, you can check the .avatar
|
263
|
+
property to retrieve the "legacy file ID" of the cached avatar. If there
|
264
|
+
is no change, you should not call
|
265
|
+
:py:meth:`slidge.core.mixins.avatar.AvatarMixin.set_avatar()` or
|
266
|
+
attempt to modify
|
267
|
+
the :attr:.avatar property.
|
268
|
+
"""
|
269
|
+
raise NotImplementedError
|
270
|
+
|
271
|
+
async def backfill(
|
272
|
+
self,
|
273
|
+
oldest_message_id: Optional[LegacyMessageType] = None,
|
274
|
+
oldest_message_date: Optional[datetime] = None,
|
275
|
+
):
|
276
|
+
"""
|
277
|
+
Override this if the legacy network provide server-side archive.
|
278
|
+
In it, send history messages using ``self.get_participant().send*``,
|
279
|
+
with the ``archive_only=True`` kwarg.
|
280
|
+
|
281
|
+
You only need to fetch messages older than ``oldest_message_id``.
|
282
|
+
|
283
|
+
:param oldest_message_id: The oldest message ID already present in the archive
|
284
|
+
:param oldest_message_date: The oldest message date already present in the archive
|
285
|
+
"""
|
286
|
+
raise NotImplementedError
|
287
|
+
|
288
|
+
async def fill_participants(self):
|
289
|
+
"""
|
290
|
+
In here, call self.get_participant(), self.get_participant_by_contact(),
|
291
|
+
of self.get_user_participant() to make an initial list of participants.
|
292
|
+
"""
|
293
|
+
raise NotImplementedError
|
294
|
+
|
295
|
+
@property
|
296
|
+
def subject(self):
|
297
|
+
return self._subject
|
298
|
+
|
299
|
+
@subject.setter
|
300
|
+
def subject(self, s: str):
|
301
|
+
if s == self._subject:
|
302
|
+
return
|
303
|
+
self.xmpp.loop.create_task(
|
304
|
+
self.__get_subject_setter_participant()
|
305
|
+
).add_done_callback(
|
306
|
+
lambda task: task.result().set_room_subject(
|
307
|
+
s, None, self.subject_date, False
|
308
|
+
)
|
309
|
+
)
|
310
|
+
self._subject = s
|
311
|
+
|
312
|
+
@property
|
313
|
+
def is_anonymous(self):
|
314
|
+
return self.type == MucType.CHANNEL
|
315
|
+
|
316
|
+
async def __get_subject_setter_participant(self):
|
317
|
+
from slidge.contact import LegacyContact
|
318
|
+
|
319
|
+
from .participant import LegacyParticipant
|
320
|
+
|
321
|
+
who = self.subject_setter
|
322
|
+
|
323
|
+
if isinstance(who, LegacyParticipant):
|
324
|
+
return who
|
325
|
+
elif isinstance(who, str):
|
326
|
+
return await self.get_participant(who, store=False)
|
327
|
+
elif isinstance(self.subject_setter, LegacyContact):
|
328
|
+
return await self.get_participant_by_contact(who)
|
329
|
+
else:
|
330
|
+
return self.get_system_participant()
|
331
|
+
|
332
|
+
def features(self):
|
333
|
+
features = [
|
334
|
+
"http://jabber.org/protocol/muc",
|
335
|
+
"http://jabber.org/protocol/muc#stable_id",
|
336
|
+
"http://jabber.org/protocol/muc#self-ping-optimization",
|
337
|
+
"urn:xmpp:mam:2",
|
338
|
+
"urn:xmpp:mam:2#extended",
|
339
|
+
"urn:xmpp:sid:0",
|
340
|
+
"muc_persistent",
|
341
|
+
"vcard-temp",
|
342
|
+
"urn:xmpp:ping",
|
343
|
+
"urn:xmpp:occupant-id:0",
|
344
|
+
self.xmpp.plugin["xep_0425"].stanza.NS,
|
345
|
+
]
|
346
|
+
if self.type == MucType.GROUP:
|
347
|
+
features.extend(["muc_membersonly", "muc_nonanonymous", "muc_hidden"])
|
348
|
+
elif self.type == MucType.CHANNEL:
|
349
|
+
features.extend(["muc_open", "muc_semianonymous", "muc_public"])
|
350
|
+
elif self.type == MucType.CHANNEL_NON_ANONYMOUS:
|
351
|
+
features.extend(["muc_open", "muc_nonanonymous", "muc_public"])
|
352
|
+
return features
|
353
|
+
|
354
|
+
async def extended_features(self):
|
355
|
+
is_group = self.type == MucType.GROUP
|
356
|
+
|
357
|
+
form = self.xmpp.plugin["xep_0004"].make_form(ftype="result")
|
358
|
+
|
359
|
+
form.add_field(
|
360
|
+
"FORM_TYPE", "hidden", value="http://jabber.org/protocol/muc#roominfo"
|
361
|
+
)
|
362
|
+
form.add_field("muc#roomconfig_persistentroom", "boolean", value=True)
|
363
|
+
form.add_field("muc#roomconfig_changesubject", "boolean", value=False)
|
364
|
+
form.add_field("muc#maxhistoryfetch", value=str(self.max_history_fetch))
|
365
|
+
form.add_field("muc#roominfo_subjectmod", "boolean", value=False)
|
366
|
+
|
367
|
+
if self._ALL_INFO_FILLED_ON_STARTUP or self.__participants_filled:
|
368
|
+
n: Optional[int] = len(await self.get_participants())
|
369
|
+
else:
|
370
|
+
n = self.n_participants
|
371
|
+
if n is not None:
|
372
|
+
form.add_field("muc#roominfo_occupants", value=str(n))
|
373
|
+
|
374
|
+
if d := self.description:
|
375
|
+
form.add_field("muc#roominfo_description", value=d)
|
376
|
+
|
377
|
+
if s := self.subject:
|
378
|
+
form.add_field("muc#roominfo_subject", value=s)
|
379
|
+
|
380
|
+
if self._set_avatar_task:
|
381
|
+
await self._set_avatar_task
|
382
|
+
avatar = self.get_avatar()
|
383
|
+
if avatar and (h := avatar.id):
|
384
|
+
form.add_field(
|
385
|
+
"{http://modules.prosody.im/mod_vcard_muc}avatar#sha1", value=h
|
386
|
+
)
|
387
|
+
form.add_field("muc#roominfo_avatarhash", "text-multi", value=[h])
|
388
|
+
|
389
|
+
form.add_field("muc#roomconfig_membersonly", "boolean", value=is_group)
|
390
|
+
form.add_field(
|
391
|
+
"muc#roomconfig_whois",
|
392
|
+
"list-single",
|
393
|
+
value="moderators" if self.is_anonymous else "anyone",
|
394
|
+
)
|
395
|
+
form.add_field("muc#roomconfig_publicroom", "boolean", value=not is_group)
|
396
|
+
form.add_field("muc#roomconfig_allowpm", "boolean", value=False)
|
397
|
+
|
398
|
+
r = [form]
|
399
|
+
|
400
|
+
if reaction_form := await self.restricted_emoji_extended_feature():
|
401
|
+
r.append(reaction_form)
|
402
|
+
|
403
|
+
return r
|
404
|
+
|
405
|
+
def shutdown(self):
|
406
|
+
user_jid = copy(self.jid)
|
407
|
+
user_jid.resource = self.user_nick
|
408
|
+
for user_full_jid in self.user_full_jids():
|
409
|
+
presence = self.xmpp.make_presence(
|
410
|
+
pfrom=user_jid, pto=user_full_jid, ptype="unavailable"
|
411
|
+
)
|
412
|
+
presence["muc"]["affiliation"] = "none"
|
413
|
+
presence["muc"]["role"] = "none"
|
414
|
+
presence["muc"]["status_codes"] = {110, 332}
|
415
|
+
presence.send()
|
416
|
+
|
417
|
+
def user_full_jids(self):
|
418
|
+
for r in self.user_resources:
|
419
|
+
j = copy(self.user.jid)
|
420
|
+
j.resource = r
|
421
|
+
yield j
|
422
|
+
|
423
|
+
@property
|
424
|
+
def user_muc_jid(self):
|
425
|
+
user_muc_jid = copy(self.jid)
|
426
|
+
user_muc_jid.resource = self.user_nick
|
427
|
+
return user_muc_jid
|
428
|
+
|
429
|
+
def _legacy_to_xmpp(self, legacy_id: LegacyMessageType):
|
430
|
+
return self.session.sent.get(legacy_id) or self.session.legacy_to_xmpp_msg_id(
|
431
|
+
legacy_id
|
432
|
+
)
|
433
|
+
|
434
|
+
async def echo(
|
435
|
+
self, msg: Message, legacy_msg_id: Optional[LegacyMessageType] = None
|
436
|
+
):
|
437
|
+
origin_id = msg.get_origin_id()
|
438
|
+
|
439
|
+
msg.set_from(self.user_muc_jid)
|
440
|
+
msg.set_id(msg.get_id())
|
441
|
+
if origin_id:
|
442
|
+
# because of slixmpp internal magic, we need to do this to ensure the origin_id
|
443
|
+
# is present
|
444
|
+
set_origin_id(msg, origin_id)
|
445
|
+
if legacy_msg_id:
|
446
|
+
msg["stanza_id"]["id"] = self.session.legacy_to_xmpp_msg_id(legacy_msg_id)
|
447
|
+
else:
|
448
|
+
msg["stanza_id"]["id"] = str(uuid4())
|
449
|
+
msg["stanza_id"]["by"] = self.jid
|
450
|
+
msg["occupant-id"]["id"] = "slidge-user"
|
451
|
+
|
452
|
+
self.archive.add(msg, await self.get_user_participant())
|
453
|
+
|
454
|
+
for user_full_jid in self.user_full_jids():
|
455
|
+
self.log.debug("Echoing to %s", user_full_jid)
|
456
|
+
msg = copy(msg)
|
457
|
+
msg.set_to(user_full_jid)
|
458
|
+
|
459
|
+
msg.send()
|
460
|
+
|
461
|
+
def _post_avatar_update(self) -> None:
|
462
|
+
self.__send_configuration_change((104,))
|
463
|
+
self._send_room_presence()
|
464
|
+
|
465
|
+
def _send_room_presence(self, user_full_jid: Optional[JID] = None):
|
466
|
+
if user_full_jid is None:
|
467
|
+
tos = self.user_full_jids()
|
468
|
+
else:
|
469
|
+
tos = [user_full_jid]
|
470
|
+
for to in tos:
|
471
|
+
p = self.xmpp.make_presence(pfrom=self.jid, pto=to)
|
472
|
+
if (avatar := self.get_avatar()) and (h := avatar.id):
|
473
|
+
p["vcard_temp_update"]["photo"] = h
|
474
|
+
else:
|
475
|
+
p["vcard_temp_update"]["photo"] = ""
|
476
|
+
p.send()
|
477
|
+
|
478
|
+
async def join(self, join_presence: Presence):
|
479
|
+
user_full_jid = join_presence.get_from()
|
480
|
+
requested_nickname = join_presence.get_to().resource
|
481
|
+
client_resource = user_full_jid.resource
|
482
|
+
|
483
|
+
if client_resource in self.user_resources:
|
484
|
+
self.log.debug("Received join from a resource that is already joined.")
|
485
|
+
|
486
|
+
self.user_resources.add(client_resource)
|
487
|
+
|
488
|
+
if not requested_nickname or not client_resource:
|
489
|
+
raise XMPPError("jid-malformed", by=self.jid)
|
490
|
+
|
491
|
+
self.log.debug(
|
492
|
+
"Resource %s of %s wants to join room %s with nickname %s",
|
493
|
+
client_resource,
|
494
|
+
self.user,
|
495
|
+
self.legacy_id,
|
496
|
+
requested_nickname,
|
497
|
+
)
|
498
|
+
|
499
|
+
await self.__fill_participants()
|
500
|
+
|
501
|
+
for participant in self._participants_by_nicknames.values():
|
502
|
+
if participant.is_user: # type:ignore
|
503
|
+
continue
|
504
|
+
if participant.is_system: # type:ignore
|
505
|
+
continue
|
506
|
+
participant.send_initial_presence(full_jid=user_full_jid)
|
507
|
+
|
508
|
+
user_nick = self.user_nick
|
509
|
+
user_participant = await self.get_user_participant()
|
510
|
+
if not user_participant.is_user: # type:ignore
|
511
|
+
self.log.warning("is_user flag not set participant on user_participant")
|
512
|
+
user_participant.is_user = True # type:ignore
|
513
|
+
user_participant.send_initial_presence(
|
514
|
+
user_full_jid,
|
515
|
+
presence_id=join_presence["id"],
|
516
|
+
nick_change=user_nick != requested_nickname,
|
517
|
+
)
|
518
|
+
|
519
|
+
history_params = join_presence["muc_join"]["history"]
|
520
|
+
maxchars = int_or_none(history_params["maxchars"])
|
521
|
+
maxstanzas = int_or_none(history_params["maxstanzas"])
|
522
|
+
seconds = int_or_none(history_params["seconds"])
|
523
|
+
try:
|
524
|
+
since = self.xmpp.plugin["xep_0082"].parse(history_params["since"])
|
525
|
+
except ValueError:
|
526
|
+
since = None
|
527
|
+
if seconds:
|
528
|
+
since = datetime.now() - timedelta(seconds=seconds)
|
529
|
+
if equals_zero(maxchars) or equals_zero(maxstanzas):
|
530
|
+
log.debug("Joining client does not want any old-school MUC history-on-join")
|
531
|
+
else:
|
532
|
+
self.log.debug("Old school history fill")
|
533
|
+
await self.__fill_history()
|
534
|
+
await self.__old_school_history(
|
535
|
+
user_full_jid,
|
536
|
+
maxchars=maxchars,
|
537
|
+
maxstanzas=maxstanzas,
|
538
|
+
since=since,
|
539
|
+
)
|
540
|
+
(await self.__get_subject_setter_participant()).set_room_subject(
|
541
|
+
self._subject if self.HAS_SUBJECT else (self.description or self.name),
|
542
|
+
user_full_jid,
|
543
|
+
self.subject_date,
|
544
|
+
)
|
545
|
+
if t := self._set_avatar_task:
|
546
|
+
await t
|
547
|
+
self._send_room_presence(user_full_jid)
|
548
|
+
self.user_resources.add(client_resource)
|
549
|
+
|
550
|
+
async def get_user_participant(self, **kwargs) -> "LegacyParticipantType":
|
551
|
+
"""
|
552
|
+
Get the participant representing the gateway user
|
553
|
+
|
554
|
+
:param kwargs: additional parameters for the :class:`.Participant`
|
555
|
+
construction (optional)
|
556
|
+
:return:
|
557
|
+
"""
|
558
|
+
p = await self.get_participant(self.user_nick, is_user=True, **kwargs)
|
559
|
+
self.__store_participant(p)
|
560
|
+
return p
|
561
|
+
|
562
|
+
def __store_participant(self, p: "LegacyParticipantType"):
|
563
|
+
# we don't want to update the participant list when we're filling history
|
564
|
+
if not self.KEEP_BACKFILLED_PARTICIPANTS and self.get_lock("fill history"):
|
565
|
+
return
|
566
|
+
self._participants_by_nicknames[p.nickname] = p # type:ignore
|
567
|
+
if p.contact:
|
568
|
+
self._participants_by_contacts[p.contact] = p
|
569
|
+
|
570
|
+
async def get_participant(
|
571
|
+
self,
|
572
|
+
nickname: str,
|
573
|
+
raise_if_not_found=False,
|
574
|
+
fill_first=False,
|
575
|
+
store=True,
|
576
|
+
**kwargs,
|
577
|
+
) -> "LegacyParticipantType":
|
578
|
+
"""
|
579
|
+
Get a participant by their nickname.
|
580
|
+
|
581
|
+
In non-anonymous groups, you probably want to use
|
582
|
+
:meth:`.LegacyMUC.get_participant_by_contact` instead.
|
583
|
+
|
584
|
+
:param nickname: Nickname of the participant (used as resource part in the MUC)
|
585
|
+
:param raise_if_not_found: Raise XMPPError("item-not-found") if they are not
|
586
|
+
in the participant list (internal use by slidge, plugins should not
|
587
|
+
need that)
|
588
|
+
:param fill_first: Ensure :meth:`.LegacyMUC.fill_participants()` has been called first
|
589
|
+
(internal use by slidge, plugins should not need that)
|
590
|
+
:param store: persistently store the user in the list of MUC participants
|
591
|
+
:param kwargs: additional parameters for the :class:`.Participant`
|
592
|
+
construction (optional)
|
593
|
+
:return:
|
594
|
+
"""
|
595
|
+
if fill_first:
|
596
|
+
await self.__fill_participants()
|
597
|
+
p = self._participants_by_nicknames.get(
|
598
|
+
nickname
|
599
|
+
) or self._participants_by_escaped_nicknames.get(nickname)
|
600
|
+
if p is None:
|
601
|
+
if raise_if_not_found:
|
602
|
+
raise XMPPError("item-not-found")
|
603
|
+
p = self.Participant(self, nickname, **kwargs)
|
604
|
+
if store:
|
605
|
+
self.__store_participant(p)
|
606
|
+
return p
|
607
|
+
|
608
|
+
def get_system_participant(self) -> "LegacyParticipantType":
|
609
|
+
"""
|
610
|
+
Get a pseudo-participant, representing the room itself
|
611
|
+
|
612
|
+
Can be useful for events that cannot be mapped to a participant,
|
613
|
+
e.g. anonymous moderation events, or announces from the legacy
|
614
|
+
service
|
615
|
+
:return:
|
616
|
+
"""
|
617
|
+
return self.Participant(self, is_system=True)
|
618
|
+
|
619
|
+
async def get_participant_by_contact(
|
620
|
+
self, c: "LegacyContact", **kwargs
|
621
|
+
) -> "LegacyParticipantType":
|
622
|
+
"""
|
623
|
+
Get a non-anonymous participant.
|
624
|
+
|
625
|
+
This is what should be used in non-anonymous groups ideally, to ensure
|
626
|
+
that the Contact jid is associated to this participant
|
627
|
+
|
628
|
+
:param c: The :class:`.LegacyContact` instance corresponding to this contact
|
629
|
+
:param kwargs: additional parameters for the :class:`.Participant`
|
630
|
+
construction (optional)
|
631
|
+
:return:
|
632
|
+
"""
|
633
|
+
await self.session.contacts.ready
|
634
|
+
p = self._participants_by_contacts.get(c)
|
635
|
+
if p is None:
|
636
|
+
nickname = c.name or _unescape_node(c.jid_username)
|
637
|
+
if nickname in self._participants_by_nicknames:
|
638
|
+
self.log.debug("Nickname conflict")
|
639
|
+
nickname = f"{nickname} ({c.jid_username})"
|
640
|
+
p = self.Participant(self, nickname, **kwargs)
|
641
|
+
p.contact = c
|
642
|
+
c.participants.add(p)
|
643
|
+
# FIXME: this is not great but given the current design,
|
644
|
+
# during participants fill and history backfill we do not
|
645
|
+
# want to send presence, because we might update affiliation
|
646
|
+
# and role afterwards.
|
647
|
+
# We need a refactor of the MUC class… later™
|
648
|
+
if not self.get_lock("fill participants") and not self.get_lock(
|
649
|
+
"fill history"
|
650
|
+
):
|
651
|
+
p.send_last_presence(force=True, no_cache_online=True)
|
652
|
+
self.__store_participant(p)
|
653
|
+
return p
|
654
|
+
|
655
|
+
async def get_participant_by_legacy_id(
|
656
|
+
self, legacy_id: LegacyUserIdType, **kwargs
|
657
|
+
) -> "LegacyParticipantType":
|
658
|
+
try:
|
659
|
+
c = await self.session.contacts.by_legacy_id(legacy_id)
|
660
|
+
except ContactIsUser:
|
661
|
+
return await self.get_user_participant(**kwargs)
|
662
|
+
return await self.get_participant_by_contact(c, **kwargs)
|
663
|
+
|
664
|
+
async def get_participants(self):
|
665
|
+
"""
|
666
|
+
Get all known participants of the group, ensure :meth:`.LegacyMUC.fill_participants`
|
667
|
+
has been awaited once before. Plugins should not use that, internal
|
668
|
+
slidge use only.
|
669
|
+
:return:
|
670
|
+
"""
|
671
|
+
await self.__fill_participants()
|
672
|
+
return list(self._participants_by_nicknames.values())
|
673
|
+
|
674
|
+
def remove_participant(self, p: "LegacyParticipantType", kick=False, ban=False):
|
675
|
+
"""
|
676
|
+
Call this when a participant leaves the room
|
677
|
+
|
678
|
+
:param p: The participant
|
679
|
+
:param kick: Whether the participant left because they were kicked
|
680
|
+
:param ban: Whether the participant left because they were banned
|
681
|
+
"""
|
682
|
+
if kick and ban:
|
683
|
+
raise TypeError("Either kick or ban")
|
684
|
+
if p.contact is not None:
|
685
|
+
try:
|
686
|
+
del self._participants_by_contacts[p.contact]
|
687
|
+
except KeyError:
|
688
|
+
self.log.warning(
|
689
|
+
"Removed a participant we didn't know was here?, %s", p
|
690
|
+
)
|
691
|
+
else:
|
692
|
+
p.contact.participants.remove(p)
|
693
|
+
try:
|
694
|
+
del self._participants_by_nicknames[p.nickname] # type:ignore
|
695
|
+
except KeyError:
|
696
|
+
self.log.warning("Removed a participant we didn't know was here?, %s", p)
|
697
|
+
if kick:
|
698
|
+
codes = {307}
|
699
|
+
elif ban:
|
700
|
+
codes = {301}
|
701
|
+
else:
|
702
|
+
codes = None
|
703
|
+
presence = p._make_presence(ptype="unavailable", status_codes=codes)
|
704
|
+
p._affiliation = "outcast" if ban else "none"
|
705
|
+
p._role = "none"
|
706
|
+
p._send(presence)
|
707
|
+
|
708
|
+
def rename_participant(self, old_nickname: str, new_nickname: str):
|
709
|
+
try:
|
710
|
+
p = self._participants_by_nicknames.pop(old_nickname)
|
711
|
+
except KeyError:
|
712
|
+
# when called by participant.nickname.setter
|
713
|
+
return
|
714
|
+
self._participants_by_nicknames[new_nickname] = p
|
715
|
+
if p.nickname == old_nickname:
|
716
|
+
p.nickname = new_nickname
|
717
|
+
|
718
|
+
async def __old_school_history(
|
719
|
+
self,
|
720
|
+
full_jid: JID,
|
721
|
+
maxchars: Optional[int] = None,
|
722
|
+
maxstanzas: Optional[int] = None,
|
723
|
+
seconds: Optional[int] = None,
|
724
|
+
since: Optional[datetime] = None,
|
725
|
+
):
|
726
|
+
"""
|
727
|
+
Old-style history join (internal slidge use)
|
728
|
+
|
729
|
+
:param full_jid:
|
730
|
+
:param maxchars:
|
731
|
+
:param maxstanzas:
|
732
|
+
:param seconds:
|
733
|
+
:param since:
|
734
|
+
:return:
|
735
|
+
"""
|
736
|
+
if since is None:
|
737
|
+
if seconds is None:
|
738
|
+
start_date = datetime.now(tz=timezone.utc) - timedelta(days=1)
|
739
|
+
else:
|
740
|
+
start_date = datetime.now(tz=timezone.utc) - timedelta(seconds=seconds)
|
741
|
+
else:
|
742
|
+
start_date = since or datetime.now(tz=timezone.utc) - timedelta(days=1)
|
743
|
+
|
744
|
+
for h_msg in self.archive.get_all(
|
745
|
+
start_date=start_date, end_date=None, last_page_n=maxstanzas
|
746
|
+
):
|
747
|
+
msg = h_msg.stanza_component_ns
|
748
|
+
msg["delay"]["stamp"] = h_msg.when
|
749
|
+
msg.set_to(full_jid)
|
750
|
+
self.xmpp.send(msg, False)
|
751
|
+
|
752
|
+
async def send_mam(self, iq: Iq):
|
753
|
+
await self.__fill_history()
|
754
|
+
|
755
|
+
form_values = iq["mam"]["form"].get_values()
|
756
|
+
|
757
|
+
start_date = str_to_datetime_or_none(form_values.get("start"))
|
758
|
+
end_date = str_to_datetime_or_none(form_values.get("end"))
|
759
|
+
|
760
|
+
after_id = form_values.get("after-id")
|
761
|
+
before_id = form_values.get("before-id")
|
762
|
+
|
763
|
+
sender = form_values.get("with")
|
764
|
+
|
765
|
+
ids = form_values.get("ids") or ()
|
766
|
+
|
767
|
+
if max_str := iq["mam"]["rsm"]["max"]:
|
768
|
+
try:
|
769
|
+
max_results = int(max_str)
|
770
|
+
except ValueError:
|
771
|
+
max_results = None
|
772
|
+
else:
|
773
|
+
max_results = None
|
774
|
+
|
775
|
+
after_id_rsm = iq["mam"]["rsm"]["after"]
|
776
|
+
after_id = after_id_rsm or after_id
|
777
|
+
|
778
|
+
before_rsm = iq["mam"]["rsm"]["before"]
|
779
|
+
if before_rsm is True and max_results is not None:
|
780
|
+
last_page_n = max_results
|
781
|
+
else:
|
782
|
+
last_page_n = None
|
783
|
+
|
784
|
+
first = None
|
785
|
+
last = None
|
786
|
+
count = 0
|
787
|
+
|
788
|
+
it = self.archive.get_all(
|
789
|
+
start_date,
|
790
|
+
end_date,
|
791
|
+
before_id,
|
792
|
+
after_id,
|
793
|
+
ids,
|
794
|
+
last_page_n,
|
795
|
+
sender,
|
796
|
+
bool(iq["mam"]["flip_page"]),
|
797
|
+
)
|
798
|
+
|
799
|
+
for history_msg in it:
|
800
|
+
last = xmpp_id = history_msg.id
|
801
|
+
if first is None:
|
802
|
+
first = xmpp_id
|
803
|
+
|
804
|
+
wrapper_msg = self.xmpp.make_message(mfrom=self.jid, mto=iq.get_from())
|
805
|
+
wrapper_msg["mam_result"]["queryid"] = iq["mam"]["queryid"]
|
806
|
+
wrapper_msg["mam_result"]["id"] = xmpp_id
|
807
|
+
wrapper_msg["mam_result"].append(history_msg.forwarded())
|
808
|
+
|
809
|
+
wrapper_msg.send()
|
810
|
+
count += 1
|
811
|
+
|
812
|
+
if max_results and count == max_results:
|
813
|
+
break
|
814
|
+
|
815
|
+
if max_results:
|
816
|
+
try:
|
817
|
+
next(it)
|
818
|
+
except StopIteration:
|
819
|
+
complete = True
|
820
|
+
else:
|
821
|
+
complete = False
|
822
|
+
else:
|
823
|
+
complete = True
|
824
|
+
|
825
|
+
reply = iq.reply()
|
826
|
+
if not self.STABLE_ARCHIVE:
|
827
|
+
reply["mam_fin"]["stable"] = "false"
|
828
|
+
if complete:
|
829
|
+
reply["mam_fin"]["complete"] = "true"
|
830
|
+
reply["mam_fin"]["rsm"]["first"] = first
|
831
|
+
reply["mam_fin"]["rsm"]["last"] = last
|
832
|
+
reply["mam_fin"]["rsm"]["count"] = str(count)
|
833
|
+
reply.send()
|
834
|
+
|
835
|
+
async def send_mam_metadata(self, iq: Iq):
|
836
|
+
await self.__fill_history()
|
837
|
+
await self.archive.send_metadata(iq)
|
838
|
+
|
839
|
+
async def kick_resource(self, r: str):
|
840
|
+
"""
|
841
|
+
Kick a XMPP client of the user. (slidge internal use)
|
842
|
+
|
843
|
+
:param r: The resource to kick
|
844
|
+
"""
|
845
|
+
pto = self.user.jid
|
846
|
+
pto.resource = r
|
847
|
+
p = self.xmpp.make_presence(
|
848
|
+
pfrom=(await self.get_user_participant()).jid, pto=pto
|
849
|
+
)
|
850
|
+
p["muc"]["affiliation"] = "none"
|
851
|
+
p["muc"]["role"] = "none"
|
852
|
+
p["muc"]["status_codes"] = {110, 333}
|
853
|
+
p.send()
|
854
|
+
|
855
|
+
async def add_to_bookmarks(self, auto_join=True, invite=False, preserve=True):
|
856
|
+
"""
|
857
|
+
Add the MUC to the user's XMPP bookmarks (:xep:`0402')
|
858
|
+
|
859
|
+
This requires that slidge has the IQ privileged set correctly
|
860
|
+
on the XMPP server
|
861
|
+
|
862
|
+
:param auto_join: whether XMPP clients should automatically join
|
863
|
+
this MUC on startup. In theory, XMPP clients will receive
|
864
|
+
a "push" notification when this is called, and they will
|
865
|
+
join if they are online.
|
866
|
+
:param invite: send an invitation to join this MUC emanating from
|
867
|
+
the gateway. While this should not be strictly necessary,
|
868
|
+
it can help for clients that do not support :xep:`0402`, or
|
869
|
+
that have 'do not honor bookmarks auto-join' turned on in their
|
870
|
+
settings.
|
871
|
+
:param preserve: preserve auto-join and bookmarks extensions
|
872
|
+
set by the user outside slidge
|
873
|
+
"""
|
874
|
+
item = Item()
|
875
|
+
item["id"] = self.jid
|
876
|
+
|
877
|
+
iq = Iq(stype="get", sfrom=self.user.jid, sto=self.user.jid)
|
878
|
+
iq["pubsub"]["items"]["node"] = self.xmpp["xep_0402"].stanza.NS
|
879
|
+
iq["pubsub"]["items"].append(item)
|
880
|
+
|
881
|
+
is_update = False
|
882
|
+
if preserve:
|
883
|
+
try:
|
884
|
+
ans = await self.xmpp["xep_0356"].send_privileged_iq(iq)
|
885
|
+
is_update = len(ans["pubsub"]["items"]) == 1
|
886
|
+
# this below creates the item if it wasn't here already
|
887
|
+
# (slixmpp annoying magic)
|
888
|
+
item = ans["pubsub"]["items"]["item"]
|
889
|
+
item["id"] = self.jid
|
890
|
+
except IqError:
|
891
|
+
item["conference"]["autojoin"] = auto_join
|
892
|
+
except PermissionError:
|
893
|
+
warnings.warn(
|
894
|
+
"IQ privileges (XEP0356) are not set, we cannot fetch the user bookmarks"
|
895
|
+
)
|
896
|
+
else:
|
897
|
+
# if the bookmark is already present, we preserve it as much as
|
898
|
+
# possible, especially custom <extensions>
|
899
|
+
self.log.debug("Existing: %s", item)
|
900
|
+
# if it's an update, we do not touch the auto join flag
|
901
|
+
if not is_update:
|
902
|
+
item["conference"]["autojoin"] = auto_join
|
903
|
+
else:
|
904
|
+
item["conference"]["autojoin"] = auto_join
|
905
|
+
|
906
|
+
item["conference"]["nick"] = self.user_nick
|
907
|
+
iq = Iq(stype="set", sfrom=self.user.jid, sto=self.user.jid)
|
908
|
+
iq["pubsub"]["publish"]["node"] = self.xmpp["xep_0402"].stanza.NS
|
909
|
+
iq["pubsub"]["publish"].append(item)
|
910
|
+
|
911
|
+
iq["pubsub"]["publish_options"] = _BOOKMARKS_OPTIONS
|
912
|
+
|
913
|
+
try:
|
914
|
+
await self.xmpp["xep_0356"].send_privileged_iq(iq)
|
915
|
+
except PermissionError:
|
916
|
+
warnings.warn(
|
917
|
+
"IQ privileges (XEP0356) are not set, we cannot add bookmarks for the user"
|
918
|
+
)
|
919
|
+
# fallback by forcing invitation
|
920
|
+
invite = True
|
921
|
+
except IqError as e:
|
922
|
+
warnings.warn(
|
923
|
+
f"Something went wrong while trying to set the bookmarks: {e}"
|
924
|
+
)
|
925
|
+
# fallback by forcing invitation
|
926
|
+
invite = True
|
927
|
+
|
928
|
+
if invite or (config.ALWAYS_INVITE_WHEN_ADDING_BOOKMARKS and not is_update):
|
929
|
+
self.session.send_gateway_invite(
|
930
|
+
self, reason="This group could not be added automatically for you"
|
931
|
+
)
|
932
|
+
|
933
|
+
async def on_avatar(
|
934
|
+
self, data: Optional[bytes], mime: Optional[str]
|
935
|
+
) -> Optional[Union[int, str]]:
|
936
|
+
"""
|
937
|
+
Called when the user tries to set the avatar of the room from an XMPP
|
938
|
+
client.
|
939
|
+
|
940
|
+
If the set avatar operation is completed, should return a legacy image
|
941
|
+
unique identifier. In this case the MUC avatar will be immediately
|
942
|
+
updated on the XMPP side.
|
943
|
+
|
944
|
+
If data is not None and this method returns None, then we assume that
|
945
|
+
self.set_avatar() will be called elsewhere, eg triggered by a legacy
|
946
|
+
room update event.
|
947
|
+
|
948
|
+
:param data: image data or None if the user meant to remove the avatar
|
949
|
+
:param mime: the mime type of the image. Since this is provided by
|
950
|
+
the XMPP client, there is no guarantee that this is valid or
|
951
|
+
correct.
|
952
|
+
:return: A unique avatar identifier, which will trigger
|
953
|
+
:py:meth:`slidge.group.room.LegacyMUC.set_avatar`. Alternatively, None, if
|
954
|
+
:py:meth:`.LegacyMUC.set_avatar` is meant to be awaited somewhere else.
|
955
|
+
"""
|
956
|
+
raise NotImplementedError
|
957
|
+
|
958
|
+
admin_set_avatar = deprecated("LegacyMUC.on_avatar", on_avatar)
|
959
|
+
|
960
|
+
async def on_set_affiliation(
|
961
|
+
self,
|
962
|
+
contact: "LegacyContact",
|
963
|
+
affiliation: MucAffiliation,
|
964
|
+
reason: Optional[str],
|
965
|
+
nickname: Optional[str],
|
966
|
+
):
|
967
|
+
"""
|
968
|
+
Triggered when the user requests changing the affiliation of a contact
|
969
|
+
for this group,
|
970
|
+
|
971
|
+
Examples: promotion them to moderator, kick (affiliation=none),
|
972
|
+
ban (affiliation=outcast).
|
973
|
+
|
974
|
+
:param contact: The contact whose affiliation change is requested
|
975
|
+
:param affiliation: The new affiliation
|
976
|
+
:param reason: A reason for this affiliation change
|
977
|
+
:param nickname:
|
978
|
+
"""
|
979
|
+
raise NotImplementedError
|
980
|
+
|
981
|
+
async def on_set_config(
|
982
|
+
self,
|
983
|
+
name: Optional[str],
|
984
|
+
description: Optional[str],
|
985
|
+
):
|
986
|
+
"""
|
987
|
+
Triggered when the user requests changing the room configuration.
|
988
|
+
Only title and description can be changed at the moment.
|
989
|
+
|
990
|
+
The legacy module is responsible for updating :attr:`.title` and/or
|
991
|
+
:attr:`.description` of this instance.
|
992
|
+
|
993
|
+
If :attr:`.HAS_DESCRIPTION` is set to False, description will always
|
994
|
+
be ``None``.
|
995
|
+
|
996
|
+
:param name: The new name of the room.
|
997
|
+
:param description: The new description of the room.
|
998
|
+
"""
|
999
|
+
raise NotImplementedError
|
1000
|
+
|
1001
|
+
async def on_destroy_request(self, reason: Optional[str]):
|
1002
|
+
"""
|
1003
|
+
Triggered when the user requests room destruction.
|
1004
|
+
|
1005
|
+
:param reason: Optionally, a reason for the destruction
|
1006
|
+
"""
|
1007
|
+
raise NotImplementedError
|
1008
|
+
|
1009
|
+
async def parse_mentions(self, text: str) -> list[Mention]:
|
1010
|
+
await self.__fill_participants()
|
1011
|
+
|
1012
|
+
if len(self._participants_by_nicknames) == 0:
|
1013
|
+
return []
|
1014
|
+
|
1015
|
+
result = []
|
1016
|
+
for match in re.finditer(
|
1017
|
+
"|".join(
|
1018
|
+
sorted(
|
1019
|
+
[re.escape(nick) for nick in self._participants_by_nicknames],
|
1020
|
+
key=lambda nick: len(nick),
|
1021
|
+
reverse=True,
|
1022
|
+
)
|
1023
|
+
),
|
1024
|
+
text,
|
1025
|
+
):
|
1026
|
+
span = match.span()
|
1027
|
+
nick = match.group()
|
1028
|
+
if span[0] != 0 and text[span[0] - 1] not in _WHITESPACE_OR_PUNCTUATION:
|
1029
|
+
continue
|
1030
|
+
if span[1] == len(text) or text[span[1]] in _WHITESPACE_OR_PUNCTUATION:
|
1031
|
+
participant = self._participants_by_nicknames[nick]
|
1032
|
+
if contact := participant.contact:
|
1033
|
+
result.append(Mention(contact=contact, start=span[0], end=span[1]))
|
1034
|
+
return result
|
1035
|
+
|
1036
|
+
async def on_set_subject(self, subject: str) -> None:
|
1037
|
+
"""
|
1038
|
+
Triggered when the user requests changing the room subject.
|
1039
|
+
|
1040
|
+
The legacy module is responsible for updating :attr:`.subject` of this
|
1041
|
+
instance.
|
1042
|
+
|
1043
|
+
:param subject: The new subject for this room.
|
1044
|
+
"""
|
1045
|
+
raise NotImplementedError
|
1046
|
+
|
1047
|
+
|
1048
|
+
def set_origin_id(msg: Message, origin_id: str):
|
1049
|
+
sub = ET.Element("{urn:xmpp:sid:0}origin-id")
|
1050
|
+
sub.attrib["id"] = origin_id
|
1051
|
+
msg.xml.append(sub)
|
1052
|
+
|
1053
|
+
|
1054
|
+
def int_or_none(x):
|
1055
|
+
try:
|
1056
|
+
return int(x)
|
1057
|
+
except ValueError:
|
1058
|
+
return None
|
1059
|
+
|
1060
|
+
|
1061
|
+
def equals_zero(x):
|
1062
|
+
if x is None:
|
1063
|
+
return False
|
1064
|
+
else:
|
1065
|
+
return x == 0
|
1066
|
+
|
1067
|
+
|
1068
|
+
def str_to_datetime_or_none(date: Optional[str]):
|
1069
|
+
if date is None:
|
1070
|
+
return
|
1071
|
+
try:
|
1072
|
+
return str_to_datetime(date)
|
1073
|
+
except ValueError:
|
1074
|
+
return None
|
1075
|
+
|
1076
|
+
|
1077
|
+
def bookmarks_form():
|
1078
|
+
form = Form()
|
1079
|
+
form["type"] = "submit"
|
1080
|
+
form.add_field(
|
1081
|
+
"FORM_TYPE",
|
1082
|
+
value="http://jabber.org/protocol/pubsub#publish-options",
|
1083
|
+
ftype="hidden",
|
1084
|
+
)
|
1085
|
+
form.add_field("pubsub#persist_items", value="1")
|
1086
|
+
form.add_field("pubsub#max_items", value="max")
|
1087
|
+
form.add_field("pubsub#send_last_published_item", value="never")
|
1088
|
+
form.add_field("pubsub#access_model", value="whitelist")
|
1089
|
+
return form
|
1090
|
+
|
1091
|
+
|
1092
|
+
_BOOKMARKS_OPTIONS = bookmarks_form()
|
1093
|
+
_WHITESPACE_OR_PUNCTUATION = string.whitespace + string.punctuation
|
1094
|
+
|
1095
|
+
log = logging.getLogger(__name__)
|