slidge 0.1.0rc1__py3-none-any.whl → 0.1.2__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. slidge/__init__.py +54 -31
  2. slidge/__main__.py +51 -5
  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 +2 -0
  15. slidge/core/cache.py +121 -39
  16. slidge/core/config.py +116 -11
  17. slidge/core/gateway/__init__.py +3 -0
  18. slidge/core/gateway/base.py +895 -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 +795 -0
  29. slidge/core/gateway/vcard_temp.py +130 -0
  30. slidge/core/mixins/__init__.py +9 -1
  31. slidge/core/mixins/attachment.py +506 -0
  32. slidge/core/mixins/avatar.py +167 -0
  33. slidge/core/mixins/base.py +6 -19
  34. slidge/core/mixins/disco.py +66 -15
  35. slidge/core/mixins/lock.py +31 -0
  36. slidge/core/mixins/message.py +254 -252
  37. slidge/core/mixins/message_maker.py +154 -0
  38. slidge/core/mixins/presence.py +128 -31
  39. slidge/core/mixins/recipient.py +43 -0
  40. slidge/core/pubsub.py +275 -116
  41. slidge/core/session.py +586 -518
  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 +458 -0
  46. slidge/group/room.py +1103 -0
  47. slidge/migration.py +18 -0
  48. slidge/slixfix/__init__.py +68 -0
  49. slidge/{util/xep_0050 → slixfix/link_preview}/__init__.py +4 -5
  50. slidge/slixfix/link_preview/link_preview.py +17 -0
  51. slidge/slixfix/link_preview/stanza.py +99 -0
  52. slidge/slixfix/roster.py +60 -0
  53. slidge/{util → slixfix}/xep_0077/register.py +1 -2
  54. slidge/slixfix/xep_0077/stanza.py +104 -0
  55. slidge/{util → slixfix}/xep_0100/gateway.py +17 -12
  56. slidge/slixfix/xep_0153/__init__.py +10 -0
  57. slidge/slixfix/xep_0153/stanza.py +25 -0
  58. slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
  59. slidge/slixfix/xep_0264/__init__.py +5 -0
  60. slidge/slixfix/xep_0264/stanza.py +36 -0
  61. slidge/slixfix/xep_0264/thumbnail.py +23 -0
  62. slidge/slixfix/xep_0292/__init__.py +5 -0
  63. slidge/slixfix/xep_0292/vcard4.py +100 -0
  64. slidge/slixfix/xep_0313/__init__.py +12 -0
  65. slidge/slixfix/xep_0313/mam.py +262 -0
  66. slidge/slixfix/xep_0313/stanza.py +359 -0
  67. slidge/slixfix/xep_0317/__init__.py +5 -0
  68. slidge/slixfix/xep_0317/hats.py +17 -0
  69. slidge/slixfix/xep_0317/stanza.py +28 -0
  70. slidge/{util → slixfix}/xep_0356_old/privilege.py +9 -7
  71. slidge/slixfix/xep_0424/__init__.py +9 -0
  72. slidge/slixfix/xep_0424/retraction.py +77 -0
  73. slidge/slixfix/xep_0424/stanza.py +28 -0
  74. slidge/slixfix/xep_0490/__init__.py +8 -0
  75. slidge/slixfix/xep_0490/mds.py +47 -0
  76. slidge/slixfix/xep_0490/stanza.py +17 -0
  77. slidge/util/__init__.py +4 -6
  78. slidge/util/archive_msg.py +61 -0
  79. slidge/util/conf.py +25 -4
  80. slidge/util/db.py +23 -69
  81. slidge/util/schema.sql +126 -0
  82. slidge/util/sql.py +508 -0
  83. slidge/util/test.py +136 -86
  84. slidge/util/types.py +155 -14
  85. slidge/util/util.py +225 -51
  86. slidge-0.1.2.dist-info/METADATA +111 -0
  87. slidge-0.1.2.dist-info/RECORD +96 -0
  88. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/WHEEL +1 -1
  89. slidge/core/adhoc.py +0 -492
  90. slidge/core/chat_command.py +0 -197
  91. slidge/core/contact.py +0 -441
  92. slidge/core/disco.py +0 -59
  93. slidge/core/gateway.py +0 -899
  94. slidge/core/muc/__init__.py +0 -3
  95. slidge/core/muc/bookmarks.py +0 -74
  96. slidge/core/muc/participant.py +0 -152
  97. slidge/core/muc/room.py +0 -348
  98. slidge/plugins/discord/__init__.py +0 -121
  99. slidge/plugins/discord/client.py +0 -121
  100. slidge/plugins/discord/session.py +0 -172
  101. slidge/plugins/dummy.py +0 -334
  102. slidge/plugins/facebook.py +0 -591
  103. slidge/plugins/hackernews.py +0 -209
  104. slidge/plugins/mattermost/__init__.py +0 -1
  105. slidge/plugins/mattermost/api.py +0 -288
  106. slidge/plugins/mattermost/gateway.py +0 -417
  107. slidge/plugins/mattermost/websocket.py +0 -248
  108. slidge/plugins/signal/__init__.py +0 -4
  109. slidge/plugins/signal/config.py +0 -4
  110. slidge/plugins/signal/contact.py +0 -104
  111. slidge/plugins/signal/gateway.py +0 -379
  112. slidge/plugins/signal/group.py +0 -76
  113. slidge/plugins/signal/session.py +0 -515
  114. slidge/plugins/signal/txt.py +0 -13
  115. slidge/plugins/signal/util.py +0 -32
  116. slidge/plugins/skype.py +0 -310
  117. slidge/plugins/steam.py +0 -400
  118. slidge/plugins/telegram/__init__.py +0 -6
  119. slidge/plugins/telegram/client.py +0 -325
  120. slidge/plugins/telegram/config.py +0 -21
  121. slidge/plugins/telegram/contact.py +0 -154
  122. slidge/plugins/telegram/gateway.py +0 -182
  123. slidge/plugins/telegram/group.py +0 -184
  124. slidge/plugins/telegram/session.py +0 -275
  125. slidge/plugins/telegram/util.py +0 -153
  126. slidge/plugins/whatsapp/__init__.py +0 -6
  127. slidge/plugins/whatsapp/config.py +0 -17
  128. slidge/plugins/whatsapp/contact.py +0 -33
  129. slidge/plugins/whatsapp/event.go +0 -455
  130. slidge/plugins/whatsapp/gateway.go +0 -156
  131. slidge/plugins/whatsapp/gateway.py +0 -69
  132. slidge/plugins/whatsapp/go.mod +0 -17
  133. slidge/plugins/whatsapp/go.sum +0 -22
  134. slidge/plugins/whatsapp/session.go +0 -371
  135. slidge/plugins/whatsapp/session.py +0 -370
  136. slidge/util/xep_0030/__init__.py +0 -13
  137. slidge/util/xep_0030/disco.py +0 -811
  138. slidge/util/xep_0030/stanza/__init__.py +0 -7
  139. slidge/util/xep_0030/stanza/info.py +0 -270
  140. slidge/util/xep_0030/stanza/items.py +0 -147
  141. slidge/util/xep_0030/static.py +0 -467
  142. slidge/util/xep_0050/adhoc.py +0 -631
  143. slidge/util/xep_0050/stanza.py +0 -180
  144. slidge/util/xep_0077/stanza.py +0 -71
  145. slidge/util/xep_0292/__init__.py +0 -1
  146. slidge/util/xep_0292/stanza.py +0 -167
  147. slidge/util/xep_0292/vcard4.py +0 -74
  148. slidge/util/xep_0356/__init__.py +0 -7
  149. slidge/util/xep_0356/permissions.py +0 -35
  150. slidge/util/xep_0356/privilege.py +0 -160
  151. slidge/util/xep_0356/stanza.py +0 -44
  152. slidge/util/xep_0461/__init__.py +0 -6
  153. slidge/util/xep_0461/reply.py +0 -48
  154. slidge/util/xep_0461/stanza.py +0 -80
  155. slidge-0.1.0rc1.dist-info/METADATA +0 -171
  156. slidge-0.1.0rc1.dist-info/RECORD +0 -99
  157. /slidge/{plugins/__init__.py → py.typed} +0 -0
  158. /slidge/{util → slixfix}/xep_0077/__init__.py +0 -0
  159. /slidge/{util → slixfix}/xep_0100/__init__.py +0 -0
  160. /slidge/{util → slixfix}/xep_0100/stanza.py +0 -0
  161. /slidge/{util → slixfix}/xep_0356_old/__init__.py +0 -0
  162. /slidge/{util → slixfix}/xep_0356_old/stanza.py +0 -0
  163. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/LICENSE +0 -0
  164. {slidge-0.1.0rc1.dist-info → slidge-0.1.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,795 @@
1
+ import logging
2
+ from copy import copy
3
+ from typing import TYPE_CHECKING, Awaitable, Callable, Optional, Union
4
+
5
+ from slixmpp import JID, CoroutineCallback, Iq, Message, Presence, StanzaPath
6
+ from slixmpp.exceptions import IqError, XMPPError
7
+ from slixmpp.plugins.xep_0004 import Form
8
+ from slixmpp.plugins.xep_0084.stanza import Info
9
+
10
+ from ... import LegacyContact
11
+ from ...group.room import LegacyMUC
12
+ from ...util.sql import db
13
+ from ...util.types import LinkPreview, Recipient, RecipientType
14
+ from ...util.util import (
15
+ dict_to_named_tuple,
16
+ merge_resources,
17
+ remove_emoji_variation_selector_16,
18
+ )
19
+ from .. import config
20
+ from ..session import BaseSession
21
+
22
+ if TYPE_CHECKING:
23
+ from .base import BaseGateway
24
+
25
+ HandlerType = Callable[[Union[Presence, Message]], Awaitable[None]]
26
+
27
+
28
+ class Ignore(BaseException):
29
+ pass
30
+
31
+
32
+ class SessionDispatcher:
33
+ def __init__(self, xmpp: "BaseGateway"):
34
+ self.xmpp = xmpp
35
+ self.http = xmpp.http
36
+
37
+ xmpp.register_handler(
38
+ CoroutineCallback(
39
+ "MUCModerate",
40
+ StanzaPath("iq/apply_to/moderate"),
41
+ _exceptions_to_xmpp_errors(self.on_user_moderation), # type:ignore
42
+ )
43
+ )
44
+ xmpp.register_handler(
45
+ CoroutineCallback(
46
+ "MUCSetAffiliation",
47
+ StanzaPath("iq@type=set/mucadmin_query"),
48
+ _exceptions_to_xmpp_errors(self.on_user_set_affiliation), # type:ignore
49
+ )
50
+ )
51
+ xmpp.register_handler(
52
+ CoroutineCallback(
53
+ "muc#admin",
54
+ StanzaPath("iq@type=get/mucowner_query"),
55
+ _exceptions_to_xmpp_errors(self.on_muc_owner_query), # type: ignore
56
+ )
57
+ )
58
+ xmpp.register_handler(
59
+ CoroutineCallback(
60
+ "muc#admin",
61
+ StanzaPath("iq@type=set/mucowner_query"),
62
+ _exceptions_to_xmpp_errors(self.on_muc_owner_set), # type: ignore
63
+ )
64
+ )
65
+
66
+ for event in (
67
+ "legacy_message",
68
+ "marker_displayed",
69
+ "presence",
70
+ "chatstate_active",
71
+ "chatstate_inactive",
72
+ "chatstate_composing",
73
+ "chatstate_paused",
74
+ "message_correction",
75
+ "reactions",
76
+ "message_retract",
77
+ "groupchat_join",
78
+ "groupchat_message",
79
+ "groupchat_direct_invite",
80
+ "groupchat_subject",
81
+ "avatar_metadata_publish",
82
+ "message_displayed_synchronization_publish",
83
+ ):
84
+ xmpp.add_event_handler(
85
+ event, _exceptions_to_xmpp_errors(getattr(self, "on_" + event))
86
+ )
87
+
88
+ async def __get_session(
89
+ self, stanza: Union[Message, Presence, Iq], timeout: Optional[int] = 10
90
+ ) -> BaseSession:
91
+ xmpp = self.xmpp
92
+ if stanza.get_from().server == xmpp.boundjid.bare:
93
+ log.debug("Ignoring echo")
94
+ raise Ignore
95
+ if (
96
+ isinstance(stanza, Message)
97
+ and stanza.get_type() == "chat"
98
+ and stanza.get_to() == xmpp.boundjid.bare
99
+ ):
100
+ log.debug("Ignoring message to component")
101
+ raise Ignore
102
+ session = xmpp.get_session_from_stanza(stanza)
103
+ await session.wait_for_ready(timeout)
104
+ if isinstance(stanza, Message) and _ignore(session, stanza):
105
+ raise Ignore
106
+ return session
107
+
108
+ def __ack(self, msg: Message):
109
+ if not self.xmpp.PROPER_RECEIPTS:
110
+ self.xmpp.delivery_receipt.ack(msg)
111
+
112
+ async def __get_session_entity_thread(
113
+ self, msg: Message
114
+ ) -> tuple["BaseSession", Recipient, Union[int, str]]:
115
+ session = await self.__get_session(msg)
116
+ e: Recipient = await _get_entity(session, msg)
117
+ legacy_thread = await _xmpp_to_legacy_thread(session, msg, e)
118
+ return session, e, legacy_thread
119
+
120
+ async def on_legacy_message(self, msg: Message):
121
+ """
122
+ Meant to be called from :class:`BaseGateway` only.
123
+
124
+ :param msg:
125
+ :return:
126
+ """
127
+ # we MUST not use `if m["replace"]["id"]` because it adds the tag if not
128
+ # present. this is a problem for MUC echoed messages
129
+ if msg.get_plugin("replace", check=True) is not None:
130
+ # ignore last message correction (handled by a specific method)
131
+ return
132
+ if msg.get_plugin("apply_to", check=True) is not None:
133
+ # ignore message retraction (handled by a specific method)
134
+ return
135
+ if msg.get_plugin("reactions", check=True) is not None:
136
+ # ignore message reaction fallback.
137
+ # the reaction itself is handled by self.react_from_msg().
138
+ return
139
+ if msg.get_plugin("retract", check=True) is not None:
140
+ # ignore message retraction fallback.
141
+ # the retraction itself is handled by self.on_retract
142
+ return
143
+
144
+ session, entity, thread = await self.__get_session_entity_thread(msg)
145
+
146
+ e: Recipient = await _get_entity(session, msg)
147
+ log.debug("Entity %r", e)
148
+
149
+ if msg.get_plugin("oob", check=True) is not None:
150
+ url = msg["oob"]["url"]
151
+ else:
152
+ url = None
153
+
154
+ text = msg["body"]
155
+
156
+ reply_to = None
157
+ reply_fallback = None
158
+ if msg.get_plugin("reply", check=True):
159
+ try:
160
+ reply_to_msg_xmpp_id = _xmpp_msg_id_to_legacy(
161
+ session, msg["reply"]["id"]
162
+ )
163
+ except XMPPError:
164
+ session.log.debug(
165
+ "Could not determine reply-to legacy msg ID, sending quote instead."
166
+ )
167
+ text = msg["body"]
168
+ reply_fallback = None
169
+ reply_to_msg_xmpp_id = None
170
+ else:
171
+ reply_to_jid = JID(msg["reply"]["to"])
172
+ if msg["type"] == "chat":
173
+ if reply_to_jid.bare != session.user.jid.bare:
174
+ try:
175
+ reply_to = await session.contacts.by_jid(reply_to_jid)
176
+ except XMPPError:
177
+ pass
178
+ elif msg["type"] == "groupchat":
179
+ nick = reply_to_jid.resource
180
+ try:
181
+ muc = await session.bookmarks.by_jid(reply_to_jid)
182
+ except XMPPError:
183
+ pass
184
+ else:
185
+ if nick != muc.user_nick:
186
+ reply_to = await muc.get_participant(
187
+ reply_to_jid.resource, store=False
188
+ )
189
+ if msg.get_plugin("fallback", check=True) and (
190
+ isinstance(e, LegacyMUC) or e.REPLIES
191
+ ):
192
+ text = msg["fallback"].get_stripped_body(
193
+ self.xmpp["xep_0461"].namespace
194
+ )
195
+ try:
196
+ reply_fallback = msg["reply"].get_fallback_body()
197
+ except AttributeError:
198
+ pass
199
+ else:
200
+ reply_to_msg_xmpp_id = None
201
+ reply_to = None
202
+
203
+ if msg.get_plugin("link_previews", check=True):
204
+ pass
205
+
206
+ kwargs = dict(
207
+ reply_to_msg_id=reply_to_msg_xmpp_id,
208
+ reply_to_fallback_text=reply_fallback,
209
+ reply_to=reply_to,
210
+ thread=thread,
211
+ )
212
+
213
+ if not url and isinstance(e, LegacyMUC):
214
+ kwargs["mentions"] = await e.parse_mentions(text)
215
+
216
+ if previews := msg["link_previews"]:
217
+ kwargs["link_previews"] = [
218
+ dict_to_named_tuple(p, LinkPreview) for p in previews
219
+ ]
220
+
221
+ if url:
222
+ async with self.http.get(url) as response:
223
+ if response.status >= 400:
224
+ session.log.warning(
225
+ (
226
+ "OOB url cannot be downloaded: %s, sending the URL as text"
227
+ " instead."
228
+ ),
229
+ response,
230
+ )
231
+ legacy_msg_id = await session.on_text(e, url, **kwargs)
232
+ else:
233
+ legacy_msg_id = await session.on_file(
234
+ e, url, http_response=response, **kwargs
235
+ )
236
+ elif text:
237
+ legacy_msg_id = await session.on_text(e, text, **kwargs)
238
+ else:
239
+ log.debug("Ignoring %s", msg.get_id())
240
+ return
241
+
242
+ if isinstance(e, LegacyMUC):
243
+ await e.echo(msg, legacy_msg_id)
244
+ if legacy_msg_id is not None:
245
+ session.muc_sent_msg_ids[legacy_msg_id] = msg.get_id()
246
+ else:
247
+ self.__ack(msg)
248
+ if legacy_msg_id is not None:
249
+ session.sent[legacy_msg_id] = msg.get_id()
250
+ if session.MESSAGE_IDS_ARE_THREAD_IDS and (t := msg["thread"]):
251
+ session.threads[t] = legacy_msg_id
252
+
253
+ async def on_groupchat_message(self, msg: Message):
254
+ await self.on_legacy_message(msg)
255
+
256
+ async def on_message_correction(self, msg: Message):
257
+ session, entity, thread = await self.__get_session_entity_thread(msg)
258
+ xmpp_id = msg["replace"]["id"]
259
+ if isinstance(entity, LegacyMUC):
260
+ legacy_id = session.muc_sent_msg_ids.inverse.get(xmpp_id)
261
+ else:
262
+ legacy_id = _xmpp_msg_id_to_legacy(session, xmpp_id)
263
+
264
+ if isinstance(entity, LegacyMUC):
265
+ mentions = await entity.parse_mentions(msg["body"])
266
+ else:
267
+ mentions = None
268
+
269
+ if previews := msg["link_previews"]:
270
+ link_previews = [dict_to_named_tuple(p, LinkPreview) for p in previews]
271
+ else:
272
+ link_previews = []
273
+
274
+ if legacy_id is None:
275
+ log.debug("Did not find legacy ID to correct")
276
+ new_legacy_msg_id = await session.on_text(
277
+ entity,
278
+ "Correction:" + msg["body"],
279
+ thread=thread,
280
+ mentions=mentions,
281
+ link_previews=link_previews,
282
+ )
283
+ elif (
284
+ not msg["body"].strip()
285
+ and config.CORRECTION_EMPTY_BODY_AS_RETRACTION
286
+ and entity.RETRACTION
287
+ ):
288
+ await session.on_retract(entity, legacy_id, thread=thread)
289
+ new_legacy_msg_id = None
290
+ elif entity.CORRECTION:
291
+ new_legacy_msg_id = await session.on_correct(
292
+ entity,
293
+ msg["body"],
294
+ legacy_id,
295
+ thread=thread,
296
+ mentions=mentions,
297
+ link_previews=link_previews,
298
+ )
299
+ else:
300
+ session.send_gateway_message(
301
+ "Last message correction is not supported by this legacy service. "
302
+ "Slidge will send your correction as new message."
303
+ )
304
+ if (
305
+ config.LAST_MESSAGE_CORRECTION_RETRACTION_WORKAROUND
306
+ and entity.RETRACTION
307
+ and legacy_id is not None
308
+ ):
309
+ if legacy_id is not None:
310
+ session.send_gateway_message(
311
+ "Slidge will attempt to retract the original message you wanted"
312
+ " to edit."
313
+ )
314
+ await session.on_retract(entity, legacy_id, thread=thread)
315
+
316
+ new_legacy_msg_id = await session.on_text(
317
+ entity,
318
+ "Correction: " + msg["body"],
319
+ thread=thread,
320
+ mentions=mentions,
321
+ link_previews=link_previews,
322
+ )
323
+
324
+ if isinstance(entity, LegacyMUC):
325
+ if new_legacy_msg_id is not None:
326
+ session.muc_sent_msg_ids[new_legacy_msg_id] = msg.get_id()
327
+ await entity.echo(msg, new_legacy_msg_id)
328
+ else:
329
+ self.__ack(msg)
330
+ if new_legacy_msg_id is not None:
331
+ session.sent[new_legacy_msg_id] = msg.get_id()
332
+
333
+ async def on_message_retract(self, msg: Message):
334
+ session, entity, thread = await self.__get_session_entity_thread(msg)
335
+ if not entity.RETRACTION:
336
+ raise XMPPError(
337
+ "bad-request",
338
+ "This legacy service does not support message retraction.",
339
+ )
340
+ xmpp_id: str = msg["retract"]["id"]
341
+ legacy_id = _xmpp_msg_id_to_legacy(session, xmpp_id)
342
+ if legacy_id:
343
+ await session.on_retract(entity, legacy_id, thread=thread)
344
+ if isinstance(entity, LegacyMUC):
345
+ await entity.echo(msg, None)
346
+ else:
347
+ log.debug("Ignored retraction from user")
348
+ self.__ack(msg)
349
+
350
+ async def on_marker_displayed(self, msg: Message):
351
+ session = await self.__get_session(msg)
352
+
353
+ e: Recipient = await _get_entity(session, msg)
354
+ legacy_thread = await _xmpp_to_legacy_thread(session, msg, e)
355
+ displayed_msg_id = msg["displayed"]["id"]
356
+ if not isinstance(e, LegacyMUC) and self.xmpp.MARK_ALL_MESSAGES:
357
+ to_mark = e.get_msg_xmpp_id_up_to(displayed_msg_id) # type: ignore
358
+ if to_mark is None:
359
+ session.log.debug("Can't mark all messages up to %s", displayed_msg_id)
360
+ to_mark = [displayed_msg_id]
361
+ else:
362
+ to_mark = [displayed_msg_id]
363
+ for xmpp_id in to_mark:
364
+ await session.on_displayed(
365
+ e, _xmpp_msg_id_to_legacy(session, xmpp_id), legacy_thread
366
+ )
367
+ if isinstance(e, LegacyMUC):
368
+ await e.echo(msg, None)
369
+
370
+ async def on_chatstate_active(self, msg: Message):
371
+ if msg["body"]:
372
+ # if there is a body, it's handled in self.on_legacy_message()
373
+ return
374
+ session, entity, thread = await self.__get_session_entity_thread(msg)
375
+ await session.on_active(entity, thread)
376
+
377
+ async def on_chatstate_inactive(self, msg: Message):
378
+ session, entity, thread = await self.__get_session_entity_thread(msg)
379
+ await session.on_inactive(entity, thread)
380
+
381
+ async def on_chatstate_composing(self, msg: Message):
382
+ session, entity, thread = await self.__get_session_entity_thread(msg)
383
+ await session.on_composing(entity, thread)
384
+
385
+ async def on_chatstate_paused(self, msg: Message):
386
+ session, entity, thread = await self.__get_session_entity_thread(msg)
387
+ await session.on_paused(entity, thread)
388
+
389
+ async def on_reactions(self, msg: Message):
390
+ session, entity, thread = await self.__get_session_entity_thread(msg)
391
+ react_to: str = msg["reactions"]["id"]
392
+
393
+ special_msg = session.SPECIAL_MSG_ID_PREFIX and react_to.startswith(
394
+ session.SPECIAL_MSG_ID_PREFIX
395
+ )
396
+
397
+ if special_msg:
398
+ legacy_id = react_to
399
+ else:
400
+ legacy_id = _xmpp_msg_id_to_legacy(session, react_to)
401
+
402
+ if not legacy_id:
403
+ log.debug("Ignored reaction from user")
404
+ raise XMPPError(
405
+ "internal-server-error",
406
+ "Could not convert the XMPP msg ID to a legacy ID",
407
+ )
408
+
409
+ emojis = [
410
+ remove_emoji_variation_selector_16(r["value"]) for r in msg["reactions"]
411
+ ]
412
+ error_msg = None
413
+ entity = entity
414
+
415
+ if not special_msg:
416
+ if entity.REACTIONS_SINGLE_EMOJI and len(emojis) > 1:
417
+ error_msg = "Maximum 1 emoji/message"
418
+
419
+ if not error_msg and (subset := await entity.available_emojis(legacy_id)):
420
+ if not set(emojis).issubset(subset):
421
+ error_msg = f"You can only react with the following emojis: {''.join(subset)}"
422
+
423
+ if error_msg:
424
+ session.send_gateway_message(error_msg)
425
+ if not isinstance(entity, LegacyMUC):
426
+ # no need to carbon for groups, we just don't echo the stanza
427
+ entity.react(legacy_id, carbon=True) # type: ignore
428
+ await session.on_react(entity, legacy_id, [], thread=thread)
429
+ raise XMPPError("not-acceptable", text=error_msg)
430
+
431
+ await session.on_react(entity, legacy_id, emojis, thread=thread)
432
+ if isinstance(entity, LegacyMUC):
433
+ await entity.echo(msg, None)
434
+ else:
435
+ self.__ack(msg)
436
+
437
+ multi = db.attachment_get_associated_xmpp_ids(react_to)
438
+ if not multi:
439
+ return
440
+
441
+ if isinstance(entity, LegacyMUC):
442
+ for xmpp_id in multi:
443
+ mc = copy(msg)
444
+ mc["reactions"]["id"] = xmpp_id
445
+ await entity.echo(mc)
446
+ elif isinstance(entity, LegacyContact):
447
+ for xmpp_id in multi:
448
+ entity.react(legacy_id, emojis, xmpp_id=xmpp_id, carbon=True)
449
+
450
+ async def on_presence(self, p: Presence):
451
+ session = await self.__get_session(p)
452
+
453
+ pto = p.get_to()
454
+ if pto == self.xmpp.boundjid.bare:
455
+ # NB: get_type() returns either a proper presence type or
456
+ # a presence show if available. Weird, weird, weird slix.
457
+ if (ptype := p.get_type()) not in _USEFUL_PRESENCES:
458
+ return
459
+ resources = self.xmpp.roster[self.xmpp.boundjid.bare][
460
+ p.get_from()
461
+ ].resources
462
+ session.log.debug("Received a presence from %s", p.get_from())
463
+ await session.on_presence(
464
+ p.get_from().resource,
465
+ ptype, # type: ignore
466
+ p["status"],
467
+ resources,
468
+ merge_resources(resources),
469
+ )
470
+ return
471
+
472
+ muc = session.bookmarks._mucs_by_bare_jid.get(pto.bare)
473
+ if muc is None or p.get_from().resource not in muc.user_resources:
474
+ return
475
+
476
+ if pto.resource == muc.user_nick:
477
+ # Ignore presence stanzas with the valid nick.
478
+ # even if joined to the group, we might receive those from clients,
479
+ # when setting a status message, or going away, etc.
480
+ return
481
+
482
+ # We can't use XMPPError here because from must be room@slidge/VALID-USER-NICK
483
+
484
+ error_from = JID(muc.jid)
485
+ error_from.resource = muc.user_nick
486
+ error_stanza = p.error()
487
+ error_stanza.set_to(p.get_from())
488
+ error_stanza.set_from(error_from)
489
+ error_stanza.enable("muc_join")
490
+ error_stanza.enable("error")
491
+ error_stanza["error"]["type"] = "cancel"
492
+ error_stanza["error"]["by"] = muc.jid
493
+ error_stanza["error"]["condition"] = "not-acceptable"
494
+ error_stanza["error"][
495
+ "text"
496
+ ] = "Slidge does not let you change your nickname in groups."
497
+ error_stanza.send()
498
+
499
+ async def on_groupchat_join(self, p: Presence):
500
+ if not self.xmpp.GROUPS:
501
+ raise XMPPError(
502
+ "feature-not-implemented",
503
+ "This gateway does not implement multi-user chats.",
504
+ )
505
+ session = await self.__get_session(p)
506
+ session.raise_if_not_logged()
507
+ muc = await session.bookmarks.by_jid(p.get_to())
508
+ await muc.join(p)
509
+
510
+ async def on_message_displayed_synchronization_publish(self, msg: Message):
511
+ session = await self.__get_session(msg, timeout=None)
512
+
513
+ chat_jid = msg["pubsub_event"]["items"]["item"]["id"]
514
+
515
+ if chat_jid == self.xmpp.boundjid.bare:
516
+ return
517
+
518
+ chat = await session.get_contact_or_group_or_participant(JID(chat_jid))
519
+ if not isinstance(chat, LegacyMUC):
520
+ session.log.debug("Ignoring non-groupchat MDS event")
521
+ return
522
+
523
+ stanza_id = msg["pubsub_event"]["items"]["item"]["displayed"]["stanza_id"]["id"]
524
+ await session.on_displayed(chat, _xmpp_msg_id_to_legacy(session, stanza_id))
525
+
526
+ async def on_avatar_metadata_publish(self, m: Message):
527
+ if not config.SYNC_AVATAR:
528
+ return
529
+
530
+ session = await self.__get_session(m, timeout=None)
531
+ info = m["pubsub_event"]["items"]["item"]["avatar_metadata"]["info"]
532
+
533
+ await self.on_avatar_metadata_info(session, info)
534
+
535
+ async def on_avatar_metadata_info(self, session: BaseSession, info: Info):
536
+ session.log.debug("Avatar metadata info: %s", info)
537
+ hash_ = info["id"]
538
+
539
+ if session.avatar_hash == hash_:
540
+ return
541
+ session.avatar_hash = hash_
542
+
543
+ if hash_:
544
+ try:
545
+ iq = await self.xmpp.plugin["xep_0084"].retrieve_avatar(
546
+ session.user.jid, hash_, ifrom=self.xmpp.boundjid.bare
547
+ )
548
+ except IqError as e:
549
+ session.log.warning("Could not fetch the user's avatar: %s", e)
550
+ return
551
+ bytes_ = iq["pubsub"]["items"]["item"]["avatar_data"]["value"]
552
+ type_ = info["type"]
553
+ height = info["height"]
554
+ width = info["width"]
555
+ else:
556
+ bytes_ = type_ = height = width = hash_ = None
557
+ try:
558
+ await session.on_avatar(bytes_, hash_, type_, width, height)
559
+ except NotImplementedError:
560
+ pass
561
+ except Exception as e:
562
+ # If something goes wrong here, replying an error stanza will to the
563
+ # avatar update will likely not show in most clients, so let's send
564
+ # a normal message from the component to the user.
565
+ session.send_gateway_message(
566
+ f"Something went wrong trying to set your avatar: {e!r}"
567
+ )
568
+
569
+ async def on_user_moderation(self, iq: Iq):
570
+ session = await self.__get_session(iq)
571
+ session.raise_if_not_logged()
572
+
573
+ muc = await session.bookmarks.by_jid(iq.get_to())
574
+
575
+ apply_to = iq["apply_to"]
576
+ xmpp_id = apply_to["id"]
577
+ if not xmpp_id:
578
+ raise XMPPError("bad-request", "Missing moderated message ID")
579
+
580
+ moderate = apply_to["moderate"]
581
+ if not moderate["retract"]:
582
+ raise XMPPError(
583
+ "feature-not-implemented",
584
+ "Slidge only implements moderation/retraction",
585
+ )
586
+
587
+ legacy_id = _xmpp_msg_id_to_legacy(session, xmpp_id)
588
+ await session.on_moderate(muc, legacy_id, moderate["reason"] or None)
589
+ iq.reply(clear=True).send()
590
+
591
+ async def on_user_set_affiliation(self, iq: Iq):
592
+ session = await self.__get_session(iq)
593
+ session.raise_if_not_logged()
594
+
595
+ item = iq["mucadmin_query"]["item"]
596
+ contact = await session.contacts.by_jid(JID(item["jid"]))
597
+
598
+ muc = await session.bookmarks.by_jid(iq.get_to())
599
+
600
+ await muc.on_set_affiliation(
601
+ contact, item["affiliation"], item["reason"] or None, item["nick"] or None
602
+ )
603
+ iq.reply(clear=True).send()
604
+
605
+ async def on_groupchat_direct_invite(self, msg: Message):
606
+ session = await self.__get_session(msg)
607
+ session.raise_if_not_logged()
608
+
609
+ invite = msg["groupchat_invite"]
610
+ jid = JID(invite["jid"])
611
+
612
+ if jid.domain != self.xmpp.boundjid.bare:
613
+ raise XMPPError(
614
+ "bad-request",
615
+ "Legacy contacts can only be invited to legacy groups, not standard XMPP MUCs.",
616
+ )
617
+
618
+ if invite["password"]:
619
+ raise XMPPError(
620
+ "bad-request", "Password-protected groups are not supported"
621
+ )
622
+
623
+ contact = await session.contacts.by_jid(msg.get_to())
624
+ muc = await session.bookmarks.by_jid(jid)
625
+
626
+ await session.on_invitation(contact, muc, invite["reason"] or None)
627
+
628
+ async def on_muc_owner_query(self, iq: Iq):
629
+ session = await self.__get_session(iq)
630
+ session.raise_if_not_logged()
631
+
632
+ muc = await session.bookmarks.by_jid(iq.get_to())
633
+
634
+ reply = iq.reply()
635
+
636
+ form = Form(title="Slidge room configuration")
637
+ form["instructions"] = (
638
+ "Complete this form to modify the configuration of your room."
639
+ )
640
+ form.add_field(
641
+ var="FORM_TYPE",
642
+ type="hidden",
643
+ value="http://jabber.org/protocol/muc#roomconfig",
644
+ )
645
+ form.add_field(
646
+ var="muc#roomconfig_roomname",
647
+ label="Natural-Language Room Name",
648
+ type="text-single",
649
+ value=muc.name,
650
+ )
651
+ if muc.HAS_DESCRIPTION:
652
+ form.add_field(
653
+ var="muc#roomconfig_roomdesc",
654
+ label="Short Description of Room",
655
+ type="text-single",
656
+ value=muc.description,
657
+ )
658
+
659
+ muc_owner = iq["mucowner_query"]
660
+ muc_owner.append(form)
661
+ reply.append(muc_owner)
662
+ reply.send()
663
+
664
+ async def on_muc_owner_set(self, iq: Iq):
665
+ session = await self.__get_session(iq)
666
+ session.raise_if_not_logged()
667
+ muc = await session.bookmarks.by_jid(iq.get_to())
668
+ query = iq["mucowner_query"]
669
+
670
+ if form := query.get_plugin("form", check=True):
671
+ values = form.get_values()
672
+ await muc.on_set_config(
673
+ name=values.get("muc#roomconfig_roomname"),
674
+ description=(
675
+ values.get("muc#roomconfig_roomdesc")
676
+ if muc.HAS_DESCRIPTION
677
+ else None
678
+ ),
679
+ )
680
+ form["type"] = "result"
681
+ clear = False
682
+ elif destroy := query.get_plugin("destroy", check=True):
683
+ reason = destroy["reason"] or None
684
+ await muc.on_destroy_request(reason)
685
+ user_participant = await muc.get_user_participant()
686
+ user_participant._affiliation = "none"
687
+ user_participant._role = "none"
688
+ presence = user_participant._make_presence(ptype="unavailable", force=True)
689
+ presence["muc"].enable("destroy")
690
+ if reason is not None:
691
+ presence["muc"]["destroy"]["reason"] = reason
692
+ user_participant._send(presence)
693
+ session.bookmarks.remove(muc)
694
+ clear = True
695
+ else:
696
+ raise XMPPError("bad-request")
697
+
698
+ iq.reply(clear=clear).send()
699
+
700
+ async def on_groupchat_subject(self, msg: Message):
701
+ session = await self.__get_session(msg)
702
+ session.raise_if_not_logged()
703
+ muc = await session.bookmarks.by_jid(msg.get_to())
704
+ if not muc.HAS_SUBJECT:
705
+ raise XMPPError(
706
+ "bad-request",
707
+ "There are no room subject in here. "
708
+ "Use the room configuration to update its name or description",
709
+ )
710
+ await muc.on_set_subject(msg["subject"])
711
+
712
+
713
+ def _xmpp_msg_id_to_legacy(session: "BaseSession", xmpp_id: str):
714
+ sent = session.sent.inverse.get(xmpp_id)
715
+ if sent:
716
+ return sent
717
+
718
+ multi = db.attachment_get_legacy_id_for_xmpp_id(xmpp_id)
719
+ if multi:
720
+ return multi
721
+
722
+ try:
723
+ return session.xmpp_to_legacy_msg_id(xmpp_id)
724
+ except XMPPError:
725
+ raise
726
+ except Exception as e:
727
+ log.debug("Couldn't convert xmpp msg ID to legacy ID.", exc_info=e)
728
+ raise XMPPError(
729
+ "internal-server-error", "Couldn't convert xmpp msg ID to legacy ID."
730
+ )
731
+
732
+
733
+ def _ignore(session: "BaseSession", msg: Message):
734
+ if (i := msg.get_id()) not in session.ignore_messages:
735
+ return False
736
+ session.log.debug("Ignored sent carbon: %s", i)
737
+ session.ignore_messages.remove(i)
738
+ return True
739
+
740
+
741
+ async def _xmpp_to_legacy_thread(
742
+ session: "BaseSession", msg: Message, recipient: RecipientType
743
+ ):
744
+ xmpp_thread = msg["thread"]
745
+ if not xmpp_thread:
746
+ return
747
+
748
+ if session.MESSAGE_IDS_ARE_THREAD_IDS:
749
+ return session.threads.get(xmpp_thread)
750
+
751
+ async with session.thread_creation_lock:
752
+ legacy_thread = session.threads.get(xmpp_thread)
753
+ if legacy_thread is None:
754
+ legacy_thread = await recipient.create_thread(xmpp_thread)
755
+ session.threads[xmpp_thread] = legacy_thread
756
+ return legacy_thread
757
+
758
+
759
+ async def _get_entity(session: "BaseSession", m: Message) -> RecipientType:
760
+ session.raise_if_not_logged()
761
+ if m.get_type() == "groupchat":
762
+ muc = await session.bookmarks.by_jid(m.get_to())
763
+ r = m.get_from().resource
764
+ if r not in muc.user_resources:
765
+ session.create_task(muc.kick_resource(r))
766
+ raise XMPPError("not-acceptable", "You are not connected to this chat")
767
+ return muc
768
+ else:
769
+ return await session.contacts.by_jid(m.get_to())
770
+
771
+
772
+ def _exceptions_to_xmpp_errors(cb: HandlerType) -> HandlerType:
773
+ async def wrapped(stanza: Union[Presence, Message]):
774
+ try:
775
+ await cb(stanza)
776
+ except Ignore:
777
+ pass
778
+ except XMPPError:
779
+ raise
780
+ except NotImplementedError:
781
+ log.debug("NotImplementedError raised in %s", cb)
782
+ raise XMPPError(
783
+ "feature-not-implemented", "Not implemented by the legacy module"
784
+ )
785
+ except Exception as e:
786
+ log.error("Failed to handle incoming stanza: %s", stanza, exc_info=e)
787
+ raise XMPPError("internal-server-error", str(e))
788
+
789
+ return wrapped
790
+
791
+
792
+ _USEFUL_PRESENCES = {"available", "unavailable", "away", "chat", "dnd", "xa"}
793
+
794
+
795
+ log = logging.getLogger(__name__)