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.
Files changed (96) hide show
  1. slidge/__init__.py +61 -0
  2. slidge/__main__.py +192 -0
  3. slidge/command/__init__.py +28 -0
  4. slidge/command/adhoc.py +258 -0
  5. slidge/command/admin.py +193 -0
  6. slidge/command/base.py +441 -0
  7. slidge/command/categories.py +3 -0
  8. slidge/command/chat_command.py +288 -0
  9. slidge/command/register.py +179 -0
  10. slidge/command/user.py +250 -0
  11. slidge/contact/__init__.py +8 -0
  12. slidge/contact/contact.py +452 -0
  13. slidge/contact/roster.py +192 -0
  14. slidge/core/__init__.py +3 -0
  15. slidge/core/cache.py +183 -0
  16. slidge/core/config.py +209 -0
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +892 -0
  19. slidge/core/gateway/caps.py +63 -0
  20. slidge/core/gateway/delivery_receipt.py +52 -0
  21. slidge/core/gateway/disco.py +80 -0
  22. slidge/core/gateway/mam.py +75 -0
  23. slidge/core/gateway/muc_admin.py +35 -0
  24. slidge/core/gateway/ping.py +66 -0
  25. slidge/core/gateway/presence.py +95 -0
  26. slidge/core/gateway/registration.py +53 -0
  27. slidge/core/gateway/search.py +102 -0
  28. slidge/core/gateway/session_dispatcher.py +757 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +19 -0
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +31 -0
  34. slidge/core/mixins/disco.py +130 -0
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +398 -0
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +217 -0
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +525 -0
  41. slidge/core/session.py +752 -0
  42. slidge/group/__init__.py +10 -0
  43. slidge/group/archive.py +125 -0
  44. slidge/group/bookmarks.py +163 -0
  45. slidge/group/participant.py +440 -0
  46. slidge/group/room.py +1095 -0
  47. slidge/migration.py +18 -0
  48. slidge/py.typed +0 -0
  49. slidge/slixfix/__init__.py +68 -0
  50. slidge/slixfix/link_preview/__init__.py +10 -0
  51. slidge/slixfix/link_preview/link_preview.py +17 -0
  52. slidge/slixfix/link_preview/stanza.py +99 -0
  53. slidge/slixfix/roster.py +60 -0
  54. slidge/slixfix/xep_0077/__init__.py +10 -0
  55. slidge/slixfix/xep_0077/register.py +289 -0
  56. slidge/slixfix/xep_0077/stanza.py +104 -0
  57. slidge/slixfix/xep_0100/__init__.py +5 -0
  58. slidge/slixfix/xep_0100/gateway.py +121 -0
  59. slidge/slixfix/xep_0100/stanza.py +9 -0
  60. slidge/slixfix/xep_0153/__init__.py +10 -0
  61. slidge/slixfix/xep_0153/stanza.py +25 -0
  62. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  63. slidge/slixfix/xep_0264/__init__.py +5 -0
  64. slidge/slixfix/xep_0264/stanza.py +36 -0
  65. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  66. slidge/slixfix/xep_0292/__init__.py +5 -0
  67. slidge/slixfix/xep_0292/vcard4.py +100 -0
  68. slidge/slixfix/xep_0313/__init__.py +12 -0
  69. slidge/slixfix/xep_0313/mam.py +262 -0
  70. slidge/slixfix/xep_0313/stanza.py +359 -0
  71. slidge/slixfix/xep_0317/__init__.py +5 -0
  72. slidge/slixfix/xep_0317/hats.py +17 -0
  73. slidge/slixfix/xep_0317/stanza.py +28 -0
  74. slidge/slixfix/xep_0356_old/__init__.py +7 -0
  75. slidge/slixfix/xep_0356_old/privilege.py +167 -0
  76. slidge/slixfix/xep_0356_old/stanza.py +44 -0
  77. slidge/slixfix/xep_0424/__init__.py +9 -0
  78. slidge/slixfix/xep_0424/retraction.py +77 -0
  79. slidge/slixfix/xep_0424/stanza.py +28 -0
  80. slidge/slixfix/xep_0490/__init__.py +8 -0
  81. slidge/slixfix/xep_0490/mds.py +47 -0
  82. slidge/slixfix/xep_0490/stanza.py +17 -0
  83. slidge/util/__init__.py +15 -0
  84. slidge/util/archive_msg.py +61 -0
  85. slidge/util/conf.py +206 -0
  86. slidge/util/db.py +229 -0
  87. slidge/util/schema.sql +126 -0
  88. slidge/util/sql.py +508 -0
  89. slidge/util/test.py +295 -0
  90. slidge/util/types.py +180 -0
  91. slidge/util/util.py +295 -0
  92. slidge-0.1.0.dist-info/LICENSE +661 -0
  93. slidge-0.1.0.dist-info/METADATA +109 -0
  94. slidge-0.1.0.dist-info/RECORD +96 -0
  95. slidge-0.1.0.dist-info/WHEEL +4 -0
  96. 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__)