slidge 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slidge/__init__.py +61 -0
- slidge/__main__.py +192 -0
- slidge/command/__init__.py +28 -0
- slidge/command/adhoc.py +258 -0
- slidge/command/admin.py +193 -0
- slidge/command/base.py +441 -0
- slidge/command/categories.py +3 -0
- slidge/command/chat_command.py +288 -0
- slidge/command/register.py +179 -0
- slidge/command/user.py +250 -0
- slidge/contact/__init__.py +8 -0
- slidge/contact/contact.py +452 -0
- slidge/contact/roster.py +192 -0
- slidge/core/__init__.py +3 -0
- slidge/core/cache.py +183 -0
- slidge/core/config.py +209 -0
- slidge/core/gateway/__init__.py +3 -0
- slidge/core/gateway/base.py +892 -0
- slidge/core/gateway/caps.py +63 -0
- slidge/core/gateway/delivery_receipt.py +52 -0
- slidge/core/gateway/disco.py +80 -0
- slidge/core/gateway/mam.py +75 -0
- slidge/core/gateway/muc_admin.py +35 -0
- slidge/core/gateway/ping.py +66 -0
- slidge/core/gateway/presence.py +95 -0
- slidge/core/gateway/registration.py +53 -0
- slidge/core/gateway/search.py +102 -0
- slidge/core/gateway/session_dispatcher.py +757 -0
- slidge/core/gateway/vcard_temp.py +130 -0
- slidge/core/mixins/__init__.py +19 -0
- slidge/core/mixins/attachment.py +506 -0
- slidge/core/mixins/avatar.py +167 -0
- slidge/core/mixins/base.py +31 -0
- slidge/core/mixins/disco.py +130 -0
- slidge/core/mixins/lock.py +31 -0
- slidge/core/mixins/message.py +398 -0
- slidge/core/mixins/message_maker.py +154 -0
- slidge/core/mixins/presence.py +217 -0
- slidge/core/mixins/recipient.py +43 -0
- slidge/core/pubsub.py +525 -0
- slidge/core/session.py +752 -0
- slidge/group/__init__.py +10 -0
- slidge/group/archive.py +125 -0
- slidge/group/bookmarks.py +163 -0
- slidge/group/participant.py +440 -0
- slidge/group/room.py +1095 -0
- slidge/migration.py +18 -0
- slidge/py.typed +0 -0
- slidge/slixfix/__init__.py +68 -0
- slidge/slixfix/link_preview/__init__.py +10 -0
- slidge/slixfix/link_preview/link_preview.py +17 -0
- slidge/slixfix/link_preview/stanza.py +99 -0
- slidge/slixfix/roster.py +60 -0
- slidge/slixfix/xep_0077/__init__.py +10 -0
- slidge/slixfix/xep_0077/register.py +289 -0
- slidge/slixfix/xep_0077/stanza.py +104 -0
- slidge/slixfix/xep_0100/__init__.py +5 -0
- slidge/slixfix/xep_0100/gateway.py +121 -0
- slidge/slixfix/xep_0100/stanza.py +9 -0
- slidge/slixfix/xep_0153/__init__.py +10 -0
- slidge/slixfix/xep_0153/stanza.py +25 -0
- slidge/slixfix/xep_0153/vcard_avatar.py +23 -0
- slidge/slixfix/xep_0264/__init__.py +5 -0
- slidge/slixfix/xep_0264/stanza.py +36 -0
- slidge/slixfix/xep_0264/thumbnail.py +23 -0
- slidge/slixfix/xep_0292/__init__.py +5 -0
- slidge/slixfix/xep_0292/vcard4.py +100 -0
- slidge/slixfix/xep_0313/__init__.py +12 -0
- slidge/slixfix/xep_0313/mam.py +262 -0
- slidge/slixfix/xep_0313/stanza.py +359 -0
- slidge/slixfix/xep_0317/__init__.py +5 -0
- slidge/slixfix/xep_0317/hats.py +17 -0
- slidge/slixfix/xep_0317/stanza.py +28 -0
- slidge/slixfix/xep_0356_old/__init__.py +7 -0
- slidge/slixfix/xep_0356_old/privilege.py +167 -0
- slidge/slixfix/xep_0356_old/stanza.py +44 -0
- slidge/slixfix/xep_0424/__init__.py +9 -0
- slidge/slixfix/xep_0424/retraction.py +77 -0
- slidge/slixfix/xep_0424/stanza.py +28 -0
- slidge/slixfix/xep_0490/__init__.py +8 -0
- slidge/slixfix/xep_0490/mds.py +47 -0
- slidge/slixfix/xep_0490/stanza.py +17 -0
- slidge/util/__init__.py +15 -0
- slidge/util/archive_msg.py +61 -0
- slidge/util/conf.py +206 -0
- slidge/util/db.py +229 -0
- slidge/util/schema.sql +126 -0
- slidge/util/sql.py +508 -0
- slidge/util/test.py +295 -0
- slidge/util/types.py +180 -0
- slidge/util/util.py +295 -0
- slidge-0.1.0.dist-info/LICENSE +661 -0
- slidge-0.1.0.dist-info/METADATA +109 -0
- slidge-0.1.0.dist-info/RECORD +96 -0
- slidge-0.1.0.dist-info/WHEEL +4 -0
- slidge-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,892 @@
|
|
1
|
+
"""
|
2
|
+
This module extends slixmpp.ComponentXMPP to make writing new LegacyClients easier
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import logging
|
7
|
+
import re
|
8
|
+
import tempfile
|
9
|
+
from copy import copy
|
10
|
+
from datetime import datetime
|
11
|
+
from typing import TYPE_CHECKING, Callable, Collection, Optional, Sequence, Union
|
12
|
+
|
13
|
+
import aiohttp
|
14
|
+
import qrcode
|
15
|
+
from slixmpp import JID, ComponentXMPP, Iq, Message, Presence
|
16
|
+
from slixmpp.exceptions import IqError, IqTimeout, XMPPError
|
17
|
+
from slixmpp.plugins.xep_0060.stanza import OwnerAffiliation
|
18
|
+
from slixmpp.types import MessageTypes
|
19
|
+
from slixmpp.xmlstream.xmlstream import NotConnectedError
|
20
|
+
|
21
|
+
from ... import command # noqa: F401
|
22
|
+
from ...command.adhoc import AdhocProvider
|
23
|
+
from ...command.admin import Exec
|
24
|
+
from ...command.base import Command, FormField
|
25
|
+
from ...command.chat_command import ChatCommandProvider
|
26
|
+
from ...command.register import RegistrationType
|
27
|
+
from ...slixfix.roster import RosterBackend
|
28
|
+
from ...slixfix.xep_0292.vcard4 import VCard4Provider
|
29
|
+
from ...util import ABCSubclassableOnceAtMost
|
30
|
+
from ...util.db import GatewayUser, user_store
|
31
|
+
from ...util.sql import db
|
32
|
+
from ...util.types import AvatarType, MessageOrPresenceTypeVar
|
33
|
+
from .. import config
|
34
|
+
from ..mixins import MessageMixin
|
35
|
+
from ..pubsub import PubSubComponent
|
36
|
+
from ..session import BaseSession
|
37
|
+
from .caps import Caps
|
38
|
+
from .delivery_receipt import DeliveryReceipt
|
39
|
+
from .disco import Disco
|
40
|
+
from .mam import Mam
|
41
|
+
from .muc_admin import MucAdmin
|
42
|
+
from .ping import Ping
|
43
|
+
from .presence import PresenceHandlerMixin
|
44
|
+
from .registration import Registration
|
45
|
+
from .search import Search
|
46
|
+
from .session_dispatcher import SessionDispatcher
|
47
|
+
from .vcard_temp import VCardTemp
|
48
|
+
|
49
|
+
if TYPE_CHECKING:
|
50
|
+
from ...group.room import LegacyMUC
|
51
|
+
|
52
|
+
|
53
|
+
class BaseGateway(
|
54
|
+
PresenceHandlerMixin,
|
55
|
+
ComponentXMPP,
|
56
|
+
MessageMixin,
|
57
|
+
metaclass=ABCSubclassableOnceAtMost,
|
58
|
+
):
|
59
|
+
"""
|
60
|
+
The gateway component, handling registrations and un-registrations.
|
61
|
+
|
62
|
+
On slidge launch, a singleton is instantiated, and it will be made available
|
63
|
+
to public classes such :class:`.LegacyContact` or :class:`.BaseSession` as the
|
64
|
+
``.xmpp`` attribute.
|
65
|
+
|
66
|
+
Must be subclassed by a legacy module to set up various aspects of the XMPP
|
67
|
+
component behaviour, such as its display name or welcome message, via
|
68
|
+
class attributes :attr:`.COMPONENT_NAME` :attr:`.WELCOME_MESSAGE`.
|
69
|
+
|
70
|
+
Abstract methods related to the registration process must be overriden
|
71
|
+
for a functional :term:`Legacy Module`:
|
72
|
+
|
73
|
+
- :meth:`.validate`
|
74
|
+
- :meth:`.validate_two_factor_code`
|
75
|
+
- :meth:`.get_qr_text`
|
76
|
+
- :meth:`.confirm_qr`
|
77
|
+
|
78
|
+
NB: Not all of these must be overridden, it depends on the
|
79
|
+
:attr:`REGISTRATION_TYPE`.
|
80
|
+
|
81
|
+
The other methods, such as :meth:`.send_text` or :meth:`.react` are the same
|
82
|
+
as those of :class:`.LegacyContact` and :class:`.LegacyParticipant`, because
|
83
|
+
the component itself is also a "messaging actor", ie, an :term:`XMPP Entity`.
|
84
|
+
For these methods, you need to specify the JID of the recipient with the
|
85
|
+
`mto` parameter.
|
86
|
+
|
87
|
+
Since it inherits from :class:`slixmpp.componentxmpp.ComponentXMPP`,you also
|
88
|
+
have a hand on low-level XMPP interactions via slixmpp methods, e.g.:
|
89
|
+
|
90
|
+
.. code-block:: python
|
91
|
+
|
92
|
+
self.send_presence(
|
93
|
+
pfrom="somebody@component.example.com",
|
94
|
+
pto="someonwelse@anotherexample.com",
|
95
|
+
)
|
96
|
+
|
97
|
+
However, you should not need to do so often since the classes of the plugin
|
98
|
+
API provides higher level abstractions around most commonly needed use-cases, such
|
99
|
+
as sending messages, or displaying a custom status.
|
100
|
+
|
101
|
+
"""
|
102
|
+
|
103
|
+
COMPONENT_NAME: str = NotImplemented
|
104
|
+
"""Name of the component, as seen in service discovery by XMPP clients"""
|
105
|
+
COMPONENT_TYPE: str = ""
|
106
|
+
"""Type of the gateway, should follow https://xmpp.org/registrar/disco-categories.html"""
|
107
|
+
COMPONENT_AVATAR: Optional[AvatarType] = None
|
108
|
+
"""
|
109
|
+
Path, bytes or URL used by the component as an avatar.
|
110
|
+
"""
|
111
|
+
|
112
|
+
REGISTRATION_FIELDS: Collection[FormField] = [
|
113
|
+
FormField(var="username", label="User name", required=True),
|
114
|
+
FormField(var="password", label="Password", required=True, private=True),
|
115
|
+
]
|
116
|
+
"""
|
117
|
+
Iterable of fields presented to the gateway user when registering using :xep:`0077`
|
118
|
+
`extended <https://xmpp.org/extensions/xep-0077.html#extensibility>`_ by :xep:`0004`.
|
119
|
+
"""
|
120
|
+
REGISTRATION_INSTRUCTIONS: str = "Enter your credentials"
|
121
|
+
"""
|
122
|
+
The text presented to a user that wants to register (or modify) their legacy account
|
123
|
+
configuration.
|
124
|
+
"""
|
125
|
+
REGISTRATION_TYPE: RegistrationType = RegistrationType.SINGLE_STEP_FORM
|
126
|
+
"""
|
127
|
+
This attribute determines how users register to the gateway, ie, how they
|
128
|
+
login to the :term:`legacy service <Legacy Service>`.
|
129
|
+
The credentials are then stored persistently, so this process should happen
|
130
|
+
once per user (unless they unregister).
|
131
|
+
|
132
|
+
The registration process always start with a basic data form (:xep:`0004`)
|
133
|
+
presented to the user.
|
134
|
+
But the legacy login flow might require something more sophisticated, see
|
135
|
+
:class:`.RegistrationType` for more details.
|
136
|
+
"""
|
137
|
+
|
138
|
+
REGISTRATION_2FA_TITLE = "Enter your 2FA code"
|
139
|
+
REGISTRATION_2FA_INSTRUCTIONS = (
|
140
|
+
"You should have received something via email or SMS, or something"
|
141
|
+
)
|
142
|
+
REGISTRATION_QR_INSTRUCTIONS = "Flash this code or follow this link"
|
143
|
+
|
144
|
+
ROSTER_GROUP: str = "slidge"
|
145
|
+
"""
|
146
|
+
Name of the group assigned to a :class:`.LegacyContact` automagically
|
147
|
+
added to the :term:`User`'s roster with :meth:`.LegacyContact.add_to_roster`.
|
148
|
+
"""
|
149
|
+
WELCOME_MESSAGE = (
|
150
|
+
"Thank you for registering. Type 'help' to list the available commands, "
|
151
|
+
"or just start messaging away!"
|
152
|
+
)
|
153
|
+
"""
|
154
|
+
A welcome message displayed to users on registration.
|
155
|
+
This is useful notably for clients that don't consider component JIDs as a
|
156
|
+
valid recipient in their UI, yet still open a functional chat window on
|
157
|
+
incoming messages from components.
|
158
|
+
"""
|
159
|
+
|
160
|
+
SEARCH_FIELDS: Sequence[FormField] = [
|
161
|
+
FormField(var="first", label="First name", required=True),
|
162
|
+
FormField(var="last", label="Last name", required=True),
|
163
|
+
FormField(var="phone", label="Phone number", required=False),
|
164
|
+
]
|
165
|
+
"""
|
166
|
+
Fields used for searching items via the component, through :xep:`0055` (jabber search).
|
167
|
+
A common use case is to allow users to search for legacy contacts by something else than
|
168
|
+
their usernames, eg their phone number.
|
169
|
+
|
170
|
+
Plugins should implement search by overriding :meth:`.BaseSession.search`
|
171
|
+
(restricted to registered users).
|
172
|
+
|
173
|
+
If there is only one field, it can also be used via the ``jabber:iq:gateway`` protocol
|
174
|
+
described in :xep:`0100`. Limitation: this only works if the search request returns
|
175
|
+
one result item, and if this item has a 'jid' var.
|
176
|
+
"""
|
177
|
+
SEARCH_TITLE: str = "Search for legacy contacts"
|
178
|
+
"""
|
179
|
+
Title of the search form.
|
180
|
+
"""
|
181
|
+
SEARCH_INSTRUCTIONS: str = ""
|
182
|
+
"""
|
183
|
+
Instructions of the search form.
|
184
|
+
"""
|
185
|
+
|
186
|
+
MARK_ALL_MESSAGES = False
|
187
|
+
"""
|
188
|
+
Set this to True for :term:`legacy networks <Legacy Network>` that expects
|
189
|
+
read marks for *all* messages and not just the latest one that was read
|
190
|
+
(as most XMPP clients will only send a read mark for the latest msg).
|
191
|
+
"""
|
192
|
+
|
193
|
+
PROPER_RECEIPTS = False
|
194
|
+
"""
|
195
|
+
Set this to True if the legacy service provides a real equivalent of message delivery receipts
|
196
|
+
(:xep:`0184`), meaning that there is an event thrown when the actual device of a contact receives
|
197
|
+
a message. Make sure to call Contact.received() adequately if this is set to True.
|
198
|
+
"""
|
199
|
+
|
200
|
+
GROUPS = False
|
201
|
+
|
202
|
+
mtype: MessageTypes = "chat"
|
203
|
+
is_group = False
|
204
|
+
_can_send_carbon = False
|
205
|
+
|
206
|
+
def __init__(self):
|
207
|
+
self.datetime_started = datetime.now()
|
208
|
+
self.xmpp = self # ugly hack to work with the BaseSender mixin :/
|
209
|
+
self.default_ns = "jabber:component:accept"
|
210
|
+
super().__init__(
|
211
|
+
config.JID,
|
212
|
+
config.SECRET,
|
213
|
+
config.SERVER,
|
214
|
+
config.PORT,
|
215
|
+
plugin_whitelist=SLIXMPP_PLUGINS,
|
216
|
+
plugin_config={
|
217
|
+
"xep_0077": {
|
218
|
+
"form_fields": None,
|
219
|
+
"form_instructions": self.REGISTRATION_INSTRUCTIONS,
|
220
|
+
"enable_subscription": self.REGISTRATION_TYPE
|
221
|
+
== RegistrationType.SINGLE_STEP_FORM,
|
222
|
+
},
|
223
|
+
"xep_0100": {
|
224
|
+
"component_name": self.COMPONENT_NAME,
|
225
|
+
"user_store": user_store,
|
226
|
+
"type": self.COMPONENT_TYPE,
|
227
|
+
},
|
228
|
+
"xep_0184": {
|
229
|
+
"auto_ack": False,
|
230
|
+
"auto_request": False,
|
231
|
+
},
|
232
|
+
"xep_0363": {
|
233
|
+
"upload_service": config.UPLOAD_SERVICE,
|
234
|
+
},
|
235
|
+
},
|
236
|
+
fix_error_ns=True,
|
237
|
+
)
|
238
|
+
self.loop.set_exception_handler(self.__exception_handler)
|
239
|
+
self.http: aiohttp.ClientSession = aiohttp.ClientSession()
|
240
|
+
self.has_crashed: bool = False
|
241
|
+
self.use_origin_id = False
|
242
|
+
|
243
|
+
self.jid_validator: re.Pattern = re.compile(config.USER_JID_VALIDATOR)
|
244
|
+
self.qr_pending_registrations = dict[str, asyncio.Future[bool]]()
|
245
|
+
|
246
|
+
self.session_cls: BaseSession = BaseSession.get_unique_subclass()
|
247
|
+
self.session_cls.xmpp = self
|
248
|
+
|
249
|
+
self.get_session_from_stanza: Callable[
|
250
|
+
[Union[Message, Presence, Iq]], BaseSession
|
251
|
+
] = self.session_cls.from_stanza # type: ignore
|
252
|
+
self.get_session_from_user: Callable[[GatewayUser], BaseSession] = (
|
253
|
+
self.session_cls.from_user
|
254
|
+
)
|
255
|
+
|
256
|
+
self.register_plugins()
|
257
|
+
self.__register_slixmpp_events()
|
258
|
+
self.roster.set_backend(RosterBackend)
|
259
|
+
|
260
|
+
self.register_plugin("pubsub", {"component_name": self.COMPONENT_NAME})
|
261
|
+
self.pubsub: PubSubComponent = self["pubsub"]
|
262
|
+
self.vcard: VCard4Provider = self["xep_0292_provider"]
|
263
|
+
self.delivery_receipt: DeliveryReceipt = DeliveryReceipt(self)
|
264
|
+
|
265
|
+
# with this we receive user avatar updates
|
266
|
+
self.plugin["xep_0030"].add_feature("urn:xmpp:avatar:metadata+notify")
|
267
|
+
|
268
|
+
if self.GROUPS:
|
269
|
+
self.plugin["xep_0030"].add_feature("http://jabber.org/protocol/muc")
|
270
|
+
self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2")
|
271
|
+
self.plugin["xep_0030"].add_feature("urn:xmpp:mam:2#extended")
|
272
|
+
self.plugin["xep_0030"].add_feature(self.plugin["xep_0421"].namespace)
|
273
|
+
self.plugin["xep_0030"].add_feature(self["xep_0317"].stanza.NS)
|
274
|
+
self.plugin["xep_0030"].add_identity(
|
275
|
+
category="conference",
|
276
|
+
name=self.COMPONENT_NAME,
|
277
|
+
itype="text",
|
278
|
+
jid=self.boundjid,
|
279
|
+
)
|
280
|
+
|
281
|
+
# why does mypy need these type annotations? no idea
|
282
|
+
self.__adhoc_handler: AdhocProvider = AdhocProvider(self)
|
283
|
+
self.__chat_commands_handler: ChatCommandProvider = ChatCommandProvider(self)
|
284
|
+
self.__disco_handler = Disco(self)
|
285
|
+
|
286
|
+
self.__ping_handler = Ping(self)
|
287
|
+
self.__mam_handler = Mam(self)
|
288
|
+
self.__search_handler = Search(self)
|
289
|
+
self.__caps_handler = Caps(self)
|
290
|
+
self.__vcard_temp_handler = VCardTemp(self)
|
291
|
+
self.__muc_admin_handler = MucAdmin(self)
|
292
|
+
self.__registration = Registration(self)
|
293
|
+
self.__dispatcher = SessionDispatcher(self)
|
294
|
+
|
295
|
+
self.__register_commands()
|
296
|
+
|
297
|
+
db.mam_launch_cleanup_task(self.loop)
|
298
|
+
|
299
|
+
def __register_commands(self):
|
300
|
+
for cls in Command.subclasses:
|
301
|
+
if any(x is NotImplemented for x in [cls.CHAT_COMMAND, cls.NODE, cls.NAME]):
|
302
|
+
log.debug("Not adding command '%s' because it looks abstract", cls)
|
303
|
+
continue
|
304
|
+
if cls is Exec:
|
305
|
+
if config.DEV_MODE:
|
306
|
+
log.warning("/!\ DEV MODE ENABLED /!\\")
|
307
|
+
else:
|
308
|
+
continue
|
309
|
+
c = cls(self)
|
310
|
+
log.debug("Registering %s", cls)
|
311
|
+
self.__adhoc_handler.register(c)
|
312
|
+
self.__chat_commands_handler.register(c)
|
313
|
+
|
314
|
+
def __exception_handler(self, loop: asyncio.AbstractEventLoop, context):
|
315
|
+
"""
|
316
|
+
Called when a task created by loop.create_task() raises an Exception
|
317
|
+
|
318
|
+
:param loop:
|
319
|
+
:param context:
|
320
|
+
:return:
|
321
|
+
"""
|
322
|
+
log.debug("Context in the exception handler: %s", context)
|
323
|
+
exc = context.get("exception")
|
324
|
+
if exc is None:
|
325
|
+
log.warning("No exception in this context: %s", context)
|
326
|
+
elif isinstance(exc, SystemExit):
|
327
|
+
log.debug("SystemExit called in an asyncio task")
|
328
|
+
else:
|
329
|
+
log.error("Crash in an asyncio task: %s", context)
|
330
|
+
log.exception("Crash in task", exc_info=exc)
|
331
|
+
self.has_crashed = True
|
332
|
+
loop.stop()
|
333
|
+
|
334
|
+
def __register_slixmpp_events(self):
|
335
|
+
self.add_event_handler("session_start", self.__on_session_start)
|
336
|
+
self.add_event_handler("disconnected", self.connect)
|
337
|
+
self.add_event_handler("user_register", self._on_user_register)
|
338
|
+
self.add_event_handler("user_unregister", self._on_user_unregister)
|
339
|
+
self.add_event_handler("groupchat_message_error", self.__on_group_chat_error)
|
340
|
+
|
341
|
+
@property # type: ignore
|
342
|
+
def jid(self):
|
343
|
+
# Override to avoid slixmpp deprecation warnings.
|
344
|
+
return self.boundjid
|
345
|
+
|
346
|
+
async def __on_group_chat_error(self, msg: Message):
|
347
|
+
condition = msg["error"].get_condition()
|
348
|
+
if condition not in KICKABLE_ERRORS:
|
349
|
+
return
|
350
|
+
|
351
|
+
try:
|
352
|
+
muc = await self.get_muc_from_stanza(msg)
|
353
|
+
except XMPPError as e:
|
354
|
+
log.debug("Not removing resource", exc_info=e)
|
355
|
+
return
|
356
|
+
mfrom = msg.get_from()
|
357
|
+
resource = mfrom.resource
|
358
|
+
try:
|
359
|
+
muc.user_resources.remove(resource)
|
360
|
+
except KeyError:
|
361
|
+
# this actually happens quite frequently on for both beagle and monal
|
362
|
+
# (not sure why?), but is of no consequence
|
363
|
+
log.debug("%s was not in the resources of %s", resource, muc)
|
364
|
+
else:
|
365
|
+
log.info(
|
366
|
+
"Removed %s from the resources of %s because of error", resource, muc
|
367
|
+
)
|
368
|
+
|
369
|
+
async def __on_session_start(self, event):
|
370
|
+
log.debug("Gateway session start: %s", event)
|
371
|
+
|
372
|
+
# prevents XMPP clients from considering the gateway as an HTTP upload
|
373
|
+
disco = self.plugin["xep_0030"]
|
374
|
+
await disco.del_feature(feature="urn:xmpp:http:upload:0", jid=self.boundjid)
|
375
|
+
await self.plugin["xep_0115"].update_caps(jid=self.boundjid)
|
376
|
+
|
377
|
+
await self.pubsub.set_avatar(
|
378
|
+
jid=self.boundjid.bare, avatar=self.COMPONENT_AVATAR
|
379
|
+
)
|
380
|
+
|
381
|
+
for user in user_store.get_all():
|
382
|
+
# TODO: before this, we should check if the user has removed us from their roster
|
383
|
+
# while we were offline and trigger unregister from there. Presence probe does not seem
|
384
|
+
# to work in this case, there must be another way. privileged entity could be used
|
385
|
+
# as last resort.
|
386
|
+
try:
|
387
|
+
await self["xep_0100"].add_component_to_roster(user.jid)
|
388
|
+
await self.__add_component_to_mds_whitelist(user.jid)
|
389
|
+
except IqError as e:
|
390
|
+
# TODO: remove the user when this happens? or at least
|
391
|
+
# this can happen when the user has unsubscribed from the XMPP server
|
392
|
+
log.warning(
|
393
|
+
"Error with user %s, not logging them automatically",
|
394
|
+
user,
|
395
|
+
exc_info=e,
|
396
|
+
)
|
397
|
+
continue
|
398
|
+
self.send_presence(
|
399
|
+
pto=user.bare_jid, ptype="probe"
|
400
|
+
) # ensure we get all resources for user
|
401
|
+
session = self.session_cls.from_user(user)
|
402
|
+
self.loop.create_task(self.__login_wrap(session))
|
403
|
+
|
404
|
+
log.info("Slidge has successfully started")
|
405
|
+
|
406
|
+
async def __add_component_to_mds_whitelist(self, user_jid: JID):
|
407
|
+
# Uses privileged entity to add ourselves to the whitelist of the PEP
|
408
|
+
# MDS node so we receive MDS events
|
409
|
+
iq_creation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set")
|
410
|
+
iq_creation["pubsub"]["create"]["node"] = self["xep_0490"].stanza.NS
|
411
|
+
|
412
|
+
try:
|
413
|
+
await self["xep_0356"].send_privileged_iq(iq_creation)
|
414
|
+
except PermissionError:
|
415
|
+
log.warning(
|
416
|
+
"IQ privileges not granted for pubsub namespace, we cannot "
|
417
|
+
"create the MDS node of %s",
|
418
|
+
user_jid,
|
419
|
+
)
|
420
|
+
except IqError as e:
|
421
|
+
# conflict this means the node already exists, we can ignore that
|
422
|
+
if e.condition != "conflict":
|
423
|
+
log.exception(
|
424
|
+
"Could not create the MDS node of %s", user_jid, exc_info=e
|
425
|
+
)
|
426
|
+
except Exception as e:
|
427
|
+
log.exception(
|
428
|
+
"Error while trying to create to the MDS node of %s",
|
429
|
+
user_jid,
|
430
|
+
exc_info=e,
|
431
|
+
)
|
432
|
+
|
433
|
+
iq_affiliation = Iq(sto=user_jid.bare, sfrom=user_jid, stype="set")
|
434
|
+
iq_affiliation["pubsub_owner"]["affiliations"]["node"] = self[
|
435
|
+
"xep_0490"
|
436
|
+
].stanza.NS
|
437
|
+
|
438
|
+
aff = OwnerAffiliation()
|
439
|
+
aff["jid"] = self.boundjid.bare
|
440
|
+
aff["affiliation"] = "member"
|
441
|
+
iq_affiliation["pubsub_owner"]["affiliations"].append(aff)
|
442
|
+
|
443
|
+
try:
|
444
|
+
await self["xep_0356"].send_privileged_iq(iq_affiliation)
|
445
|
+
except PermissionError:
|
446
|
+
log.warning(
|
447
|
+
"IQ privileges not granted for pubsub#owner namespace, we cannot "
|
448
|
+
"listen to the MDS events of %s",
|
449
|
+
user_jid,
|
450
|
+
)
|
451
|
+
except Exception as e:
|
452
|
+
log.exception(
|
453
|
+
"Error while trying to subscribe to the MDS node of %s",
|
454
|
+
user_jid,
|
455
|
+
exc_info=e,
|
456
|
+
)
|
457
|
+
|
458
|
+
async def __login_wrap(self, session: "BaseSession"):
|
459
|
+
session.send_gateway_status("Logging in…", show="dnd")
|
460
|
+
try:
|
461
|
+
status = await session.login()
|
462
|
+
except Exception as e:
|
463
|
+
log.warning("Login problem for %s", session.user, exc_info=e)
|
464
|
+
log.exception(e)
|
465
|
+
session.send_gateway_status(f"Could not login: {e}", show="busy")
|
466
|
+
session.send_gateway_message(
|
467
|
+
"You are not connected to this gateway! "
|
468
|
+
f"Maybe this message will tell you why: {e}"
|
469
|
+
)
|
470
|
+
return
|
471
|
+
|
472
|
+
log.info("Login success for %s", session.user)
|
473
|
+
session.logged = True
|
474
|
+
session.send_gateway_status("Syncing contacts…", show="dnd")
|
475
|
+
await session.contacts.fill()
|
476
|
+
if not (r := session.contacts.ready).done():
|
477
|
+
r.set_result(True)
|
478
|
+
if self.GROUPS:
|
479
|
+
session.send_gateway_status("Syncing groups…", show="dnd")
|
480
|
+
await session.bookmarks.fill()
|
481
|
+
if not (r := session.bookmarks.ready).done():
|
482
|
+
r.set_result(True)
|
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(pfrom=c.jid, pto=session.user.bare_jid, ptype="probe")
|
487
|
+
if status is None:
|
488
|
+
session.send_gateway_status("Logged in", show="chat")
|
489
|
+
else:
|
490
|
+
session.send_gateway_status(status, show="chat")
|
491
|
+
# If we stored users avatars (or their hash) persistently across slidge
|
492
|
+
# restarts, we would not need to fetch it on startup
|
493
|
+
self.loop.create_task(self.__fetch_user_avatar(session))
|
494
|
+
|
495
|
+
async def __fetch_user_avatar(self, session: BaseSession):
|
496
|
+
try:
|
497
|
+
iq = await self.xmpp.plugin["xep_0060"].get_items(
|
498
|
+
session.user.bare_jid,
|
499
|
+
self.xmpp.plugin["xep_0084"].stanza.MetaData.namespace,
|
500
|
+
ifrom=self.boundjid.bare,
|
501
|
+
)
|
502
|
+
except IqError as e:
|
503
|
+
session.log.debug("Failed to retrieve avatar: %r", e)
|
504
|
+
return
|
505
|
+
await self.__dispatcher.on_avatar_metadata_info(
|
506
|
+
session, iq["pubsub"]["items"]["item"]["avatar_metadata"]["info"]
|
507
|
+
)
|
508
|
+
|
509
|
+
def _send(
|
510
|
+
self, stanza: MessageOrPresenceTypeVar, **send_kwargs
|
511
|
+
) -> MessageOrPresenceTypeVar:
|
512
|
+
stanza.set_from(self.boundjid.bare)
|
513
|
+
if mto := send_kwargs.get("mto"):
|
514
|
+
stanza.set_to(mto)
|
515
|
+
stanza.send()
|
516
|
+
return stanza
|
517
|
+
|
518
|
+
async def _on_user_register(self, iq: Iq):
|
519
|
+
session = self.get_session_from_stanza(iq)
|
520
|
+
for jid in config.ADMINS:
|
521
|
+
self.send_message(
|
522
|
+
mto=jid,
|
523
|
+
mbody=f"{iq.get_from()} has registered",
|
524
|
+
mtype="headline",
|
525
|
+
mfrom=self.boundjid.bare,
|
526
|
+
)
|
527
|
+
session.send_gateway_message(self.WELCOME_MESSAGE)
|
528
|
+
await self.__login_wrap(session)
|
529
|
+
|
530
|
+
async def _on_user_unregister(self, iq: Iq):
|
531
|
+
await self.session_cls.kill_by_jid(iq.get_from())
|
532
|
+
|
533
|
+
def raise_if_not_allowed_jid(self, jid: JID):
|
534
|
+
if not self.jid_validator.match(jid.bare):
|
535
|
+
raise XMPPError(
|
536
|
+
condition="not-allowed",
|
537
|
+
text="Your account is not allowed to use this gateway.",
|
538
|
+
)
|
539
|
+
|
540
|
+
def send_raw(self, data: Union[str, bytes]):
|
541
|
+
# overridden from XMLStream to strip base64-encoded data from the logs
|
542
|
+
# to make them more readable.
|
543
|
+
if log.isEnabledFor(level=logging.DEBUG):
|
544
|
+
if isinstance(data, str):
|
545
|
+
stripped = copy(data)
|
546
|
+
else:
|
547
|
+
stripped = data.decode("utf-8")
|
548
|
+
# there is probably a way to do that in a single RE,
|
549
|
+
# but since it's only for debugging, the perf penalty
|
550
|
+
# does not matter much
|
551
|
+
for el in LOG_STRIP_ELEMENTS:
|
552
|
+
stripped = re.sub(
|
553
|
+
f"(<{el}.*?>)(.*)(</{el}>)",
|
554
|
+
"\1[STRIPPED]\3",
|
555
|
+
stripped,
|
556
|
+
flags=re.DOTALL | re.IGNORECASE,
|
557
|
+
)
|
558
|
+
log.debug("SEND: %s", stripped)
|
559
|
+
if not self.transport:
|
560
|
+
raise NotConnectedError()
|
561
|
+
if isinstance(data, str):
|
562
|
+
data = data.encode("utf-8")
|
563
|
+
self.transport.write(data)
|
564
|
+
|
565
|
+
def get_session_from_jid(self, j: JID):
|
566
|
+
try:
|
567
|
+
return self.session_cls.from_jid(j)
|
568
|
+
except XMPPError:
|
569
|
+
pass
|
570
|
+
|
571
|
+
async def get_muc_from_stanza(self, iq: Union[Iq, Message]) -> "LegacyMUC":
|
572
|
+
ito = iq.get_to()
|
573
|
+
|
574
|
+
if ito == self.boundjid.bare:
|
575
|
+
raise XMPPError(
|
576
|
+
text="No MAM on the component itself, use a JID with a resource"
|
577
|
+
)
|
578
|
+
|
579
|
+
ifrom = iq.get_from()
|
580
|
+
user = user_store.get_by_jid(ifrom)
|
581
|
+
if user is None:
|
582
|
+
raise XMPPError("registration-required")
|
583
|
+
|
584
|
+
session = self.get_session_from_user(user)
|
585
|
+
session.raise_if_not_logged()
|
586
|
+
|
587
|
+
muc = await session.bookmarks.by_jid(ito)
|
588
|
+
|
589
|
+
return muc
|
590
|
+
|
591
|
+
def exception(self, exception: Exception):
|
592
|
+
# """
|
593
|
+
# Called when a task created by slixmpp's internal (eg, on slix events) raises an Exception.
|
594
|
+
#
|
595
|
+
# Stop the event loop and exit on unhandled exception.
|
596
|
+
#
|
597
|
+
# The default :class:`slixmpp.basexmpp.BaseXMPP` behaviour is just to
|
598
|
+
# log the exception, but we want to avoid undefined behaviour.
|
599
|
+
#
|
600
|
+
# :param exception: An unhandled :class:`Exception` object.
|
601
|
+
# """
|
602
|
+
if isinstance(exception, IqError):
|
603
|
+
iq = exception.iq
|
604
|
+
log.error("%s: %s", iq["error"]["condition"], iq["error"]["text"])
|
605
|
+
log.warning("You should catch IqError exceptions")
|
606
|
+
elif isinstance(exception, IqTimeout):
|
607
|
+
iq = exception.iq
|
608
|
+
log.error("Request timed out: %s", iq)
|
609
|
+
log.warning("You should catch IqTimeout exceptions")
|
610
|
+
elif isinstance(exception, SyntaxError):
|
611
|
+
# Hide stream parsing errors that occur when the
|
612
|
+
# stream is disconnected (they've been handled, we
|
613
|
+
# don't need to make a mess in the logs).
|
614
|
+
pass
|
615
|
+
else:
|
616
|
+
if exception:
|
617
|
+
log.exception(exception)
|
618
|
+
self.loop.stop()
|
619
|
+
exit(1)
|
620
|
+
|
621
|
+
def re_login(self, session: "BaseSession"):
|
622
|
+
async def w():
|
623
|
+
await session.logout()
|
624
|
+
await self.__login_wrap(session)
|
625
|
+
|
626
|
+
self.loop.create_task(w())
|
627
|
+
|
628
|
+
async def make_registration_form(self, _jid, _node, _ifrom, iq: Iq):
|
629
|
+
self.raise_if_not_allowed_jid(iq.get_from())
|
630
|
+
reg = iq["register"]
|
631
|
+
user = user_store.get_by_stanza(iq)
|
632
|
+
log.debug("User found: %s", user)
|
633
|
+
|
634
|
+
form = reg["form"]
|
635
|
+
form.add_field(
|
636
|
+
"FORM_TYPE",
|
637
|
+
ftype="hidden",
|
638
|
+
value="jabber:iq:register",
|
639
|
+
)
|
640
|
+
form["title"] = f"Registration to '{self.COMPONENT_NAME}'"
|
641
|
+
form["instructions"] = self.REGISTRATION_INSTRUCTIONS
|
642
|
+
|
643
|
+
if user is not None:
|
644
|
+
reg["registered"] = False
|
645
|
+
form.add_field(
|
646
|
+
"remove",
|
647
|
+
label="Remove my registration",
|
648
|
+
required=True,
|
649
|
+
ftype="boolean",
|
650
|
+
value=False,
|
651
|
+
)
|
652
|
+
|
653
|
+
for field in self.REGISTRATION_FIELDS:
|
654
|
+
if field.var in reg.interfaces:
|
655
|
+
val = None if user is None else user.get(field.var)
|
656
|
+
if val is None:
|
657
|
+
reg.add_field(field.var)
|
658
|
+
else:
|
659
|
+
reg[field.var] = val
|
660
|
+
|
661
|
+
reg["instructions"] = self.REGISTRATION_INSTRUCTIONS
|
662
|
+
|
663
|
+
for field in self.REGISTRATION_FIELDS:
|
664
|
+
form.add_field(
|
665
|
+
field.var,
|
666
|
+
label=field.label,
|
667
|
+
required=field.required,
|
668
|
+
ftype=field.type,
|
669
|
+
options=field.options,
|
670
|
+
value=field.value if user is None else user.get(field.var, field.value),
|
671
|
+
)
|
672
|
+
|
673
|
+
reply = iq.reply()
|
674
|
+
reply.set_payload(reg)
|
675
|
+
return reply
|
676
|
+
|
677
|
+
async def user_prevalidate(self, ifrom: JID, form_dict: dict[str, Optional[str]]):
|
678
|
+
# Pre validate a registration form using the content of self.REGISTRATION_FIELDS
|
679
|
+
# before passing it to the plugin custom validation logic.
|
680
|
+
for field in self.REGISTRATION_FIELDS:
|
681
|
+
if field.required and not form_dict.get(field.var):
|
682
|
+
raise ValueError(f"Missing field: '{field.label}'")
|
683
|
+
|
684
|
+
await self.validate(ifrom, form_dict)
|
685
|
+
|
686
|
+
async def validate(
|
687
|
+
self, user_jid: JID, registration_form: dict[str, Optional[str]]
|
688
|
+
):
|
689
|
+
"""
|
690
|
+
Validate a user's initial registration form.
|
691
|
+
|
692
|
+
Should raise the appropriate :class:`slixmpp.exceptions.XMPPError`
|
693
|
+
if the registration does not allow to continue the registration process.
|
694
|
+
|
695
|
+
If :py:attr:`REGISTRATION_TYPE` is a
|
696
|
+
:attr:`.RegistrationType.SINGLE_STEP_FORM`,
|
697
|
+
this method should raise something if it wasn't possible to successfully
|
698
|
+
log in to the legacy service with the registration form content.
|
699
|
+
|
700
|
+
It is also used for other types of :py:attr:`REGISTRATION_TYPE` too, since
|
701
|
+
the first step is always a form. If :attr:`.REGISTRATION_FIELDS` is an
|
702
|
+
empty list (ie, it declares no :class:`.FormField`), the "form" is
|
703
|
+
effectively a confirmation dialog displaying
|
704
|
+
:attr:`.REGISTRATION_INSTRUCTIONS`.
|
705
|
+
|
706
|
+
:param user_jid: JID of the user that has just registered
|
707
|
+
:param registration_form: A dict where keys are the :attr:`.FormField.var` attributes
|
708
|
+
of the :attr:`.BaseGateway.REGISTRATION_FIELDS` iterable
|
709
|
+
"""
|
710
|
+
raise NotImplementedError
|
711
|
+
|
712
|
+
async def validate_two_factor_code(self, user: GatewayUser, code: str):
|
713
|
+
"""
|
714
|
+
Called when the user enters their 2FA code.
|
715
|
+
|
716
|
+
Should raise the appropriate :class:`slixmpp.exceptions.XMPPError`
|
717
|
+
if the login fails, and return successfully otherwise.
|
718
|
+
|
719
|
+
Only used when :attr:`REGISTRATION_TYPE` is
|
720
|
+
:attr:`.RegistrationType.TWO_FACTOR_CODE`.
|
721
|
+
|
722
|
+
:param user: The :class:`.GatewayUser` whose registration is pending
|
723
|
+
Use their :attr:`.GatewayUser.bare_jid` and/or
|
724
|
+
:attr:`.registration_form` attributes to get what you need.
|
725
|
+
:param code: The code they entered, either via "chatbot" message or
|
726
|
+
adhoc command
|
727
|
+
"""
|
728
|
+
raise NotImplementedError
|
729
|
+
|
730
|
+
async def get_qr_text(self, user: GatewayUser) -> str:
|
731
|
+
"""
|
732
|
+
This is where slidge gets the QR code content for the QR-based
|
733
|
+
registration process. It will turn it into a QR code image and send it
|
734
|
+
to the not-yet-fully-registered :class:`.GatewayUser`.
|
735
|
+
|
736
|
+
Only used in when :attr:`BaseGateway.REGISTRATION_TYPE` is
|
737
|
+
:attr:`.RegistrationType.QRCODE`.
|
738
|
+
|
739
|
+
:param user: The :class:`.GatewayUser` whose registration is pending
|
740
|
+
Use their :attr:`.GatewayUser.bare_jid` and/or
|
741
|
+
:attr:`.registration_form` attributes to get what you need.
|
742
|
+
"""
|
743
|
+
raise NotImplementedError
|
744
|
+
|
745
|
+
async def confirm_qr(
|
746
|
+
self, user_bare_jid: str, exception: Optional[Exception] = None
|
747
|
+
):
|
748
|
+
"""
|
749
|
+
This method is meant to be called to finalize QR code-based registration
|
750
|
+
flows, once the legacy service confirms the QR flashing.
|
751
|
+
|
752
|
+
Only used in when :attr:`BaseGateway.REGISTRATION_TYPE` is
|
753
|
+
:attr:`.RegistrationType.QRCODE`.
|
754
|
+
|
755
|
+
:param user_bare_jid: The bare JID of the almost-registered
|
756
|
+
:class:`GatewayUser` instance
|
757
|
+
:param exception: Optionally, an XMPPError to be raised to **not** confirm
|
758
|
+
QR code flashing.
|
759
|
+
"""
|
760
|
+
fut = self.qr_pending_registrations[user_bare_jid]
|
761
|
+
if exception is None:
|
762
|
+
fut.set_result(True)
|
763
|
+
else:
|
764
|
+
fut.set_exception(exception)
|
765
|
+
|
766
|
+
async def unregister_user(self, user: GatewayUser):
|
767
|
+
await self.xmpp.plugin["xep_0077"].api["user_remove"](None, None, user.jid)
|
768
|
+
await self.xmpp.session_cls.kill_by_jid(user.jid)
|
769
|
+
|
770
|
+
async def unregister(self, user: GatewayUser):
|
771
|
+
"""
|
772
|
+
Optionally override this if you need to clean additional
|
773
|
+
stuff after a user has been removed from the permanent user_store.
|
774
|
+
|
775
|
+
By default, this just calls :meth:`BaseSession.logout`.
|
776
|
+
|
777
|
+
:param user:
|
778
|
+
"""
|
779
|
+
session = self.get_session_from_user(user)
|
780
|
+
await session.logout()
|
781
|
+
|
782
|
+
async def input(
|
783
|
+
self, jid: JID, text=None, mtype: MessageTypes = "chat", **msg_kwargs
|
784
|
+
) -> str:
|
785
|
+
"""
|
786
|
+
Request arbitrary user input using a simple chat message, and await the result.
|
787
|
+
|
788
|
+
You shouldn't need to call this directly bust instead use
|
789
|
+
:meth:`.BaseSession.input` to directly target a user.
|
790
|
+
|
791
|
+
:param jid: The JID we want input from
|
792
|
+
:param text: A prompt to display for the user
|
793
|
+
:param mtype: Message type
|
794
|
+
:return: The user's reply
|
795
|
+
"""
|
796
|
+
return await self.__chat_commands_handler.input(jid, text, mtype, **msg_kwargs)
|
797
|
+
|
798
|
+
async def send_qr(self, text: str, **msg_kwargs):
|
799
|
+
"""
|
800
|
+
Sends a QR Code to a JID
|
801
|
+
|
802
|
+
You shouldn't need to call directly bust instead use
|
803
|
+
:meth:`.BaseSession.send_qr` to directly target a user.
|
804
|
+
|
805
|
+
:param text: The text that will be converted to a QR Code
|
806
|
+
:param msg_kwargs: Optional additional arguments to pass to
|
807
|
+
:meth:`.BaseGateway.send_file`, such as the recipient of the QR,
|
808
|
+
code
|
809
|
+
"""
|
810
|
+
qr = qrcode.make(text)
|
811
|
+
with tempfile.NamedTemporaryFile(
|
812
|
+
suffix=".png", delete=config.NO_UPLOAD_METHOD != "move"
|
813
|
+
) as f:
|
814
|
+
qr.save(f.name)
|
815
|
+
await self.send_file(f.name, **msg_kwargs)
|
816
|
+
|
817
|
+
def shutdown(self):
|
818
|
+
# """
|
819
|
+
# Called by the slidge entrypoint on normal exit.
|
820
|
+
#
|
821
|
+
# Sends offline presences from all contacts of all user sessions and from
|
822
|
+
# the gateway component itself.
|
823
|
+
# No need to call this manually, :func:`slidge.__main__.main` should take care of it.
|
824
|
+
# """
|
825
|
+
log.debug("Shutting down")
|
826
|
+
for user in user_store.get_all():
|
827
|
+
self.session_cls.from_jid(user.jid).shutdown()
|
828
|
+
self.send_presence(ptype="unavailable", pto=user.jid)
|
829
|
+
|
830
|
+
|
831
|
+
KICKABLE_ERRORS = {
|
832
|
+
"gone",
|
833
|
+
"internal-server-error",
|
834
|
+
"item-not-found",
|
835
|
+
"jid-malformed",
|
836
|
+
"recipient-unavailable",
|
837
|
+
"redirect",
|
838
|
+
"remote-server-not-found",
|
839
|
+
"remote-server-timeout",
|
840
|
+
"service-unavailable",
|
841
|
+
"malformed error",
|
842
|
+
}
|
843
|
+
|
844
|
+
|
845
|
+
SLIXMPP_PLUGINS = [
|
846
|
+
"link_preview", # https://wiki.soprani.ca/CheogramApp/LinkPreviews
|
847
|
+
"xep_0030", # Service discovery
|
848
|
+
"xep_0045", # Multi-User Chat
|
849
|
+
"xep_0050", # Adhoc commands
|
850
|
+
"xep_0054", # VCard-temp (for MUC avatars)
|
851
|
+
"xep_0055", # Jabber search
|
852
|
+
"xep_0059", # Result Set Management
|
853
|
+
"xep_0066", # Out of Band Data
|
854
|
+
"xep_0077", # In-band registration
|
855
|
+
"xep_0084", # User Avatar
|
856
|
+
"xep_0085", # Chat state notifications
|
857
|
+
"xep_0100", # Gateway interaction
|
858
|
+
"xep_0106", # JID Escaping
|
859
|
+
"xep_0115", # Entity capabilities
|
860
|
+
"xep_0122", # Data Forms Validation
|
861
|
+
"xep_0153", # vCard-Based Avatars (for MUC avatars)
|
862
|
+
"xep_0172", # User nickname
|
863
|
+
"xep_0184", # Message Delivery Receipts
|
864
|
+
"xep_0199", # XMPP Ping
|
865
|
+
"xep_0221", # Data Forms Media Element
|
866
|
+
"xep_0249", # Direct MUC Invitations
|
867
|
+
"xep_0264", # Jingle Content Thumbnails
|
868
|
+
"xep_0280", # Carbons
|
869
|
+
"xep_0292_provider", # VCard4
|
870
|
+
"xep_0308", # Last message correction
|
871
|
+
"xep_0313", # Message Archive Management
|
872
|
+
"xep_0317", # Hats
|
873
|
+
"xep_0319", # Last User Interaction in Presence
|
874
|
+
"xep_0333", # Chat markers
|
875
|
+
"xep_0334", # Message Processing Hints
|
876
|
+
"xep_0356", # Privileged Entity
|
877
|
+
"xep_0356_old", # Privileged Entity (old namespace)
|
878
|
+
"xep_0363", # HTTP file upload
|
879
|
+
"xep_0385", # Stateless in-line media sharing
|
880
|
+
"xep_0402", # PEP Native Bookmarks
|
881
|
+
"xep_0421", # Anonymous unique occupant identifiers for MUCs
|
882
|
+
"xep_0424", # Message retraction
|
883
|
+
"xep_0425", # Message moderation
|
884
|
+
"xep_0444", # Message reactions
|
885
|
+
"xep_0447", # Stateless File Sharing
|
886
|
+
"xep_0461", # Message replies
|
887
|
+
"xep_0490", # Message Displayed Synchronization
|
888
|
+
]
|
889
|
+
|
890
|
+
LOG_STRIP_ELEMENTS = ["data", "binval"]
|
891
|
+
|
892
|
+
log = logging.getLogger(__name__)
|