slidge 0.1.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 +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__)
|