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
@@ -0,0 +1,440 @@
|
|
1
|
+
import logging
|
2
|
+
import string
|
3
|
+
import stringprep
|
4
|
+
import uuid
|
5
|
+
import warnings
|
6
|
+
from copy import copy
|
7
|
+
from datetime import datetime
|
8
|
+
from functools import cached_property
|
9
|
+
from typing import TYPE_CHECKING, Optional, Union
|
10
|
+
|
11
|
+
from slixmpp import JID, InvalidJID, Message, Presence
|
12
|
+
from slixmpp.plugins.xep_0045.stanza import MUCAdminItem
|
13
|
+
from slixmpp.stringprep import StringprepError, resourceprep
|
14
|
+
from slixmpp.types import MessageTypes, OptJid
|
15
|
+
from slixmpp.util.stringprep_profiles import StringPrepError, prohibit_output
|
16
|
+
|
17
|
+
from ..contact import LegacyContact
|
18
|
+
from ..core.mixins import ChatterDiscoMixin, MessageMixin, PresenceMixin
|
19
|
+
from ..util import SubclassableOnce, strip_illegal_chars
|
20
|
+
from ..util.sql import CachedPresence
|
21
|
+
from ..util.types import (
|
22
|
+
Hat,
|
23
|
+
LegacyMessageType,
|
24
|
+
MessageOrPresenceTypeVar,
|
25
|
+
MucAffiliation,
|
26
|
+
MucRole,
|
27
|
+
)
|
28
|
+
|
29
|
+
if TYPE_CHECKING:
|
30
|
+
from .room import LegacyMUC
|
31
|
+
|
32
|
+
|
33
|
+
def strip_non_printable(nickname: str):
|
34
|
+
new = (
|
35
|
+
"".join(x for x in nickname if x in string.printable)
|
36
|
+
+ f"-slidge-{hash(nickname)}"
|
37
|
+
)
|
38
|
+
warnings.warn(f"Could not use {nickname} as a nickname, using {new}")
|
39
|
+
return new
|
40
|
+
|
41
|
+
|
42
|
+
class LegacyParticipant(
|
43
|
+
PresenceMixin,
|
44
|
+
MessageMixin,
|
45
|
+
ChatterDiscoMixin,
|
46
|
+
metaclass=SubclassableOnce,
|
47
|
+
):
|
48
|
+
"""
|
49
|
+
A legacy participant of a legacy group chat.
|
50
|
+
"""
|
51
|
+
|
52
|
+
mtype: MessageTypes = "groupchat"
|
53
|
+
_can_send_carbon = False
|
54
|
+
USE_STANZA_ID = True
|
55
|
+
STRIP_SHORT_DELAY = False
|
56
|
+
|
57
|
+
def __init__(
|
58
|
+
self,
|
59
|
+
muc: "LegacyMUC",
|
60
|
+
nickname: Optional[str] = None,
|
61
|
+
is_user=False,
|
62
|
+
is_system=False,
|
63
|
+
):
|
64
|
+
super().__init__()
|
65
|
+
self._hats = list[Hat]()
|
66
|
+
self.muc = muc
|
67
|
+
self.session = session = muc.session
|
68
|
+
self.user = session.user
|
69
|
+
self.xmpp = session.xmpp
|
70
|
+
self._role: MucRole = "participant"
|
71
|
+
self._affiliation: MucAffiliation = "member"
|
72
|
+
self.is_user: bool = is_user
|
73
|
+
self.is_system: bool = is_system
|
74
|
+
|
75
|
+
self._nickname = nickname
|
76
|
+
|
77
|
+
self.__update_jid(nickname)
|
78
|
+
log.debug("Instantiation of: %r", self)
|
79
|
+
|
80
|
+
self.contact: Optional["LegacyContact"] = None
|
81
|
+
# we track if we already sent a presence for this participant.
|
82
|
+
# if we didn't, we send it before the first message.
|
83
|
+
# this way, event in plugins that don't map "user has joined" events,
|
84
|
+
# we send a "join"-presence from the participant before the first message
|
85
|
+
self.__presence_sent = False
|
86
|
+
self.log = logging.getLogger(f"{self.user.bare_jid}:{self.jid}")
|
87
|
+
|
88
|
+
def __repr__(self):
|
89
|
+
return f"<Participant '{self.nickname}'/'{self.jid}' of '{self.muc}'>"
|
90
|
+
|
91
|
+
@property
|
92
|
+
def affiliation(self):
|
93
|
+
return self._affiliation
|
94
|
+
|
95
|
+
@affiliation.setter
|
96
|
+
def affiliation(self, affiliation: MucAffiliation):
|
97
|
+
if self._affiliation == affiliation:
|
98
|
+
return
|
99
|
+
self._affiliation = affiliation
|
100
|
+
if not self.__presence_sent:
|
101
|
+
return
|
102
|
+
self.send_last_presence(force=True, no_cache_online=True)
|
103
|
+
|
104
|
+
@property
|
105
|
+
def role(self):
|
106
|
+
return self._role
|
107
|
+
|
108
|
+
@role.setter
|
109
|
+
def role(self, role: MucRole):
|
110
|
+
if self._role == role:
|
111
|
+
return
|
112
|
+
self._role = role
|
113
|
+
if not self.__presence_sent:
|
114
|
+
return
|
115
|
+
self.send_last_presence(force=True, no_cache_online=True)
|
116
|
+
|
117
|
+
def set_hats(self, hats: list[Hat]):
|
118
|
+
if self._hats == hats:
|
119
|
+
return
|
120
|
+
self._hats = hats
|
121
|
+
if not self.__presence_sent:
|
122
|
+
return
|
123
|
+
self.send_last_presence(force=True, no_cache_online=True)
|
124
|
+
|
125
|
+
def __update_jid(self, unescaped_nickname: Optional[str]):
|
126
|
+
j: JID = copy(self.muc.jid)
|
127
|
+
|
128
|
+
if self.is_system:
|
129
|
+
self.jid = j
|
130
|
+
return
|
131
|
+
|
132
|
+
nickname = unescaped_nickname
|
133
|
+
|
134
|
+
if nickname:
|
135
|
+
nickname = self._nickname_no_illegal = strip_illegal_chars(nickname)
|
136
|
+
else:
|
137
|
+
warnings.warn(
|
138
|
+
"Only the system participant is allowed to not have a nickname"
|
139
|
+
)
|
140
|
+
nickname = f"unnamed-{uuid.uuid4()}"
|
141
|
+
|
142
|
+
assert isinstance(nickname, str)
|
143
|
+
|
144
|
+
try:
|
145
|
+
# workaround for https://codeberg.org/poezio/slixmpp/issues/3480
|
146
|
+
prohibit_output(nickname, [stringprep.in_table_a1])
|
147
|
+
resourceprep(nickname)
|
148
|
+
except (StringPrepError, StringprepError):
|
149
|
+
nickname = nickname.encode("punycode").decode()
|
150
|
+
|
151
|
+
# at this point there still might be control chars
|
152
|
+
try:
|
153
|
+
j.resource = nickname
|
154
|
+
except InvalidJID:
|
155
|
+
j.resource = strip_non_printable(nickname)
|
156
|
+
|
157
|
+
if nickname != unescaped_nickname:
|
158
|
+
self.muc._participants_by_escaped_nicknames[nickname] = self # type:ignore
|
159
|
+
|
160
|
+
self.jid = j
|
161
|
+
|
162
|
+
def send_configuration_change(self, codes: tuple[int]):
|
163
|
+
if not self.is_system:
|
164
|
+
raise RuntimeError("This is only possible for the system participant")
|
165
|
+
msg = self._make_message()
|
166
|
+
msg["muc"]["status_codes"] = codes
|
167
|
+
self._send(msg)
|
168
|
+
|
169
|
+
@property
|
170
|
+
def nickname(self):
|
171
|
+
return self._nickname
|
172
|
+
|
173
|
+
@nickname.setter
|
174
|
+
def nickname(self, new_nickname: str):
|
175
|
+
old = self._nickname
|
176
|
+
if new_nickname == old:
|
177
|
+
return
|
178
|
+
|
179
|
+
cache = getattr(self, "_last_presence", None)
|
180
|
+
if cache:
|
181
|
+
last_seen = cache.last_seen
|
182
|
+
kwargs = cache.presence_kwargs
|
183
|
+
else:
|
184
|
+
last_seen = None
|
185
|
+
kwargs = {}
|
186
|
+
|
187
|
+
kwargs["status_codes"] = {303}
|
188
|
+
|
189
|
+
p = self._make_presence(ptype="unavailable", last_seen=last_seen, **kwargs)
|
190
|
+
# in this order so pfrom=old resource and we actually use the escaped nick
|
191
|
+
# in the muc/item/nick element
|
192
|
+
self.__update_jid(new_nickname)
|
193
|
+
p["muc"]["item"]["nick"] = self.jid.resource
|
194
|
+
self._send(p)
|
195
|
+
|
196
|
+
self._nickname = new_nickname
|
197
|
+
|
198
|
+
kwargs["status_codes"] = set()
|
199
|
+
p = self._make_presence(ptype="available", last_seen=last_seen, **kwargs)
|
200
|
+
self.__add_nick_element(p)
|
201
|
+
self._send(p)
|
202
|
+
|
203
|
+
if old:
|
204
|
+
self.muc.rename_participant(old, new_nickname)
|
205
|
+
|
206
|
+
def _make_presence(
|
207
|
+
self,
|
208
|
+
*,
|
209
|
+
last_seen: Optional[datetime] = None,
|
210
|
+
status_codes: Optional[set[int]] = None,
|
211
|
+
user_full_jid: Optional[JID] = None,
|
212
|
+
**presence_kwargs,
|
213
|
+
):
|
214
|
+
p = super()._make_presence(last_seen=last_seen, **presence_kwargs)
|
215
|
+
p["muc"]["affiliation"] = self.affiliation
|
216
|
+
p["muc"]["role"] = self.role
|
217
|
+
if self._hats:
|
218
|
+
p["hats"].add_hats(self._hats)
|
219
|
+
codes = status_codes or set()
|
220
|
+
if self.is_user:
|
221
|
+
codes.add(110)
|
222
|
+
if not self.muc.is_anonymous:
|
223
|
+
if self.is_user:
|
224
|
+
if user_full_jid:
|
225
|
+
p["muc"]["jid"] = user_full_jid
|
226
|
+
else:
|
227
|
+
jid = copy(self.user.jid)
|
228
|
+
try:
|
229
|
+
jid.resource = next(
|
230
|
+
iter(self.muc.user_resources) # type:ignore
|
231
|
+
)
|
232
|
+
except StopIteration:
|
233
|
+
jid.resource = "pseudo-resource"
|
234
|
+
p["muc"]["jid"] = self.user.jid
|
235
|
+
codes.add(100)
|
236
|
+
elif self.contact:
|
237
|
+
p["muc"]["jid"] = self.contact.jid
|
238
|
+
if a := self.contact.get_avatar():
|
239
|
+
p["vcard_temp_update"]["photo"] = a.id
|
240
|
+
else:
|
241
|
+
warnings.warn(
|
242
|
+
f"Private group but no 1:1 JID associated to '{self}'",
|
243
|
+
)
|
244
|
+
if self.is_user and (hash_ := self.session.avatar_hash):
|
245
|
+
p["vcard_temp_update"]["photo"] = hash_
|
246
|
+
p["muc"]["status_codes"] = codes
|
247
|
+
return p
|
248
|
+
|
249
|
+
@property
|
250
|
+
def DISCO_NAME(self):
|
251
|
+
return self.nickname
|
252
|
+
|
253
|
+
def __send_presence_if_needed(
|
254
|
+
self, stanza: Union[Message, Presence], full_jid: JID, archive_only: bool
|
255
|
+
):
|
256
|
+
if (
|
257
|
+
archive_only
|
258
|
+
or self.is_system
|
259
|
+
or self.is_user
|
260
|
+
or self.__presence_sent
|
261
|
+
or stanza["subject"]
|
262
|
+
):
|
263
|
+
return
|
264
|
+
if isinstance(stanza, Message):
|
265
|
+
self.send_initial_presence(full_jid)
|
266
|
+
|
267
|
+
@cached_property
|
268
|
+
def __occupant_id(self):
|
269
|
+
if self.contact:
|
270
|
+
return self.contact.jid
|
271
|
+
elif self.is_user:
|
272
|
+
return "slidge-user"
|
273
|
+
elif self.is_system:
|
274
|
+
return "room"
|
275
|
+
else:
|
276
|
+
return str(uuid.uuid4())
|
277
|
+
|
278
|
+
def _send(
|
279
|
+
self,
|
280
|
+
stanza: MessageOrPresenceTypeVar,
|
281
|
+
full_jid: Optional[JID] = None,
|
282
|
+
archive_only=False,
|
283
|
+
**send_kwargs,
|
284
|
+
) -> MessageOrPresenceTypeVar:
|
285
|
+
stanza["occupant-id"]["id"] = self.__occupant_id
|
286
|
+
if isinstance(stanza, Presence):
|
287
|
+
if stanza["type"] == "unavailable" and not self.__presence_sent:
|
288
|
+
return stanza # type:ignore
|
289
|
+
self.__presence_sent = True
|
290
|
+
if full_jid:
|
291
|
+
stanza["to"] = full_jid
|
292
|
+
self.__send_presence_if_needed(stanza, full_jid, archive_only)
|
293
|
+
if self.is_user:
|
294
|
+
assert stanza.stream is not None
|
295
|
+
stanza.stream.send(stanza, use_filters=False)
|
296
|
+
else:
|
297
|
+
stanza.send()
|
298
|
+
else:
|
299
|
+
if isinstance(stanza, Message):
|
300
|
+
self.muc.archive.add(stanza, self)
|
301
|
+
if archive_only:
|
302
|
+
return stanza
|
303
|
+
for user_full_jid in self.muc.user_full_jids():
|
304
|
+
stanza = copy(stanza)
|
305
|
+
stanza["to"] = user_full_jid
|
306
|
+
self.__send_presence_if_needed(stanza, user_full_jid, archive_only)
|
307
|
+
stanza.send()
|
308
|
+
return stanza
|
309
|
+
|
310
|
+
def mucadmin_item(self):
|
311
|
+
item = MUCAdminItem()
|
312
|
+
item["nick"] = self.nickname
|
313
|
+
item["affiliation"] = self.affiliation
|
314
|
+
item["role"] = self.role
|
315
|
+
if not self.muc.is_anonymous:
|
316
|
+
if self.is_user:
|
317
|
+
item["jid"] = self.user.bare_jid
|
318
|
+
elif self.contact:
|
319
|
+
item["jid"] = self.contact.jid.bare
|
320
|
+
else:
|
321
|
+
warnings.warn(
|
322
|
+
(
|
323
|
+
f"Public group but no contact JID associated to {self.jid} in"
|
324
|
+
f" {self}"
|
325
|
+
),
|
326
|
+
)
|
327
|
+
return item
|
328
|
+
|
329
|
+
def __add_nick_element(self, p: Presence):
|
330
|
+
if (nick := self._nickname_no_illegal) != self.jid.resource:
|
331
|
+
n = self.xmpp.plugin["xep_0172"].stanza.UserNick()
|
332
|
+
n["nick"] = nick
|
333
|
+
p.append(n)
|
334
|
+
|
335
|
+
def _get_last_presence(self) -> Optional[CachedPresence]:
|
336
|
+
own = super()._get_last_presence()
|
337
|
+
if own is None and self.contact:
|
338
|
+
return self.contact._get_last_presence()
|
339
|
+
return own
|
340
|
+
|
341
|
+
def send_initial_presence(
|
342
|
+
self,
|
343
|
+
full_jid: JID,
|
344
|
+
nick_change=False,
|
345
|
+
presence_id: Optional[str] = None,
|
346
|
+
):
|
347
|
+
"""
|
348
|
+
Called when the user joins a MUC, as a mechanism
|
349
|
+
to indicate to the joining XMPP client the list of "participants".
|
350
|
+
|
351
|
+
Can be called this to trigger a "participant has joined the group" event.
|
352
|
+
|
353
|
+
:param full_jid: Set this to only send to a specific user XMPP resource.
|
354
|
+
:param nick_change: Used when the user joins and the MUC renames them (code 210)
|
355
|
+
:param presence_id: set the presence ID. used internally by slidge
|
356
|
+
"""
|
357
|
+
# MUC status codes: https://xmpp.org/extensions/xep-0045.html#registrar-statuscodes
|
358
|
+
codes = set()
|
359
|
+
if nick_change:
|
360
|
+
codes.add(210)
|
361
|
+
|
362
|
+
if self.is_user:
|
363
|
+
# the "initial presence" of the user has to be vanilla, as it is
|
364
|
+
# a crucial part of the MUC join sequence for XMPP clients.
|
365
|
+
kwargs = {}
|
366
|
+
else:
|
367
|
+
cache = self._get_last_presence()
|
368
|
+
self.log.debug("Join muc, initial presence: %s", cache)
|
369
|
+
if cache:
|
370
|
+
ptype = cache.ptype
|
371
|
+
if ptype == "unavailable":
|
372
|
+
return
|
373
|
+
kwargs = dict(
|
374
|
+
last_seen=cache.last_seen, pstatus=cache.pstatus, pshow=cache.pshow
|
375
|
+
)
|
376
|
+
else:
|
377
|
+
kwargs = {}
|
378
|
+
p = self._make_presence(
|
379
|
+
status_codes=codes,
|
380
|
+
user_full_jid=full_jid,
|
381
|
+
**kwargs, # type:ignore
|
382
|
+
)
|
383
|
+
if presence_id:
|
384
|
+
p["id"] = presence_id
|
385
|
+
self.__add_nick_element(p)
|
386
|
+
self._send(p, full_jid)
|
387
|
+
|
388
|
+
def leave(self):
|
389
|
+
"""
|
390
|
+
Call this when the participant leaves the room
|
391
|
+
"""
|
392
|
+
self.muc.remove_participant(self)
|
393
|
+
|
394
|
+
def kick(self):
|
395
|
+
"""
|
396
|
+
Call this when the participant is kicked from the room
|
397
|
+
"""
|
398
|
+
self.muc.remove_participant(self, kick=True)
|
399
|
+
|
400
|
+
def ban(self):
|
401
|
+
"""
|
402
|
+
Call this when the participant is banned from the room
|
403
|
+
"""
|
404
|
+
self.muc.remove_participant(self, ban=True)
|
405
|
+
|
406
|
+
def get_disco_info(self, jid: OptJid = None, node: Optional[str] = None):
|
407
|
+
if self.contact is not None:
|
408
|
+
return self.contact.get_disco_info()
|
409
|
+
return super().get_disco_info()
|
410
|
+
|
411
|
+
def moderate(self, legacy_msg_id: LegacyMessageType, reason: Optional[str] = None):
|
412
|
+
m = self.muc.get_system_participant()._make_message()
|
413
|
+
m["apply_to"]["id"] = self._legacy_to_xmpp(legacy_msg_id)
|
414
|
+
m["apply_to"]["moderated"].enable("retract")
|
415
|
+
m["apply_to"]["moderated"]["by"] = self.jid
|
416
|
+
if reason:
|
417
|
+
m["apply_to"]["moderated"]["reason"] = reason
|
418
|
+
self._send(m)
|
419
|
+
|
420
|
+
def set_room_subject(
|
421
|
+
self,
|
422
|
+
subject: str,
|
423
|
+
full_jid: Optional[JID] = None,
|
424
|
+
when: Optional[datetime] = None,
|
425
|
+
update_muc=True,
|
426
|
+
):
|
427
|
+
if update_muc:
|
428
|
+
self.muc._subject = subject # type: ignore
|
429
|
+
self.muc.subject_setter = self
|
430
|
+
self.muc.subject_date = when
|
431
|
+
|
432
|
+
msg = self._make_message()
|
433
|
+
if when is not None:
|
434
|
+
msg["delay"].set_stamp(when)
|
435
|
+
msg["delay"]["from"] = self.muc.jid
|
436
|
+
msg["subject"] = subject or str(self.muc.name)
|
437
|
+
self._send(msg, full_jid)
|
438
|
+
|
439
|
+
|
440
|
+
log = logging.getLogger(__name__)
|