slidge 0.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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__)
|