slidge 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. slidge/__init__.py +3 -5
  2. slidge/__main__.py +2 -197
  3. slidge/__version__.py +5 -0
  4. slidge/command/adhoc.py +40 -17
  5. slidge/command/admin.py +24 -12
  6. slidge/command/base.py +10 -8
  7. slidge/command/categories.py +13 -3
  8. slidge/command/chat_command.py +29 -2
  9. slidge/command/register.py +32 -16
  10. slidge/command/user.py +106 -13
  11. slidge/contact/contact.py +254 -50
  12. slidge/contact/roster.py +124 -53
  13. slidge/core/config.py +19 -13
  14. slidge/core/dispatcher/__init__.py +3 -0
  15. slidge/core/{gateway → dispatcher}/caps.py +12 -8
  16. slidge/core/{gateway → dispatcher}/disco.py +10 -18
  17. slidge/core/dispatcher/message/__init__.py +10 -0
  18. slidge/core/dispatcher/message/chat_state.py +40 -0
  19. slidge/core/dispatcher/message/marker.py +62 -0
  20. slidge/core/dispatcher/message/message.py +397 -0
  21. slidge/core/dispatcher/muc/__init__.py +12 -0
  22. slidge/core/dispatcher/muc/admin.py +98 -0
  23. slidge/core/{gateway → dispatcher/muc}/mam.py +25 -17
  24. slidge/core/dispatcher/muc/misc.py +121 -0
  25. slidge/core/dispatcher/muc/owner.py +96 -0
  26. slidge/core/{gateway → dispatcher/muc}/ping.py +11 -17
  27. slidge/core/dispatcher/presence.py +176 -0
  28. slidge/core/dispatcher/registration.py +85 -0
  29. slidge/core/{gateway → dispatcher}/search.py +9 -16
  30. slidge/core/dispatcher/session_dispatcher.py +84 -0
  31. slidge/core/dispatcher/util.py +174 -0
  32. slidge/core/{gateway/vcard_temp.py → dispatcher/vcard.py} +35 -19
  33. slidge/core/{gateway/base.py → gateway.py} +176 -153
  34. slidge/core/mixins/__init__.py +11 -1
  35. slidge/core/mixins/attachment.py +106 -67
  36. slidge/core/mixins/avatar.py +94 -25
  37. slidge/core/mixins/base.py +10 -4
  38. slidge/core/mixins/db.py +18 -0
  39. slidge/core/mixins/disco.py +0 -10
  40. slidge/core/mixins/lock.py +10 -8
  41. slidge/core/mixins/message.py +11 -195
  42. slidge/core/mixins/message_maker.py +17 -9
  43. slidge/core/mixins/message_text.py +211 -0
  44. slidge/core/mixins/presence.py +17 -4
  45. slidge/core/pubsub.py +114 -288
  46. slidge/core/session.py +101 -40
  47. slidge/db/__init__.py +4 -0
  48. slidge/db/alembic/__init__.py +0 -0
  49. slidge/db/alembic/env.py +64 -0
  50. slidge/db/alembic/old_user_store.py +183 -0
  51. slidge/db/alembic/script.py.mako +26 -0
  52. slidge/db/alembic/versions/09f27f098baa_add_missing_attributes_in_room.py +36 -0
  53. slidge/db/alembic/versions/15b0bd83407a_remove_bogus_unique_constraints_on_room_.py +85 -0
  54. slidge/db/alembic/versions/2461390c0af2_store_contacts_caps_verstring_in_db.py +36 -0
  55. slidge/db/alembic/versions/29f5280c61aa_store_subject_setter_in_room.py +37 -0
  56. slidge/db/alembic/versions/2b1f45ab7379_store_room_subject_setter_by_nickname.py +41 -0
  57. slidge/db/alembic/versions/3071e0fa69d4_add_contact_client_type.py +52 -0
  58. slidge/db/alembic/versions/45c24cc73c91_add_bob.py +42 -0
  59. slidge/db/alembic/versions/5bd48bfdffa2_lift_room_legacy_id_constraint.py +61 -0
  60. slidge/db/alembic/versions/82a4af84b679_add_muc_history_filled.py +48 -0
  61. slidge/db/alembic/versions/8b993243a536_add_vcard_content_to_contact_table.py +43 -0
  62. slidge/db/alembic/versions/8d2ced764698_rely_on_db_to_store_contacts_rooms_and_.py +139 -0
  63. slidge/db/alembic/versions/aa9d82a7f6ef_db_creation.py +101 -0
  64. slidge/db/alembic/versions/abba1ae0edb3_store_avatar_legacy_id_in_the_contact_.py +79 -0
  65. slidge/db/alembic/versions/b33993e87db3_move_everything_to_persistent_db.py +214 -0
  66. slidge/db/alembic/versions/b64b1a793483_add_source_and_legacy_id_for_archived_.py +52 -0
  67. slidge/db/alembic/versions/c4a8ec35a0e8_per_room_user_nick.py +34 -0
  68. slidge/db/alembic/versions/e91195719c2c_store_users_avatars_persistently.py +26 -0
  69. slidge/db/avatar.py +205 -0
  70. slidge/db/meta.py +72 -0
  71. slidge/db/models.py +405 -0
  72. slidge/db/store.py +1257 -0
  73. slidge/group/archive.py +58 -14
  74. slidge/group/bookmarks.py +89 -65
  75. slidge/group/participant.py +107 -40
  76. slidge/group/room.py +402 -213
  77. slidge/main.py +202 -0
  78. slidge/migration.py +45 -1
  79. slidge/slixfix/__init__.py +31 -1
  80. slidge/{core/gateway → slixfix}/delivery_receipt.py +1 -1
  81. slidge/slixfix/roster.py +13 -4
  82. slidge/slixfix/xep_0292/vcard4.py +1 -87
  83. slidge/util/archive_msg.py +2 -1
  84. slidge/util/db.py +4 -228
  85. slidge/util/test.py +91 -4
  86. slidge/util/types.py +39 -4
  87. slidge/util/util.py +45 -2
  88. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/METADATA +10 -5
  89. slidge-0.2.0.dist-info/RECORD +131 -0
  90. slidge-0.2.0.dist-info/entry_points.txt +3 -0
  91. slidge/core/cache.py +0 -183
  92. slidge/core/gateway/__init__.py +0 -3
  93. slidge/core/gateway/muc_admin.py +0 -35
  94. slidge/core/gateway/presence.py +0 -95
  95. slidge/core/gateway/registration.py +0 -53
  96. slidge/core/gateway/session_dispatcher.py +0 -804
  97. slidge/util/schema.sql +0 -126
  98. slidge/util/sql.py +0 -508
  99. slidge-0.1.3.dist-info/RECORD +0 -96
  100. slidge-0.1.3.dist-info/entry_points.txt +0 -3
  101. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/LICENSE +0 -0
  102. {slidge-0.1.3.dist-info → slidge-0.2.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,397 @@
1
+ import logging
2
+ from copy import copy
3
+ from xml.etree import ElementTree
4
+
5
+ from slixmpp import JID, Message
6
+ from slixmpp.exceptions import XMPPError
7
+
8
+ from ....contact.contact import LegacyContact
9
+ from ....group.participant import LegacyParticipant
10
+ from ....group.room import LegacyMUC
11
+ from ....util.types import LinkPreview, Recipient
12
+ from ....util.util import dict_to_named_tuple, remove_emoji_variation_selector_16
13
+ from ... import config
14
+ from ...session import BaseSession
15
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
16
+
17
+
18
+ class MessageContentMixin(DispatcherMixin):
19
+ def __init__(self, xmpp):
20
+ super().__init__(xmpp)
21
+ xmpp.add_event_handler("legacy_message", self.on_legacy_message)
22
+ xmpp.add_event_handler("message_correction", self.on_message_correction)
23
+ xmpp.add_event_handler("message_retract", self.on_message_retract)
24
+ xmpp.add_event_handler("groupchat_message", self.on_groupchat_message)
25
+ xmpp.add_event_handler("reactions", self.on_reactions)
26
+
27
+ async def on_groupchat_message(self, msg: Message) -> None:
28
+ await self.on_legacy_message(msg)
29
+
30
+ @exceptions_to_xmpp_errors
31
+ async def on_legacy_message(self, msg: Message):
32
+ """
33
+ Meant to be called from :class:`BaseGateway` only.
34
+
35
+ :param msg:
36
+ :return:
37
+ """
38
+ # we MUST not use `if m["replace"]["id"]` because it adds the tag if not
39
+ # present. this is a problem for MUC echoed messages
40
+ if msg.get_plugin("replace", check=True) is not None:
41
+ # ignore last message correction (handled by a specific method)
42
+ return
43
+ if msg.get_plugin("apply_to", check=True) is not None:
44
+ # ignore message retraction (handled by a specific method)
45
+ return
46
+ if msg.get_plugin("reactions", check=True) is not None:
47
+ # ignore message reaction fallback.
48
+ # the reaction itself is handled by self.react_from_msg().
49
+ return
50
+ if msg.get_plugin("retract", check=True) is not None:
51
+ # ignore message retraction fallback.
52
+ # the retraction itself is handled by self.on_retract
53
+ return
54
+ cid = None
55
+ if msg.get_plugin("html", check=True) is not None:
56
+ body = ElementTree.fromstring("<body>" + msg["html"].get_body() + "</body>")
57
+ p = body.findall("p")
58
+ if p is not None and len(p) == 1:
59
+ if p[0].text is None or not p[0].text.strip():
60
+ images = p[0].findall("img")
61
+ if len(images) == 1:
62
+ # no text, single img ⇒ this is a sticker
63
+ # other cases should be interpreted as "custom emojis" in text
64
+ src = images[0].get("src")
65
+ if src is not None and src.startswith("cid:"):
66
+ cid = src.removeprefix("cid:")
67
+
68
+ session, entity, thread = await self._get_session_entity_thread(msg)
69
+
70
+ if msg.get_plugin("oob", check=True) is not None:
71
+ url = msg["oob"]["url"]
72
+ else:
73
+ url = None
74
+
75
+ if msg.get_plugin("reply", check=True):
76
+ text, reply_to_msg_id, reply_to, reply_fallback = await self.__get_reply(
77
+ msg, session, entity
78
+ )
79
+ else:
80
+ text = msg["body"]
81
+ reply_to_msg_id = None
82
+ reply_to = None
83
+ reply_fallback = None
84
+
85
+ if msg.get_plugin("link_previews", check=True):
86
+ link_previews = [
87
+ dict_to_named_tuple(p, LinkPreview) for p in msg["link_previews"]
88
+ ]
89
+ else:
90
+ link_previews = []
91
+
92
+ if url:
93
+ legacy_msg_id = await self.__send_url(
94
+ url,
95
+ session,
96
+ entity,
97
+ reply_to_msg_id=reply_to_msg_id,
98
+ reply_to_fallback_text=reply_fallback,
99
+ reply_to=reply_to,
100
+ thread=thread,
101
+ )
102
+ elif cid:
103
+ legacy_msg_id = await self.__send_bob(
104
+ msg.get_from(),
105
+ cid,
106
+ session,
107
+ entity,
108
+ reply_to_msg_id=reply_to_msg_id,
109
+ reply_to_fallback_text=reply_fallback,
110
+ reply_to=reply_to,
111
+ thread=thread,
112
+ )
113
+ elif text:
114
+ if isinstance(entity, LegacyMUC):
115
+ mentions = {"mentions": await entity.parse_mentions(text)}
116
+ else:
117
+ mentions = {}
118
+ legacy_msg_id = await session.on_text(
119
+ entity,
120
+ text,
121
+ reply_to_msg_id=reply_to_msg_id,
122
+ reply_to_fallback_text=reply_fallback,
123
+ reply_to=reply_to,
124
+ thread=thread,
125
+ link_previews=link_previews,
126
+ **mentions,
127
+ )
128
+ else:
129
+ log.debug("Ignoring %s", msg.get_id())
130
+ return
131
+
132
+ if isinstance(entity, LegacyMUC):
133
+ await entity.echo(msg, legacy_msg_id)
134
+ if legacy_msg_id is not None:
135
+ self.xmpp.store.sent.set_group_message(
136
+ session.user_pk, str(legacy_msg_id), msg.get_id()
137
+ )
138
+ else:
139
+ self.__ack(msg)
140
+ if legacy_msg_id is not None:
141
+ self.xmpp.store.sent.set_message(
142
+ session.user_pk, str(legacy_msg_id), msg.get_id()
143
+ )
144
+ if session.MESSAGE_IDS_ARE_THREAD_IDS and (t := msg["thread"]):
145
+ self.xmpp.store.sent.set_thread(
146
+ session.user_pk, t, str(legacy_msg_id)
147
+ )
148
+
149
+ @exceptions_to_xmpp_errors
150
+ async def on_message_correction(self, msg: Message):
151
+ if msg.get_plugin("retract", check=True) is not None:
152
+ # ignore message retraction fallback (fallback=last msg correction)
153
+ return
154
+ session, entity, thread = await self._get_session_entity_thread(msg)
155
+ xmpp_id = msg["replace"]["id"]
156
+ if isinstance(entity, LegacyMUC):
157
+ legacy_id_str = self.xmpp.store.sent.get_group_legacy_id(
158
+ session.user_pk, xmpp_id
159
+ )
160
+ if legacy_id_str is None:
161
+ legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
162
+ else:
163
+ legacy_id = self.xmpp.LEGACY_MSG_ID_TYPE(legacy_id_str)
164
+ else:
165
+ legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
166
+
167
+ if isinstance(entity, LegacyMUC):
168
+ mentions = await entity.parse_mentions(msg["body"])
169
+ else:
170
+ mentions = None
171
+
172
+ if previews := msg["link_previews"]:
173
+ link_previews = [dict_to_named_tuple(p, LinkPreview) for p in previews]
174
+ else:
175
+ link_previews = []
176
+
177
+ if legacy_id is None:
178
+ log.debug("Did not find legacy ID to correct")
179
+ new_legacy_msg_id = await session.on_text(
180
+ entity,
181
+ "Correction:" + msg["body"],
182
+ thread=thread,
183
+ mentions=mentions,
184
+ link_previews=link_previews,
185
+ )
186
+ elif (
187
+ not msg["body"].strip()
188
+ and config.CORRECTION_EMPTY_BODY_AS_RETRACTION
189
+ and entity.RETRACTION
190
+ ):
191
+ await session.on_retract(entity, legacy_id, thread=thread)
192
+ new_legacy_msg_id = None
193
+ elif entity.CORRECTION:
194
+ new_legacy_msg_id = await session.on_correct(
195
+ entity,
196
+ msg["body"],
197
+ legacy_id,
198
+ thread=thread,
199
+ mentions=mentions,
200
+ link_previews=link_previews,
201
+ )
202
+ else:
203
+ session.send_gateway_message(
204
+ "Last message correction is not supported by this legacy service. "
205
+ "Slidge will send your correction as new message."
206
+ )
207
+ if (
208
+ config.LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND
209
+ and entity.RETRACTION
210
+ and legacy_id is not None
211
+ ):
212
+ if legacy_id is not None:
213
+ session.send_gateway_message(
214
+ "Slidge will attempt to retract the original message you wanted"
215
+ " to edit."
216
+ )
217
+ await session.on_retract(entity, legacy_id, thread=thread)
218
+
219
+ new_legacy_msg_id = await session.on_text(
220
+ entity,
221
+ "Correction: " + msg["body"],
222
+ thread=thread,
223
+ mentions=mentions,
224
+ link_previews=link_previews,
225
+ )
226
+
227
+ if isinstance(entity, LegacyMUC):
228
+ if new_legacy_msg_id is not None:
229
+ self.xmpp.store.sent.set_group_message(
230
+ session.user_pk, new_legacy_msg_id, msg.get_id()
231
+ )
232
+ await entity.echo(msg, new_legacy_msg_id)
233
+ else:
234
+ self.__ack(msg)
235
+ if new_legacy_msg_id is not None:
236
+ self.xmpp.store.sent.set_message(
237
+ session.user_pk, new_legacy_msg_id, msg.get_id()
238
+ )
239
+
240
+ @exceptions_to_xmpp_errors
241
+ async def on_message_retract(self, msg: Message):
242
+ session, entity, thread = await self._get_session_entity_thread(msg)
243
+ if not entity.RETRACTION:
244
+ raise XMPPError(
245
+ "bad-request",
246
+ "This legacy service does not support message retraction.",
247
+ )
248
+ xmpp_id: str = msg["retract"]["id"]
249
+ legacy_id = self._xmpp_msg_id_to_legacy(session, xmpp_id)
250
+ if legacy_id:
251
+ await session.on_retract(entity, legacy_id, thread=thread)
252
+ if isinstance(entity, LegacyMUC):
253
+ await entity.echo(msg, None)
254
+ else:
255
+ log.debug("Ignored retraction from user")
256
+ self.__ack(msg)
257
+
258
+ @exceptions_to_xmpp_errors
259
+ async def on_reactions(self, msg: Message):
260
+ session, entity, thread = await self._get_session_entity_thread(msg)
261
+ react_to: str = msg["reactions"]["id"]
262
+
263
+ special_msg = session.SPECIAL_MSG_ID_PREFIX and react_to.startswith(
264
+ session.SPECIAL_MSG_ID_PREFIX
265
+ )
266
+
267
+ if special_msg:
268
+ legacy_id = react_to
269
+ else:
270
+ legacy_id = self._xmpp_msg_id_to_legacy(session, react_to)
271
+
272
+ if not legacy_id:
273
+ log.debug("Ignored reaction from user")
274
+ raise XMPPError(
275
+ "internal-server-error",
276
+ "Could not convert the XMPP msg ID to a legacy ID",
277
+ )
278
+
279
+ emojis = [
280
+ remove_emoji_variation_selector_16(r["value"]) for r in msg["reactions"]
281
+ ]
282
+ error_msg = None
283
+ entity = entity
284
+
285
+ if not special_msg:
286
+ if entity.REACTIONS_SINGLE_EMOJI and len(emojis) > 1:
287
+ error_msg = "Maximum 1 emoji/message"
288
+
289
+ if not error_msg and (subset := await entity.available_emojis(legacy_id)):
290
+ if not set(emojis).issubset(subset):
291
+ error_msg = f"You can only react with the following emojis: {''.join(subset)}"
292
+
293
+ if error_msg:
294
+ session.send_gateway_message(error_msg)
295
+ if not isinstance(entity, LegacyMUC):
296
+ # no need to carbon for groups, we just don't echo the stanza
297
+ entity.react(legacy_id, carbon=True) # type: ignore
298
+ await session.on_react(entity, legacy_id, [], thread=thread)
299
+ raise XMPPError("not-acceptable", text=error_msg)
300
+
301
+ await session.on_react(entity, legacy_id, emojis, thread=thread)
302
+ if isinstance(entity, LegacyMUC):
303
+ await entity.echo(msg, None)
304
+ else:
305
+ self.__ack(msg)
306
+
307
+ multi = self.xmpp.store.multi.get_xmpp_ids(session.user_pk, react_to)
308
+ if not multi:
309
+ return
310
+ multi = [m for m in multi if react_to != m]
311
+
312
+ if isinstance(entity, LegacyMUC):
313
+ for xmpp_id in multi:
314
+ mc = copy(msg)
315
+ mc["reactions"]["id"] = xmpp_id
316
+ await entity.echo(mc)
317
+ elif isinstance(entity, LegacyContact):
318
+ for xmpp_id in multi:
319
+ entity.react(legacy_id, emojis, xmpp_id=xmpp_id, carbon=True)
320
+
321
+ def __ack(self, msg: Message):
322
+ if not self.xmpp.PROPER_RECEIPTS:
323
+ self.xmpp.delivery_receipt.ack(msg)
324
+
325
+ async def __get_reply(
326
+ self, msg: Message, session: BaseSession, entity: Recipient
327
+ ) -> tuple[
328
+ str, str | int | None, LegacyContact | LegacyParticipant | None, str | None
329
+ ]:
330
+ try:
331
+ reply_to_msg_id = self._xmpp_msg_id_to_legacy(session, msg["reply"]["id"])
332
+ except XMPPError:
333
+ session.log.debug(
334
+ "Could not determine reply-to legacy msg ID, sending quote instead."
335
+ )
336
+ return msg["body"], None, None, None
337
+
338
+ reply_to_jid = JID(msg["reply"]["to"])
339
+ reply_to = None
340
+ if msg["type"] == "chat":
341
+ if reply_to_jid.bare != session.user_jid.bare:
342
+ try:
343
+ reply_to = await session.contacts.by_jid(reply_to_jid)
344
+ except XMPPError:
345
+ pass
346
+ elif msg["type"] == "groupchat":
347
+ nick = reply_to_jid.resource
348
+ try:
349
+ muc = await session.bookmarks.by_jid(reply_to_jid)
350
+ except XMPPError:
351
+ pass
352
+ else:
353
+ if nick != muc.user_nick:
354
+ reply_to = await muc.get_participant(
355
+ reply_to_jid.resource, store=False
356
+ )
357
+
358
+ if msg.get_plugin("fallback", check=True) and (
359
+ isinstance(entity, LegacyMUC) or entity.REPLIES
360
+ ):
361
+ text = msg["fallback"].get_stripped_body(self.xmpp["xep_0461"].namespace)
362
+ try:
363
+ reply_fallback = msg["reply"].get_fallback_body()
364
+ except AttributeError:
365
+ reply_fallback = None
366
+ else:
367
+ text = msg["body"]
368
+ reply_fallback = None
369
+
370
+ return text, reply_to_msg_id, reply_to, reply_fallback
371
+
372
+ async def __send_url(
373
+ self, url: str, session: BaseSession, entity: Recipient, **kwargs
374
+ ) -> int | str | None:
375
+ async with self.xmpp.http.get(url) as response:
376
+ if response.status >= 400:
377
+ session.log.warning(
378
+ "OOB url cannot be downloaded: %s, sending the URL as text"
379
+ " instead.",
380
+ response,
381
+ )
382
+ return await session.on_text(entity, url, **kwargs)
383
+
384
+ return await session.on_file(entity, url, http_response=response, **kwargs)
385
+
386
+ async def __send_bob(
387
+ self, from_: JID, cid: str, session: BaseSession, entity: Recipient, **kwargs
388
+ ) -> int | str | None:
389
+ sticker = self.xmpp.store.bob.get_sticker(cid)
390
+ if sticker is None:
391
+ await self.xmpp.plugin["xep_0231"].get_bob(from_, cid)
392
+ sticker = self.xmpp.store.bob.get_sticker(cid)
393
+ assert sticker is not None
394
+ return await session.on_sticker(entity, sticker, **kwargs)
395
+
396
+
397
+ log = logging.getLogger(__name__)
@@ -0,0 +1,12 @@
1
+ from .admin import MucAdminMixin
2
+ from .mam import MamMixin
3
+ from .misc import MucMiscMixin
4
+ from .owner import MucOwnerMixin
5
+ from .ping import PingMixin
6
+
7
+
8
+ class MucMixin(PingMixin, MamMixin, MucAdminMixin, MucOwnerMixin, MucMiscMixin):
9
+ pass
10
+
11
+
12
+ __all__ = ("MucMixin",)
@@ -0,0 +1,98 @@
1
+ from slixmpp import JID, CoroutineCallback, Iq, StanzaPath
2
+ from slixmpp.exceptions import XMPPError
3
+ from slixmpp.xmlstream import StanzaBase
4
+
5
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
6
+
7
+
8
+ class MucAdminMixin(DispatcherMixin):
9
+ def __init__(self, xmpp) -> None:
10
+ super().__init__(xmpp)
11
+ self.xmpp.register_handler(
12
+ CoroutineCallback(
13
+ "MUCModerate",
14
+ StanzaPath("iq/apply_to/moderate"),
15
+ self.on_user_moderation,
16
+ )
17
+ )
18
+ self.xmpp.register_handler(
19
+ CoroutineCallback(
20
+ "MUCSetAffiliation",
21
+ StanzaPath("iq@type=set/mucadmin_query"),
22
+ self.on_user_set_affiliation,
23
+ )
24
+ )
25
+ self.xmpp.register_handler(
26
+ CoroutineCallback(
27
+ "MUCGetAffiliation",
28
+ StanzaPath("iq@type=get/mucadmin_query"),
29
+ self.on_muc_admin_query_get,
30
+ )
31
+ )
32
+
33
+ @exceptions_to_xmpp_errors
34
+ async def on_user_moderation(self, iq: StanzaBase) -> None:
35
+ assert isinstance(iq, Iq)
36
+ muc = await self.get_muc_from_stanza(iq)
37
+
38
+ apply_to = iq["apply_to"]
39
+ xmpp_id = apply_to["id"]
40
+ if not xmpp_id:
41
+ raise XMPPError("bad-request", "Missing moderated message ID")
42
+
43
+ moderate = apply_to["moderate"]
44
+ if not moderate["retract"]:
45
+ raise XMPPError(
46
+ "feature-not-implemented",
47
+ "Slidge only implements moderation/retraction",
48
+ )
49
+
50
+ legacy_id = self._xmpp_msg_id_to_legacy(muc.session, xmpp_id)
51
+ await muc.session.on_moderate(muc, legacy_id, moderate["reason"] or None)
52
+ iq.reply(clear=True).send()
53
+
54
+ @exceptions_to_xmpp_errors
55
+ async def on_user_set_affiliation(self, iq: StanzaBase) -> None:
56
+ assert isinstance(iq, Iq)
57
+ muc = await self.get_muc_from_stanza(iq)
58
+
59
+ item = iq["mucadmin_query"]["item"]
60
+ if item["jid"]:
61
+ contact = await muc.session.contacts.by_jid(JID(item["jid"]))
62
+ else:
63
+ part = await muc.get_participant(
64
+ item["nick"], fill_first=True, raise_if_not_found=True
65
+ )
66
+ assert part.contact is not None
67
+ contact = part.contact
68
+
69
+ if item["affiliation"]:
70
+ await muc.on_set_affiliation(
71
+ contact,
72
+ item["affiliation"],
73
+ item["reason"] or None,
74
+ item["nick"] or None,
75
+ )
76
+ elif item["role"] == "none":
77
+ await muc.on_kick(contact, item["reason"] or None)
78
+
79
+ iq.reply(clear=True).send()
80
+
81
+ @exceptions_to_xmpp_errors
82
+ async def on_muc_admin_query_get(self, iq: StanzaBase) -> None:
83
+ assert isinstance(iq, Iq)
84
+ affiliation = iq["mucadmin_query"]["item"]["affiliation"]
85
+
86
+ if not affiliation:
87
+ raise XMPPError("bad-request")
88
+
89
+ session = await self._get_session(iq, 1, logged=True)
90
+ muc = await session.bookmarks.by_jid(iq.get_to())
91
+
92
+ reply = iq.reply()
93
+ reply.enable("mucadmin_query")
94
+ async for participant in muc.get_participants():
95
+ if not participant.affiliation == affiliation:
96
+ continue
97
+ reply["mucadmin_query"].append(participant.mucadmin_item())
98
+ reply.send()
@@ -1,44 +1,57 @@
1
+ import asyncio
1
2
  from typing import TYPE_CHECKING
2
3
 
3
4
  from slixmpp import CoroutineCallback, Iq, StanzaPath
4
5
  from slixmpp.exceptions import XMPPError
6
+ from slixmpp.xmlstream import StanzaBase
5
7
 
6
- from ...util.db import user_store
8
+ from ... import config
9
+ from ..util import DispatcherMixin, exceptions_to_xmpp_errors
7
10
 
8
11
  if TYPE_CHECKING:
9
- from .base import BaseGateway
12
+ from slidge.core.gateway import BaseGateway
10
13
 
11
14
 
12
- class Mam:
15
+ class MamMixin(DispatcherMixin):
13
16
  def __init__(self, xmpp: "BaseGateway"):
14
- self.xmpp = xmpp
17
+ super().__init__(xmpp)
18
+ self.__mam_cleanup_task = xmpp.loop.create_task(self.__mam_cleanup())
15
19
  xmpp.register_handler(
16
20
  CoroutineCallback(
17
21
  "MAM_query",
18
22
  StanzaPath("iq@type=set/mam"),
19
- self.__handle_mam, # type:ignore
23
+ self.__handle_mam,
20
24
  )
21
25
  )
22
26
  xmpp.register_handler(
23
27
  CoroutineCallback(
24
28
  "MAM_get_from",
25
29
  StanzaPath("iq@type=get/mam"),
26
- self.__handle_mam_get_form, # type:ignore
30
+ self.__handle_mam_get_form,
27
31
  )
28
32
  )
29
33
  xmpp.register_handler(
30
34
  CoroutineCallback(
31
35
  "MAM_get_meta",
32
36
  StanzaPath("iq@type=get/mam_metadata"),
33
- self.__handle_mam_metadata, # type:ignore
37
+ self.__handle_mam_metadata,
34
38
  )
35
39
  )
36
40
 
41
+ async def __mam_cleanup(self):
42
+ if not config.MAM_MAX_DAYS:
43
+ return
44
+ while True:
45
+ await asyncio.sleep(3600 * 6)
46
+ self.xmpp.store.mam.nuke_older_than(config.MAM_MAX_DAYS)
47
+
48
+ @exceptions_to_xmpp_errors
37
49
  async def __handle_mam(self, iq: Iq):
38
- muc = await self.xmpp.get_muc_from_stanza(iq)
50
+ muc = await self.get_muc_from_stanza(iq)
39
51
  await muc.send_mam(iq)
40
52
 
41
- async def __handle_mam_get_form(self, iq: Iq):
53
+ async def __handle_mam_get_form(self, iq: StanzaBase):
54
+ assert isinstance(iq, Iq)
42
55
  ito = iq.get_to()
43
56
 
44
57
  if ito == self.xmpp.boundjid.bare:
@@ -46,13 +59,7 @@ class Mam:
46
59
  text="No MAM on the component itself, use a JID with a resource"
47
60
  )
48
61
 
49
- ifrom = iq.get_from()
50
- user = user_store.get_by_jid(ifrom)
51
- if user is None:
52
- raise XMPPError("registration-required")
53
-
54
- session = self.xmpp.get_session_from_user(user)
55
-
62
+ session = await self._get_session(iq, 0, logged=True)
56
63
  await session.bookmarks.by_jid(ito)
57
64
 
58
65
  reply = iq.reply()
@@ -70,6 +77,7 @@ class Mam:
70
77
  reply["mam"].append(form)
71
78
  reply.send()
72
79
 
80
+ @exceptions_to_xmpp_errors
73
81
  async def __handle_mam_metadata(self, iq: Iq):
74
- muc = await self.xmpp.get_muc_from_stanza(iq)
82
+ muc = await self.get_muc_from_stanza(iq)
75
83
  await muc.send_mam_metadata(iq)