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
slidge/core/gateway.py DELETED
@@ -1,899 +0,0 @@
1
- """
2
- This module extends slixmpp.ComponentXMPP to make writing new LegacyClients easier
3
- """
4
- import asyncio
5
- import logging
6
- import re
7
- import tempfile
8
- from asyncio import Future
9
- from typing import Callable, Generic, Iterable, Optional, Sequence, Type
10
-
11
- import qrcode
12
- from slixmpp import JID, ComponentXMPP, CoroutineCallback, Iq, Message, StanzaPath
13
- from slixmpp.exceptions import IqError, IqTimeout, XMPPError
14
- from slixmpp.plugins.xep_0363 import FileUploadError
15
- from slixmpp.types import MessageTypes
16
-
17
- from ..util import ABCSubclassableOnceAtMost, FormField
18
- from ..util.db import GatewayUser, RosterBackend, user_store
19
- from ..util.types import AvatarType
20
- from ..util.xep_0292.vcard4 import VCard4Provider
21
- from . import config
22
- from .adhoc import AdhocProvider, RegistrationType
23
- from .chat_command import ChatCommandProvider
24
- from .disco import Disco
25
- from .pubsub import PubSubComponent
26
- from .session import BaseSession, SessionType
27
-
28
-
29
- class BaseGateway(
30
- Generic[SessionType], ComponentXMPP, metaclass=ABCSubclassableOnceAtMost
31
- ):
32
- """
33
- Must be subclassed by a plugin to set up various aspects of the XMPP
34
- component behaviour, such as its display name or its registration process.
35
-
36
- On slidge launch, a singleton is instantiated, and it will be made available
37
- to public classes such :class:`.LegacyContact` or :class:`.BaseSession` as the
38
- ``.xmpp`` attribute.
39
- Since it inherits from :class:`slixmpp.componentxmpp.ComponentXMPP`, this gives you a hand
40
- on low-level XMPP interactions via slixmpp plugins, e.g.:
41
-
42
- .. code-block:: python
43
-
44
- self.send_presence(
45
- pfrom="somebody@component.example.com",
46
- pto="someonwelse@anotherexample.com",
47
- )
48
-
49
- However, you should not need to do so often since the classes of the plugin
50
- API provides higher level abstractions around most commonly needed use-cases, such
51
- as sending messages, or displaying a custom status.
52
-
53
- """
54
-
55
- REGISTRATION_FIELDS: Iterable[FormField] = [
56
- FormField(var="username", label="User name", required=True),
57
- FormField(var="password", label="Password", required=True, private=True),
58
- ]
59
- """
60
- Iterable of fields presented to the gateway user when registering using :xep:`0077`
61
- `extended <https://xmpp.org/extensions/xep-0077.html#extensibility>`_ by :xep:`0004`.
62
- """
63
- REGISTRATION_INSTRUCTIONS: str = "Enter your credentials"
64
- """
65
- The text presented to a user that wants to register (or modify) their legacy account
66
- configuration.
67
- """
68
- REGISTRATION_TYPE = RegistrationType.SINGLE_STEP_FORM
69
- """
70
- SINGLE_STEP_FORM: 1 step, 1 form, compatible with :xep:`0077` (in-band registration)
71
-
72
- QRCODE: The registration requires flashing a QR code in an official client.
73
- See :meth:`.BaseGateway.`
74
-
75
- TWO_FACTOR_CODE: The registration requires confirming login with a 2FA code
76
- """
77
-
78
- COMPONENT_NAME: str = NotImplemented
79
- """Name of the component, as seen in service discovery by XMPP clients"""
80
- COMPONENT_TYPE: Optional[str] = ""
81
- """Type of the gateway, should ideally follow https://xmpp.org/registrar/disco-categories.html"""
82
- COMPONENT_AVATAR: Optional[AvatarType] = None
83
- """
84
- Path, bytes or URL used by the component as an avatar.
85
- """
86
-
87
- ROSTER_GROUP: str = "slidge"
88
- """
89
- Roster entries added by the plugin in the user's roster will be part of the group specified here.
90
- """
91
-
92
- SEARCH_FIELDS: Sequence[FormField] = [
93
- FormField(var="first", label="First name", required=True),
94
- FormField(var="last", label="Last name", required=True),
95
- FormField(var="phone", label="Last name", required=False),
96
- ]
97
- """
98
- Fields used for searching items via the component, through :xep:`0055` (jabber search).
99
- A common use case is to allow users to search for legacy contacts by something else than
100
- their usernames, eg their phone number.
101
-
102
- Plugins should implement search by overriding :meth:`.BaseSession.search`, effectively
103
- restricting search to registered users by default.
104
-
105
- If there is only one field, it can also be used via the ``jabber:iq:gateway`` protocol
106
- described in :xep:`0100`. Limitation: this only works if the search request returns
107
- one result item, and if this item has a 'jid' var.
108
- """
109
- SEARCH_TITLE: str = "Search for legacy contacts"
110
- """
111
- Title of the search form.
112
- """
113
- SEARCH_INSTRUCTIONS: str = ""
114
- """
115
- Instructions of the search form.
116
- """
117
-
118
- _BASE_CHAT_COMMANDS = {
119
- "find": "_chat_command_search",
120
- "help": "_chat_command_help",
121
- "register": "_chat_command_register",
122
- "unregister": "_chat_command_unregister",
123
- "contacts": "_chat_command_list_contacts",
124
- "groups": "_chat_command_list_groups",
125
- }
126
- CHAT_COMMANDS: dict[str, str] = {}
127
- """
128
- Keys of this dict can be used to trigger a command by a simple chat message to the gateway
129
- component. Extra words after the key are passed as *args to the handler. Values of the dict
130
- are strings, and handlers are resolved using ``getattr()`` on the :class:`.BaseGateway`
131
- instance.
132
-
133
- Handlers are coroutines with following signature:
134
-
135
- .. code-block:: python
136
-
137
- async def _chat_command_xxx(*args, msg: Message, session: Optional[Session] = None)
138
- ...
139
-
140
- The original :class:`slixmpp.stanza.Message` is also passed to the handler as the
141
- msg kwarg. If the command comes from a registered gateway user, its session attribute is also
142
- passed to the handler.
143
-
144
- Refer to the :class:`slixmpp.plugins.xep_0050.XEP_0050` for more details on how to use ad-hoc
145
- commands.
146
- """
147
-
148
- WELCOME_MESSAGE = (
149
- "Thank you for registering. Type 'help' to list the available commands, "
150
- "or just start messaging away!"
151
- )
152
- """
153
- A welcome message displayed to users on registration.
154
- This is useful notably for clients that don't consider component JIDs as a valid recipient in their UI,
155
- yet still open a functional chat window on incoming messages from components.
156
- """
157
-
158
- MARK_ALL_MESSAGES = False
159
- """
160
- Set this to True for legacy networks that expects read marks for *all* messages and not just
161
- the latest one that was read (as most XMPP clients will only send a reak mark for the latest msg).
162
- """
163
-
164
- REGISTRATION_2FA_TITLE = "Enter your 2FA code"
165
- REGISTRATION_2FA_INSTRUCTIONS = (
166
- "You should have received something via email or SMS, or something"
167
- )
168
- REGISTRATION_QR_INSTRUCTIONS = "Flash this code or follow this link"
169
-
170
- GROUPS = False
171
-
172
- def __init__(self):
173
- super().__init__(
174
- config.JID,
175
- config.SECRET,
176
- config.SERVER,
177
- config.PORT,
178
- plugin_whitelist=SLIXMPP_PLUGINS,
179
- plugin_config={
180
- "xep_0077": {
181
- "form_fields": None,
182
- "form_instructions": self.REGISTRATION_INSTRUCTIONS,
183
- "enable_subscription": self.REGISTRATION_TYPE
184
- == RegistrationType.SINGLE_STEP_FORM,
185
- },
186
- "xep_0100": {
187
- "component_name": self.COMPONENT_NAME,
188
- "user_store": user_store,
189
- "type": self.COMPONENT_TYPE,
190
- },
191
- "xep_0184": {
192
- "auto_ack": False,
193
- "auto_request": True,
194
- },
195
- "xep_0363": {
196
- "upload_service": config.UPLOAD_SERVICE,
197
- },
198
- },
199
- )
200
- self.loop.set_exception_handler(self.__exception_handler)
201
- self.has_crashed = False
202
-
203
- self.jid_validator = re.compile(config.USER_JID_VALIDATOR)
204
-
205
- self.session_cls: Type[SessionType] = BaseSession.get_unique_subclass()
206
- self.session_cls.xmpp = self
207
-
208
- self.get_session_from_stanza = self.session_cls.from_stanza
209
- self.get_session_from_user = self.session_cls.from_user
210
- self.register_plugins()
211
- self.__register_slixmpp_api()
212
- self.__register_handlers()
213
- self._input_futures: dict[str, Future] = {}
214
-
215
- self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME})
216
- self.pubsub: PubSubComponent = self["pubsub"]
217
- self.vcard: VCard4Provider = self["xep_0292_provider"]
218
- if self.GROUPS:
219
- self.plugin["xep_0030"].add_feature("http://jabber.org/protocol/muc")
220
- self.plugin["xep_0030"].add_identity(
221
- category="conference",
222
- name="Slidged rooms",
223
- itype="text",
224
- jid=self.boundjid,
225
- )
226
-
227
- self.adhoc = AdhocProvider(self)
228
- self.add_adhoc_commands()
229
-
230
- self.disco = Disco(self)
231
-
232
- self.chat_commands = chat = ChatCommandProvider(self)
233
- self._chat_commands: dict[str, Callable] = {
234
- k: getattr(self, v, None) or getattr(chat, v)
235
- for k, v in (self._BASE_CHAT_COMMANDS | self.CHAT_COMMANDS).items()
236
- }
237
-
238
- self.use_origin_id = False
239
-
240
- self.remove_handler("Ping")
241
- self.register_handler(
242
- CoroutineCallback(
243
- "Ping",
244
- StanzaPath("iq@type=get/ping"),
245
- self.__handle_ping, # type:ignore
246
- )
247
- )
248
-
249
- self.qr_pending_registrations = dict[str, asyncio.Future[bool]]()
250
-
251
- async def __handle_ping(self, iq: Iq):
252
- ito = iq.get_to()
253
-
254
- if ito == self.boundjid.bare:
255
- iq.reply().send()
256
-
257
- ifrom = iq.get_from()
258
- user = user_store.get_by_jid(ifrom)
259
- if user is None:
260
- raise XMPPError("registration-required")
261
-
262
- session = self.get_session_from_user(user)
263
-
264
- try:
265
- muc = await session.bookmarks.by_jid(ito)
266
- except XMPPError:
267
- pass
268
- else:
269
- muc.handle_ping(iq)
270
- return
271
-
272
- try:
273
- await session.contacts.by_jid(ito)
274
- except XMPPError:
275
- pass
276
- else:
277
- iq.reply().send()
278
-
279
- def __exception_handler(self, loop: asyncio.AbstractEventLoop, context):
280
- """
281
- Called when a task created by loop.create_task() raises an Exception
282
-
283
- :param loop:
284
- :param context:
285
- :return:
286
- """
287
- log.debug("CONTEXT: %s", context)
288
- exc = context.get("exception")
289
- if exc is None:
290
- log.warning("No exception in this context: %s", context)
291
- elif isinstance(exc, SystemExit):
292
- log.debug("SystemExit called in an asyncio task")
293
- else:
294
- log.exception("Crash in an asyncio task: %s", context)
295
- self.has_crashed = True
296
- loop.stop()
297
-
298
- def _raise_if_not_allowed_jid(self, jid: JID):
299
- if not self.jid_validator.match(jid.bare):
300
- raise XMPPError(
301
- condition="not-allowed",
302
- text="Your account is not allowed to use this gateway.",
303
- )
304
-
305
- def exception(self, exception: Exception):
306
- """
307
- Called when a task created by slixmpp's internal (eg, on slix events) raises an Exception.
308
-
309
- Stop the event loop and exit on unhandled exception.
310
-
311
- The default :class:`slixmpp.basexmpp.BaseXMPP` behaviour is just to
312
- log the exception, but we want to avoid undefined behaviour.
313
-
314
- :param exception: An unhandled :class:`Exception` object.
315
- """
316
- if isinstance(exception, IqError):
317
- iq = exception.iq
318
- log.error("%s: %s", iq["error"]["condition"], iq["error"]["text"])
319
- log.warning("You should catch IqError exceptions")
320
- elif isinstance(exception, IqTimeout):
321
- iq = exception.iq
322
- log.error("Request timed out: %s", iq)
323
- log.warning("You should catch IqTimeout exceptions")
324
- elif isinstance(exception, SyntaxError):
325
- # Hide stream parsing errors that occur when the
326
- # stream is disconnected (they've been handled, we
327
- # don't need to make a mess in the logs).
328
- pass
329
- else:
330
- self.loop.stop()
331
- exit(1)
332
-
333
- def __register_slixmpp_api(self):
334
- self["xep_0077"].api.register(
335
- user_store.get,
336
- "user_get",
337
- )
338
- self["xep_0077"].api.register(
339
- user_store.remove,
340
- "user_remove",
341
- )
342
- self["xep_0077"].api.register(
343
- self.make_registration_form, "make_registration_form"
344
- )
345
- self["xep_0077"].api.register(self._user_validate, "user_validate")
346
- self["xep_0077"].api.register(self._user_modify, "user_modify")
347
-
348
- self["xep_0055"].api.register(self.search_get_form, "search_get_form")
349
- self["xep_0055"].api.register(self._search_query, "search_query")
350
-
351
- self.roster.set_backend(RosterBackend)
352
-
353
- def __register_handlers(self):
354
- self.add_event_handler("session_start", self.__on_session_start)
355
- self.add_event_handler("disconnected", self.connect)
356
- self.add_event_handler("gateway_message", self._on_gateway_message_private)
357
- self.add_event_handler("user_register", self._on_user_register)
358
- self.add_event_handler("user_unregister", self._on_user_unregister)
359
- get_session = self.get_session_from_stanza
360
-
361
- # fmt: off
362
- async def msg(m): await get_session(m).send_from_msg(m)
363
- async def disp(m): await get_session(m).displayed_from_msg(m)
364
- async def active(m): await get_session(m).active_from_msg(m)
365
- async def inactive(m): await get_session(m).inactive_from_msg(m)
366
- async def composing(m): await get_session(m).composing_from_msg(m)
367
- async def paused(m): await get_session(m).paused_from_msg(m)
368
- async def correct(m): await get_session(m).correct_from_msg(m)
369
- async def react(m): await get_session(m).react_from_msg(m)
370
- async def retract(m): await get_session(m).retract_from_msg(m)
371
- async def groupchat_join(p): await get_session(p).join_groupchat(p)
372
- # fmt: on
373
-
374
- self.add_event_handler("legacy_message", msg)
375
- self.add_event_handler("marker_displayed", disp)
376
- self.add_event_handler("chatstate_active", active)
377
- self.add_event_handler("chatstate_inactive", inactive)
378
- self.add_event_handler("chatstate_composing", composing)
379
- self.add_event_handler("chatstate_paused", paused)
380
- self.add_event_handler("message_correction", correct)
381
- self.add_event_handler("reactions", react)
382
- self.add_event_handler("message_retract", retract)
383
-
384
- self.add_event_handler("groupchat_join", groupchat_join)
385
- self.add_event_handler("groupchat_message", msg)
386
-
387
- self.register_handler(
388
- CoroutineCallback(
389
- f"muc#admin",
390
- StanzaPath(f"iq/mucadmin_query"),
391
- self._handle_admin, # type: ignore
392
- )
393
- )
394
-
395
- self.plugin["xep_0030"].add_feature("jabber:iq:gateway")
396
- self.register_handler(
397
- CoroutineCallback(
398
- f"iq:gateway",
399
- StanzaPath(f"iq/gateway"),
400
- self._handle_gateway_iq, # type: ignore
401
- )
402
- )
403
-
404
- async def _handle_admin(self, iq: Iq):
405
- log.debug("MUC ADMIN request %s", iq)
406
- raise XMPPError("not-authorized")
407
-
408
- async def _handle_gateway_iq(self, iq: Iq):
409
- user = user_store.get_by_jid(iq.get_from())
410
- if user is None:
411
- raise XMPPError("not-authorized", "Register to the gateway first")
412
-
413
- if len(self.SEARCH_FIELDS) > 1:
414
- raise XMPPError("not-implemented", "Use jabber search for this gateway")
415
-
416
- field = self.SEARCH_FIELDS[0]
417
-
418
- reply = iq.reply()
419
- if iq["type"] == "get":
420
- reply["gateway"]["desc"] = self.SEARCH_TITLE
421
- reply["gateway"]["prompt"] = field.label
422
- elif iq["type"] == "set":
423
- prompt = iq["gateway"]["prompt"]
424
- session = self.session_cls.from_user(user)
425
- result = await session.search({field.var: prompt})
426
- if result is None or not result.items:
427
- raise XMPPError(
428
- "item-not-found", "No contact was found with the info you provided."
429
- )
430
- if len(result.items) > 1:
431
- raise XMPPError(
432
- "bad-request", "Your search yielded more than one result."
433
- )
434
- reply["gateway"]["jid"] = result.items[0]["jid"]
435
-
436
- reply.send()
437
-
438
- async def __on_session_start(self, event):
439
- log.debug("Gateway session start: %s", event)
440
-
441
- # prevents XMPP clients from considering the gateway as an HTTP upload
442
- disco = self.plugin["xep_0030"]
443
- await disco.del_feature(feature="urn:xmpp:http:upload:0", jid=self.boundjid)
444
- await self.plugin["xep_0115"].update_caps(jid=self.boundjid)
445
-
446
- await self.pubsub.set_avatar(
447
- jid=self.boundjid.bare, avatar=self.COMPONENT_AVATAR
448
- )
449
-
450
- for user in user_store.get_all():
451
- # TODO: before this, we should check if the user has removed us from their roster
452
- # while we were offline and trigger unregister from there. Presence probe does not seem
453
- # to work in this case, there must be another way. privileged entity could be used
454
- # as last resort.
455
- await self["xep_0100"].add_component_to_roster(user.jid)
456
- self.send_presence(
457
- pto=user.bare_jid, ptype="probe"
458
- ) # ensure we get all resources for user
459
- session = self.session_cls.from_user(user)
460
- self.loop.create_task(self._login_wrap(session))
461
-
462
- log.info("Slidge has successfully started")
463
-
464
- async def _login_wrap(self, session: "SessionType"):
465
- session.send_gateway_status("Logging in…", show="dnd")
466
- try:
467
- status = await session.login()
468
- except Exception as e:
469
- log.warning(f"Login problem for %s: %r", session.user, e)
470
- log.exception(e)
471
- session.send_gateway_status(f"Could not login: {e}", show="busy")
472
- session.send_gateway_message(
473
- f"You are not connected to this gateway! "
474
- f"Maybe this message will tell you why: {e}"
475
- )
476
- else:
477
- log.info(f"Login success for %s", session.user)
478
- session.never_logged = False
479
- if status is None:
480
- session.send_gateway_status("Logged in", show="chat")
481
- else:
482
- session.send_gateway_status(status, show="chat")
483
- for c in session.contacts:
484
- # we need to receive presences directed at the contacts, in
485
- # order to send pubsub events for their +notify features
486
- self.send_presence(
487
- pfrom=c.jid, pto=session.user.bare_jid, ptype="probe"
488
- )
489
-
490
- def re_login(self, session: "SessionType"):
491
- async def w():
492
- await session.logout()
493
- await self._login_wrap(session)
494
-
495
- self.loop.create_task(w())
496
-
497
- async def make_registration_form(self, _jid, _node, _ifrom, iq: Iq):
498
- self._raise_if_not_allowed_jid(iq.get_from())
499
- reg = iq["register"]
500
- user = user_store.get_by_stanza(iq)
501
- log.debug("User found: %s", user)
502
-
503
- form = reg["form"]
504
- form.add_field(
505
- "FORM_TYPE",
506
- ftype="hidden",
507
- value="jabber:iq:register",
508
- )
509
- form["title"] = f"Registration to '{self.COMPONENT_NAME}'"
510
- form["instructions"] = self.REGISTRATION_INSTRUCTIONS
511
-
512
- if user is not None:
513
- reg["registered"] = False
514
- form.add_field(
515
- "remove",
516
- label="Remove my registration",
517
- required=True,
518
- ftype="boolean",
519
- value=False,
520
- )
521
-
522
- for field in self.REGISTRATION_FIELDS:
523
- if field.var in reg.interfaces:
524
- val = None if user is None else user.get(field.var)
525
- if val is None:
526
- reg.add_field(field.var)
527
- else:
528
- reg[field.var] = val
529
-
530
- reg["instructions"] = self.REGISTRATION_INSTRUCTIONS
531
-
532
- for field in self.REGISTRATION_FIELDS:
533
- form.add_field(
534
- field.var,
535
- label=field.label,
536
- required=field.required,
537
- ftype=field.type,
538
- options=field.options,
539
- value=field.value if user is None else user.get(field.var, field.value),
540
- )
541
-
542
- reply = iq.reply()
543
- reply.set_payload(reg)
544
- return reply
545
-
546
- async def user_prevalidate(self, ifrom: JID, form_dict: dict[str, Optional[str]]):
547
- """
548
- Pre validate a registration form using the content of self.REGISTRATION_FIELDS
549
- before passing it to the plugin custom validation logic.
550
- """
551
- for field in self.REGISTRATION_FIELDS:
552
- if field.required and not form_dict.get(field.var):
553
- raise ValueError(f"Missing field: '{field.label}'")
554
-
555
- await self.validate(ifrom, form_dict)
556
-
557
- async def _user_validate(self, _gateway_jid, _node, ifrom: JID, iq: Iq):
558
- """
559
- SliXMPP internal API stuff
560
- """
561
- log.debug("User validate: %s", ifrom.bare)
562
- form_dict = {f.var: iq.get(f.var) for f in self.REGISTRATION_FIELDS}
563
- self._raise_if_not_allowed_jid(ifrom)
564
- await self.user_prevalidate(ifrom, form_dict)
565
- log.info("New user: %s", ifrom.bare)
566
- user_store.add(ifrom, form_dict)
567
-
568
- async def _user_modify(
569
- self, _gateway_jid, _node, ifrom: JID, form_dict: dict[str, Optional[str]]
570
- ):
571
- """
572
- SliXMPP internal API stuff
573
- """
574
- user = user_store.get_by_jid(ifrom)
575
- log.debug("Modify user: %s", user)
576
- await self.user_prevalidate(ifrom, form_dict)
577
- user_store.add(ifrom, form_dict)
578
-
579
- async def _on_user_register(self, iq: Iq):
580
- session = self.get_session_from_stanza(iq)
581
- for jid in config.ADMINS:
582
- self.send_message(
583
- mto=jid,
584
- mbody=f"{iq.get_from()} has registered",
585
- mtype="headline",
586
- mfrom=self.boundjid.bare,
587
- )
588
- session.send_gateway_message(self.WELCOME_MESSAGE)
589
- await self._login_wrap(session)
590
-
591
- async def _on_user_unregister(self, iq: Iq):
592
- await self.session_cls.kill_by_jid(iq.get_from())
593
-
594
- async def search_get_form(self, _gateway_jid, _node, ifrom: JID, iq: Iq):
595
- """
596
- Prepare the search form using :attr:`.BaseSession.SEARCH_FIELDS`
597
- """
598
- user = user_store.get_by_jid(ifrom)
599
- if user is None:
600
- raise XMPPError(text="Search is only allowed for registered users")
601
-
602
- reply = iq.reply()
603
- form = reply["search"]["form"]
604
- form["title"] = self.SEARCH_TITLE
605
- form["instructions"] = self.SEARCH_INSTRUCTIONS
606
- for field in self.SEARCH_FIELDS:
607
- form.add_field(**field.dict())
608
- return reply
609
-
610
- async def _search_query(self, _gateway_jid, _node, ifrom: JID, iq: Iq):
611
- """
612
- Handles a search request
613
- """
614
- user = user_store.get_by_jid(ifrom)
615
- if user is None:
616
- raise XMPPError(text="Search is only allowed for registered users")
617
-
618
- result = await self.get_session_from_stanza(iq).search(
619
- iq["search"]["form"].get_values()
620
- )
621
-
622
- if not result:
623
- raise XMPPError("item-not-found", text="Nothing was found")
624
-
625
- reply = iq.reply()
626
- form = reply["search"]["form"]
627
- for field in result.fields:
628
- form.add_reported(field.var, label=field.label, type=field.type)
629
- for item in result.items:
630
- form.add_item(item)
631
- return reply
632
-
633
- def add_adhoc_commands(self):
634
- """
635
- Override this if you want to provide custom adhoc commands (:xep:`0050`)
636
- for your plugin, using :class:`slixmpp.plugins.xep_0050.XEP_0050`
637
-
638
- Basic example:
639
-
640
- .. code-block:: python
641
-
642
- def add_adhoc_commands(self):
643
- self["xep_0050"].add_command(
644
- node="account_info",
645
- name="Account Information",
646
- handler=self.handle_account_info
647
- )
648
-
649
- async def handle_account_info(self, iq: Iq, adhoc_session: dict[str, Any]):
650
- # beware, 'adhoc_session' is not a slidge session!
651
- user = user_store.get_by_stanza(iq)
652
-
653
- if user is None:
654
- raise XMPPError("subscription-required")
655
-
656
- form = self["xep_0004"].make_form("result", "Account info")
657
- form.add_field(
658
- label="Credits",
659
- value=await FakeLegacyClient.get_credits(user.registration_form['username']),
660
- )
661
-
662
- adhoc_session["payload"] = form
663
- adhoc_session["has_next"] = False
664
-
665
- return session
666
- """
667
- pass
668
-
669
- async def validate(
670
- self, user_jid: JID, registration_form: dict[str, Optional[str]]
671
- ):
672
- """
673
- Validate a registration form from a user.
674
-
675
- Since :xep:`0077` is pretty limited in terms of validation, it is OK to validate
676
- anything that looks good here and continue the legacy auth process via direct messages
677
- to the user (using :meth:`.BaseGateway.input` for instance).
678
-
679
- :param user_jid: JID of the user that has just registered
680
- :param registration_form: A dict where keys are the :attr:`.FormField.var` attributes
681
- of the :attr:`.BaseGateway.REGISTRATION_FIELDS` iterable
682
- """
683
- pass
684
-
685
- async def unregister(self, user: GatewayUser):
686
- """
687
- Optionally override this if you need to clean additional
688
- stuff after a user has been removed from the permanent user_store.
689
-
690
- :param user:
691
- """
692
- pass
693
-
694
- async def _on_gateway_message_private(self, msg: Message):
695
- """
696
- Called when an XMPP user (not necessarily registered as a gateway user) sends a direct message to
697
- the gateway.
698
-
699
- If you override this and still want :meth:`.BaseGateway.input` to work, make sure to include the try/except part.
700
-
701
- :param msg: Message sent by the XMPP user
702
- """
703
- try:
704
- f = self._input_futures.pop(msg.get_from().bare)
705
- except KeyError:
706
- text = msg["body"]
707
- command, *rest = text.split(" ")
708
-
709
- user = user_store.get_by_stanza(msg)
710
- if user is None:
711
- session = None
712
- else:
713
- session = self.get_session_from_user(user)
714
-
715
- handler = self._chat_commands.get(command)
716
- if handler is None:
717
- await self.on_gateway_message(msg, session=session)
718
- else:
719
- log.debug("Chat command handler: %s", handler)
720
- await handler(*rest, msg=msg, session=session)
721
- else:
722
- f.set_result(msg["body"])
723
-
724
- @staticmethod
725
- async def on_gateway_message(msg: Message, session: Optional[SessionType] = None):
726
- """
727
- Called when the gateway component receives a direct gateway message.
728
-
729
- Can be used to implement bot like commands, especially in conjunction with
730
- :meth:`.BaseGateway.input`
731
-
732
- :param msg:
733
- :param session: If the message comes from a registered gateway user, their :.BaseSession:
734
- """
735
- if session is None:
736
- r = msg.reply(
737
- body="I got that, but I'm not doing anything with it. I don't even know you!"
738
- )
739
- else:
740
- r = msg.reply(body="What? Type 'help' for the list of available commands.")
741
- r["type"] = "chat"
742
- r.send()
743
-
744
- async def input(
745
- self, jid: JID, text=None, mtype: MessageTypes = "chat", **msg_kwargs
746
- ) -> str:
747
- """
748
- Request arbitrary user input using a simple chat message, and await the result.
749
-
750
- You shouldn't need to call directly bust instead use :meth:`.BaseSession.input`
751
- to directly target a user.
752
-
753
- NB: When using this, the next message that the user sent to the component will
754
- not be transmitted to :meth:`.BaseGateway.on_gateway_message`, but rather intercepted.
755
- Await the coroutine to get its content.
756
-
757
- :param jid: The JID we want input from
758
- :param text: A prompt to display for the user
759
- :param mtype: Message type
760
- :return: The user's reply
761
- """
762
- if text is not None:
763
- self.send_message(
764
- mto=jid, mbody=text, mtype=mtype, mfrom=self.boundjid.bare, **msg_kwargs
765
- )
766
- f = self.loop.create_future()
767
- self._input_futures[jid.bare] = f
768
- await f
769
- return f.result()
770
-
771
- async def send_file(self, filename: str, **msg_kwargs):
772
- """
773
- Upload a file using :xep:`0363` and send the link as out of band (:xep:`0066`)
774
- content in a message.
775
-
776
- :param filename:
777
- :param msg_kwargs:
778
- :return:
779
- """
780
- msg = self.make_message(**msg_kwargs)
781
- msg.set_from(self.boundjid.bare)
782
- try:
783
- url = await self["xep_0363"].upload_file(
784
- filename=filename, ifrom=config.UPLOAD_REQUESTER
785
- )
786
- except FileUploadError as e:
787
- log.warning(
788
- "Something is wrong with the upload service, see the traceback below"
789
- )
790
- log.exception(e)
791
- msg["body"] = (
792
- "I tried to send a file, but something went wrong. "
793
- "Tell your XMPP admin to check slidge logs."
794
- )
795
- msg.send()
796
- return
797
- msg["oob"]["url"] = url
798
- msg["body"] = url
799
- msg.send()
800
-
801
- async def send_qr(self, text: str, **msg_kwargs):
802
- """
803
- Sends a QR Code to a JID
804
-
805
- :param text: The text that will be converted to a QR Code
806
- :param msg_kwargs: Optional additional arguments to pass to :meth:`.BaseGateway.send_file`,
807
- such as the recipient of the QR code.
808
- """
809
- qr = qrcode.make(text)
810
- with tempfile.NamedTemporaryFile(suffix=".png") as f:
811
- qr.save(f.name)
812
- await self.send_file(f.name, **msg_kwargs)
813
-
814
- def shutdown(self):
815
- """
816
- Called by the slidge entrypoint on normal exit.
817
-
818
- Sends offline presences from all contacts of all user sessions and from
819
- the gateway component itself.
820
- No need to call this manually, :func:`slidge.__main__.main` should take care of it.
821
- """
822
- log.debug("Shutting down")
823
- for user in user_store.get_all():
824
- self.session_cls.from_jid(user.jid).shutdown()
825
- self.send_presence(ptype="unavailable", pto=user.jid)
826
-
827
- async def validate_two_factor_code(self, user: GatewayUser, code: str):
828
- """
829
- Called when the user enters their 2FA code.
830
-
831
- Should raise the appropriate ``XMPPError`` if the login fails
832
-
833
- :param user: The gateway user whose registration is pending
834
- Use their ``.bare_jid`` and/or``.registration_form`` attributes
835
- to get what you need
836
- :param code: The code they entered, either via "chatbot" message or
837
- adhoc command
838
- """
839
- raise NotImplementedError
840
-
841
- async def get_qr_text(self, user: GatewayUser) -> str:
842
- """
843
- Plugins should call this to complete registration with QR codes
844
-
845
- :param user: The not-yet-fully-registered GatewayUser.
846
- Use its ``.bare_jid`` and/or``.registration_form`` attributes
847
- to get what you need
848
- """
849
- raise NotImplementedError
850
-
851
- async def confirm_qr(
852
- self, user_bare_jid: str, exception: Optional[Exception] = None
853
- ):
854
- """
855
- Plugins should call this to complete registration with QR codes
856
-
857
- :param user_bare_jid: The not-yet-fully-registered ``GatewayUser`` instance
858
- Use their ``.bare_jid`` and/or``.registration_form`` attributes
859
- to get what you need
860
- :param exception: Optionally, an XMPPError to be raised to **not** confirm
861
- QR code flashing.
862
- """
863
- fut = self.qr_pending_registrations[user_bare_jid]
864
- if exception is None:
865
- fut.set_result(True)
866
- else:
867
- fut.set_exception(exception)
868
-
869
-
870
- SLIXMPP_PLUGINS = [
871
- "xep_0030", # Service discovery
872
- "xep_0045", # Multi-User Chat
873
- "xep_0050", # Adhoc commands
874
- "xep_0055", # Jabber search
875
- "xep_0066", # Out of Band Data
876
- "xep_0077", # In-band registration
877
- "xep_0084", # User Avatar
878
- "xep_0085", # Chat state notifications
879
- "xep_0100", # Gateway interaction
880
- "xep_0106", # JID Escaping
881
- "xep_0115", # Entity capabilities
882
- "xep_0172", # User nickname
883
- "xep_0184", # Message Delivery Receipts
884
- "xep_0199", # XMPP Ping
885
- "xep_0221", # Data Forms Media Element
886
- "xep_0280", # Carbons
887
- "xep_0292_provider", # VCard4
888
- "xep_0308", # Last message correction
889
- "xep_0319", # Last User Interaction in Presence
890
- "xep_0333", # Chat markers
891
- "xep_0334", # Message Processing Hints
892
- "xep_0356", # Privileged Entity
893
- "xep_0356_old", # Privileged Entity (old namespace)
894
- "xep_0363", # HTTP file upload
895
- "xep_0424", # Message retraction
896
- "xep_0444", # Message reactions
897
- "xep_0461", # Message replies
898
- ]
899
- log = logging.getLogger(__name__)