summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml27
-rw-r--r--doap.xml46
-rw-r--r--docs/api/plugins/xep_0055.rst18
-rw-r--r--docs/howto/guide_xep_0030.rst22
-rw-r--r--docs/howto/index.rst1
-rw-r--r--docs/howto/remove_process.rst55
-rw-r--r--docs/howto/xmpp_tdg.rst2
-rw-r--r--docs/using_asyncio.rst35
-rwxr-xr-xexamples/http_upload.py65
-rw-r--r--itests/test_pep.py9
-rwxr-xr-xsetup.py8
-rw-r--r--slixmpp/__init__.py17
-rw-r--r--slixmpp/basexmpp.py2
-rw-r--r--slixmpp/features/feature_starttls/stanza.py12
-rw-r--r--slixmpp/jid.py12
-rw-r--r--slixmpp/plugins/__init__.py3
-rw-r--r--slixmpp/plugins/xep_0009/binding.py2
-rw-r--r--slixmpp/plugins/xep_0009/rpc.py12
-rw-r--r--slixmpp/plugins/xep_0030/disco.py3
-rw-r--r--slixmpp/plugins/xep_0045/muc.py67
-rw-r--r--slixmpp/plugins/xep_0045/stanza.py40
-rw-r--r--slixmpp/plugins/xep_0047/stream.py9
-rw-r--r--slixmpp/plugins/xep_0055/__init__.py6
-rw-r--r--slixmpp/plugins/xep_0055/search.py89
-rw-r--r--slixmpp/plugins/xep_0055/stanza.py10
-rw-r--r--slixmpp/plugins/xep_0082.py18
-rw-r--r--slixmpp/plugins/xep_0084/stanza.py6
-rw-r--r--slixmpp/plugins/xep_0115/static.py4
-rw-r--r--slixmpp/plugins/xep_0202/stanza.py5
-rw-r--r--slixmpp/plugins/xep_0203/stanza.py4
-rw-r--r--slixmpp/plugins/xep_0313/mam.py4
-rw-r--r--slixmpp/plugins/xep_0333/markers.py5
-rw-r--r--slixmpp/plugins/xep_0356/stanza.py11
-rw-r--r--slixmpp/plugins/xep_0363/http_upload.py13
-rw-r--r--slixmpp/plugins/xep_0425/moderation.py1
-rw-r--r--slixmpp/plugins/xep_0444/stanza.py4
-rw-r--r--slixmpp/plugins/xep_0454/__init__.py176
-rw-r--r--slixmpp/plugins/xep_0461/__init__.py6
-rw-r--r--slixmpp/plugins/xep_0461/reply.py48
-rw-r--r--slixmpp/plugins/xep_0461/stanza.py47
-rw-r--r--slixmpp/stanza/message.py8
-rw-r--r--slixmpp/thirdparty/__init__.py1
-rw-r--r--slixmpp/thirdparty/mini_dateutil.py273
-rw-r--r--slixmpp/version.py5
-rw-r--r--slixmpp/xmlstream/handler/waiter.py2
-rw-r--r--slixmpp/xmlstream/resolver.py36
-rw-r--r--slixmpp/xmlstream/xmlstream.py101
-rw-r--r--tests/test_stanza_xep_0055.py59
-rw-r--r--tests/test_stanza_xep_0356.py2
-rw-r--r--tests/test_stanza_xep_0444.py2
-rw-r--r--tests/test_stanza_xep_0461.py48
-rw-r--r--tests/test_stream_xep_0055.py170
-rw-r--r--tests/test_stream_xep_0356.py4
-rw-r--r--tests/test_stream_xep_0461.py48
-rw-r--r--tests/test_xep_0454.py41
55 files changed, 1289 insertions, 435 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 48c6be9a..ebcc24eb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,30 @@ test:
script:
- apt update
- apt install -y python3 python3-pip cython3 gpg
- - pip3 install emoji aiohttp
+ - pip3 install emoji aiohttp cryptography
+ - ./run_tests.py
+
+test-3.10:
+ stage: test
+ tags:
+ - docker
+ image: python:3.10
+ script:
+ - apt update
+ - apt install -y python3 python3-pip cython3 gpg
+ - pip3 install emoji aiohttp cryptography
+ - ./run_tests.py
+
+test-3.11:
+ stage: test
+ tags:
+ - docker
+ image: python:3.11-rc
+ allow_failure: true
+ script:
+ - apt update
+ - apt install -y python3 python3-pip cython3 gpg
+ - pip3 install emoji aiohttp cryptography
- ./run_tests.py
test_integration:
@@ -42,6 +65,6 @@ trigger_poezio:
stage: trigger
tags:
- docker
- image: appropriate/curl:latest
+ image: curlimages/curl:7.79.1
script:
- curl --request POST -F token="$SLIXMPP_TRIGGER_TOKEN" -F ref=master https://lab.louiz.org/api/v4/projects/18/trigger/pipeline
diff --git a/doap.xml b/doap.xml
index 7f1db05f..50557c43 100644
--- a/doap.xml
+++ b/doap.xml
@@ -457,6 +457,14 @@
</implements>
<implements>
<xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0175.html"/>
+ <xmpp:status>complete</xmpp:status>
+ <xmpp:version>1.2</xmpp:version>
+ <xmpp:since>1.0</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
+ <implements>
+ <xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0184.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>1.4.0</xmpp:version>
@@ -892,6 +900,14 @@
<xmpp:since>1.6.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
+ <implements>
+ <xmpp:SupportedXep>
+ <xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0454.html"/>
+ <xmpp:status>no thumbnail support</xmpp:status>
+ <xmpp:version>0.1.0</xmpp:version>
+ <xmpp:since>1.8.1</xmpp:since>
+ </xmpp:SupportedXep>
+ </implements>
<release>
<Version>
@@ -1002,7 +1018,35 @@
<Version>
<revision>1.7.0</revision>
<created>2021-01-29</created>
- <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.9/slixmpp-slix-1.7.9.tar.gz"/>
+ <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.0/slixmpp-slix-1.7.0.tar.gz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>1.7.1</revision>
+ <created>2021-04-30</created>
+ <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.7.1/slixmpp-slix-1.7.1.tar.gz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>1.8.0</revision>
+ <created>2022-02-27</created>
+ <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.0/slixmpp-slix-1.8.0.tar.gz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>1.8.1</revision>
+ <created>2022-03-20</created>
+ <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.1/slixmpp-slix-1.8.1.tar.gz"/>
+ </Version>
+ </release>
+ <release>
+ <Version>
+ <revision>1.8.2</revision>
+ <created>2022-04-06</created>
+ <file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.8.2/slixmpp-slix-1.8.2.tar.gz"/>
</Version>
</release>
</Project>
diff --git a/docs/api/plugins/xep_0055.rst b/docs/api/plugins/xep_0055.rst
new file mode 100644
index 00000000..75abe991
--- /dev/null
+++ b/docs/api/plugins/xep_0055.rst
@@ -0,0 +1,18 @@
+
+XEP-0055: Jabber search
+=======================
+
+.. module:: slixmpp.plugins.xep_0055
+
+.. autoclass:: XEP_0055
+ :members:
+ :exclude-members: session_bind, plugin_init, plugin_end
+
+
+Stanza elements
+---------------
+
+.. automodule:: slixmpp.plugins.xep_0055.stanza
+ :members:
+ :undoc-members:
+
diff --git a/docs/howto/guide_xep_0030.rst b/docs/howto/guide_xep_0030.rst
index a3af4857..4b795c93 100644
--- a/docs/howto/guide_xep_0030.rst
+++ b/docs/howto/guide_xep_0030.rst
@@ -172,14 +172,14 @@ the `XEP-0059 <http://xmpp.org/extensions/xep-0059.html>`_ plug-in.
.. code-block:: python
- info = yield from self['xep_0030'].get_info(jid='foo@example.com',
- node='bar',
- ifrom='baz@mycomponent.example.com',
- timeout=30)
+ info = await self['xep_0030'].get_info(jid='foo@example.com',
+ node='bar',
+ ifrom='baz@mycomponent.example.com',
+ timeout=30)
- items = self['xep_0030'].get_info(jid='foo@example.com',
- node='bar',
- iterator=True)
+ items = await self['xep_0030'].get_items(jid='foo@example.com',
+ node='bar',
+ iterator=True)
For more examples on how to use basic disco queries, check the ``disco_browser.py``
example in the ``examples`` directory.
@@ -194,7 +194,7 @@ a full Iq stanza.
.. code-block:: python
- info = self['xep_0030'].get_info(node='foo', local=True)
- items = self['xep_0030'].get_items(jid='somejid@mycomponent.example.com',
- node='bar',
- local=True)
+ info = await self['xep_0030'].get_info(node='foo', local=True)
+ items = await self['xep_0030'].get_items(jid='somejid@mycomponent.example.com',
+ node='bar',
+ local=True)
diff --git a/docs/howto/index.rst b/docs/howto/index.rst
index e4dee4d7..a4f0f3b0 100644
--- a/docs/howto/index.rst
+++ b/docs/howto/index.rst
@@ -9,6 +9,7 @@ Tutorials, FAQs, and How To Guides
internal_api
features
sasl
+ remove_process
handlersmatchers
guide_xep_0030
xmpp_tdg
diff --git a/docs/howto/remove_process.rst b/docs/howto/remove_process.rst
new file mode 100644
index 00000000..26d54a62
--- /dev/null
+++ b/docs/howto/remove_process.rst
@@ -0,0 +1,55 @@
+.. _remove-process:
+
+How to remove xmpp.process()
+============================
+
+
+Starting from slixmpp 1.8.0, running ``process()`` on an
+XMLStream/ClientXMPP/ComponentXMPP instance is deprecated, and starting from
+1.9.0, it will be removed.
+
+Why
+---
+
+This has been the usual way of running an application using SleekXMPP/slixmpp
+for ages, but it has come at a price: people do not understand how they
+should run their application without it, or how to integrate their slixmpp
+code with the rest of their asyncio application.
+
+In essence, ``process()`` is only a very thin wrapper around asyncio loop
+functions:
+
+.. code-block:: python
+
+ if timeout is None:
+ if forever:
+ self.loop.run_forever()
+ else:
+ self.loop.run_until_complete(self.disconnected)
+ else:
+ tasks: List[Future] = [asyncio.sleep(timeout)]
+ if not forever:
+ tasks.append(self.disconnected)
+ self.loop.run_until_complete(asyncio.wait(tasks))
+
+How
+---
+
+Hence it can be replaced according to what you want your application to do:
+
+- To run forever, ``loop.run_forever()`` will work just fine
+
+- To run until disconnected, ``loop.run_until_complete(xmpp.disconnected)``
+ will be enough (XMLStream.disconnected is an future which result is set when
+ the stream gets disconnected.
+
+- To run for a scheduled time (and still abort when disconnected):
+
+.. code-block:: python
+
+ tasks = [asyncio.sleep(timeout)]
+ tasks.append(xmpp.disconnected)
+ loop.run_until_complete(asyncio.wait(tasks))
+
+There is no magic at play here and anything is possible if a more flexible
+execution scheme is expected.
diff --git a/docs/howto/xmpp_tdg.rst b/docs/howto/xmpp_tdg.rst
index 53194e13..d3b761d2 100644
--- a/docs/howto/xmpp_tdg.rst
+++ b/docs/howto/xmpp_tdg.rst
@@ -193,7 +193,7 @@ implementation should work correctly.
.. tip::
To see how to implement in-band registration as a Slixmpp plugin,
- see the tutorial :ref:`tutorial-create-plugin`.
+ see the tutorial :ref:`create-plugin`.
`View full source (5) <http://github.com/legastero/xmpp-tdg/blob/master/code/CheshiR/RegistrableComponent.py>`_ |
`View original code (5) <http://github.com/remko/xmpp-tdg/blob/master/code/CheshiR/RegistrableComponent.py>`_
diff --git a/docs/using_asyncio.rst b/docs/using_asyncio.rst
index 55ed7679..ca85b7c6 100644
--- a/docs/using_asyncio.rst
+++ b/docs/using_asyncio.rst
@@ -11,7 +11,7 @@ Block on IQ sending
.. code-block:: python
- result = yield from iq.send()
+ result = await iq.send()
.. warning::
@@ -28,13 +28,7 @@ The same changes from the SleekXMPP API apply, so you can do:
.. code-block:: python
- iq_info = yield from self.xmpp['xep_0030'].get_info(jid)
-
-But the following will only return a Future:
-
-.. code-block:: python
-
- iq_info = self.xmpp['xep_0030'].get_info(jid)
+ iq_info = await self.xmpp['xep_0030'].get_info(jid)
Callbacks, Event Handlers, and Stream Handlers
@@ -42,7 +36,7 @@ Callbacks, Event Handlers, and Stream Handlers
IQ callbacks and :term:`Event Handlers <event handler>` can be coroutine
functions; in this case, they will be scheduled in the event loop using
-:meth:`.asyncio.async` and not ran immediately.
+:meth:`.asyncio.ensure_future` and not ran immediately.
A :class:`.CoroutineCallback` class has been added as well for
:term:`Stream Handlers <stream handler>`, which will use
@@ -94,18 +88,18 @@ a simple <message>.
.. code-block:: python
- import asyncio, aiohttp, slixmpp
+ import aiohttp, slixmpp
- @asyncio.coroutine
- def get_pythonorg(event):
- req = yield from aiohttp.request('get', 'http://www.python.org')
- text = yield from req.text
+ async def get_pythonorg(event):
+ async with aiohttp.ClientSession() as session:
+ async with session.get('http://www.python.org') as resp:
+ text = await req.text()
client.send_message(mto='jid2@example', mbody=text)
- @asyncio.coroutine
- def get_asyncioorg(event):
- req = yield from aiohttp.request('get', 'http://www.asyncio.org')
- text = yield from req.text
+ async def get_asyncioorg(event):
+ async with aiohttp.ClientSession() as session:
+ async with session.get('http://www.asyncio.org') as resp:
+ text = await req.text()
client.send_message(mto='jid3@example', mbody=text)
client = slixmpp.ClientXMPP('jid@example', 'password')
@@ -132,11 +126,10 @@ JID indicating its findings.
self.register_plugin('xep_0092')
self.add_event_handler('message', self.on_message)
- @asyncio.coroutine
- def on_message(self, event):
+ async def on_message(self, event):
# You should probably handle IqError and IqTimeout exceptions here
# but this is an example.
- version = yield from self['xep_0092'].get_version(message['from'])
+ version = await self['xep_0092'].get_version(message['from'])
text = "%s sent me a message, he runs %s" % (message['from'],
version['software_version']['name'])
self.send_message(mto='master@example.tld', mbody=text)
diff --git a/examples/http_upload.py b/examples/http_upload.py
index a926fd47..b62c736e 100755
--- a/examples/http_upload.py
+++ b/examples/http_upload.py
@@ -5,11 +5,16 @@
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
+from typing import Optional
+
+import sys
import logging
+from pathlib import Path
from getpass import getpass
from argparse import ArgumentParser
import slixmpp
+from slixmpp import JID
from slixmpp.exceptions import IqTimeout
log = logging.getLogger(__name__)
@@ -21,20 +26,40 @@ class HttpUpload(slixmpp.ClientXMPP):
A basic client asking an entity if they confirm the access to an HTTP URL.
"""
- def __init__(self, jid, password, recipient, filename, domain=None):
+ def __init__(
+ self,
+ jid: JID,
+ password: str,
+ recipient: JID,
+ filename: Path,
+ domain: Optional[JID] = None,
+ encrypted: bool = False,
+ ):
slixmpp.ClientXMPP.__init__(self, jid, password)
self.recipient = recipient
self.filename = filename
self.domain = domain
+ self.encrypted = encrypted
self.add_event_handler("session_start", self.start)
async def start(self, event):
log.info('Uploading file %s...', self.filename)
try:
- url = await self['xep_0363'].upload_file(
- self.filename, domain=self.domain, timeout=10
+ upload_file = self['xep_0363'].upload_file
+ if self.encrypted and not self['xep_0454']:
+ print(
+ 'The xep_0454 module isn\'t available. '
+ 'Ensure you have \'cryptography\' '
+ 'from extras_require installed.',
+ file=sys.stderr,
+ )
+ return
+ elif self.encrypted:
+ upload_file = self['xep_0454'].upload_file
+ url = await upload_file(
+ self.filename, domain=self.domain, timeout=10,
)
except IqTimeout:
raise TimeoutError('Could not send message in time')
@@ -79,6 +104,10 @@ if __name__ == '__main__':
parser.add_argument("--domain",
help="Domain to use for HTTP File Upload (leave out for your own server’s)")
+ parser.add_argument("-e", "--encrypt", dest="encrypted",
+ help="Whether to encrypt", action="store_true",
+ default=False)
+
args = parser.parse_args()
# Setup logging.
@@ -86,15 +115,41 @@ if __name__ == '__main__':
format='%(levelname)-8s %(message)s')
if args.jid is None:
- args.jid = input("Username: ")
+ args.jid = JID(input("Username: "))
if args.password is None:
args.password = getpass("Password: ")
- xmpp = HttpUpload(args.jid, args.password, args.recipient, args.file, args.domain)
+ domain = args.domain
+ if domain is not None:
+ domain = JID(domain)
+
+ if args.encrypted:
+ print(
+ 'You are using the --encrypt flag. '
+ 'Be aware that the transport being used is NOT end-to-end '
+ 'encrypted. The server will be able to decrypt the file.',
+ file=sys.stderr,
+ )
+
+ xmpp = HttpUpload(
+ jid=args.jid,
+ password=args.password,
+ recipient=JID(args.recipient),
+ filename=Path(args.file),
+ domain=domain,
+ encrypted=args.encrypted,
+ )
xmpp.register_plugin('xep_0066')
xmpp.register_plugin('xep_0071')
xmpp.register_plugin('xep_0128')
xmpp.register_plugin('xep_0363')
+ try:
+ xmpp.register_plugin('xep_0454')
+ except slixmpp.plugins.base.PluginNotFound:
+ log.error(
+ 'Could not load xep_0454. '
+ 'Ensure you have \'cryptography\' from extras_require installed.'
+ )
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
diff --git a/itests/test_pep.py b/itests/test_pep.py
index a674a348..382c22b6 100644
--- a/itests/test_pep.py
+++ b/itests/test_pep.py
@@ -34,7 +34,14 @@ class TestPEP(SlixIntegration):
"""Check we can get and set public PEP data"""
stanza = Mystanza()
stanza['test'] = str(uuid4().hex)
- await self.clients[0]['xep_0222'].store(stanza, id='toto')
+ try:
+ await self.clients[0]['xep_0060'].delete_node(
+ self.clients[0].boundjid.bare,
+ node=stanza.namespace,
+ )
+ except:
+ pass
+ await self.clients[0]['xep_0222'].store(stanza, node=stanza.namespace, id='toto')
fetched = await self.clients[0]['xep_0222'].retrieve(
stanza.namespace,
)
diff --git a/setup.py b/setup.py
index bcbcdf56..4830efcc 100755
--- a/setup.py
+++ b/setup.py
@@ -86,10 +86,16 @@ setup(
package_data={'slixmpp': ['py.typed']},
packages=packages,
ext_modules=ext_modules,
- install_requires=['aiodns>=1.0', 'pyasn1', 'pyasn1_modules', 'typing_extensions; python_version < "3.8.0"'],
+ install_requires=[
+ 'aiodns>=1.0',
+ 'pyasn1',
+ 'pyasn1_modules',
+ 'typing_extensions; python_version < "3.8.0"',
+ ],
extras_require={
'XEP-0363': ['aiohttp'],
'XEP-0444 compliance': ['emoji'],
+ 'XEP-0454': ['cryptography'],
'Safer XML parsing': ['defusedxml'],
},
classifiers=CLASSIFIERS,
diff --git a/slixmpp/__init__.py b/slixmpp/__init__.py
index 403c9299..5841cdf7 100644
--- a/slixmpp/__init__.py
+++ b/slixmpp/__init__.py
@@ -4,14 +4,17 @@
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import logging
-logging.getLogger(__name__).addHandler(logging.NullHandler())
+from os import getenv
-# Use defusedxml if available
-try:
- import defusedxml
- defusedxml.defuse_stdlib()
-except ImportError:
- pass
+# Use defusedxml if wanted
+# Since enabling it can have adverse consequences for the programs using
+# slixmpp, do not enable it by default.
+if getenv('SLIXMPP_ENABLE_DEFUSEDXML', default='false').lower() == 'true':
+ try:
+ import defusedxml
+ defusedxml.defuse_stdlib()
+ except ImportError:
+ pass
from slixmpp.stanza import Message, Presence, Iq
from slixmpp.jid import JID, InvalidJID
diff --git a/slixmpp/basexmpp.py b/slixmpp/basexmpp.py
index cd228312..c54ec63a 100644
--- a/slixmpp/basexmpp.py
+++ b/slixmpp/basexmpp.py
@@ -140,7 +140,7 @@ class BaseXMPP(XMLStream):
self.use_presence_ids = True
#: XEP-0359 <origin-id/> tag that gets added to <message/> stanzas.
- self.use_origin_id = True
+ self.use_origin_id = False
#: The API registry is a way to process callbacks based on
#: JID+node combinations. Each callback in the registry is
diff --git a/slixmpp/features/feature_starttls/stanza.py b/slixmpp/features/feature_starttls/stanza.py
index 70979402..f433f1b2 100644
--- a/slixmpp/features/feature_starttls/stanza.py
+++ b/slixmpp/features/feature_starttls/stanza.py
@@ -3,8 +3,12 @@
# Copyright (C) 2011 Nathanael C. Fritz
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
-from slixmpp.xmlstream import StanzaBase, ElementBase
from typing import Set, ClassVar
+from slixmpp.xmlstream import StanzaBase, ElementBase
+from slixmpp.xmlstream.xmlstream import InvalidCABundle
+
+import logging
+log = logging.getLogger(__name__)
class STARTTLS(StanzaBase):
@@ -36,6 +40,12 @@ class Proceed(StanzaBase):
namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
interfaces: ClassVar[Set[str]] = set()
+ def exception(self, e: Exception) -> None:
+ log.exception('Error handling {%s}%s stanza',
+ self.namespace, self.name)
+ if isinstance(e, InvalidCABundle):
+ raise e
+
class Failure(StanzaBase):
"""
diff --git a/slixmpp/jid.py b/slixmpp/jid.py
index adde95a4..c705a422 100644
--- a/slixmpp/jid.py
+++ b/slixmpp/jid.py
@@ -133,15 +133,15 @@ def _validate_domain(domain: str):
try:
domain = idna(domain)
except StringprepError:
- raise InvalidJID('idna validation failed')
+ raise InvalidJID(f'idna validation failed: {domain}')
if ':' in domain:
- raise InvalidJID('Domain containing a port')
+ raise InvalidJID(f'Domain containing a port: {domain}')
for label in domain.split('.'):
if not label:
- raise InvalidJID('Domain containing too many dots')
+ raise InvalidJID(f'Domain containing too many dots: {domain}')
if '-' in (label[0], label[-1]):
- raise InvalidJID('Domain started or ended with -')
+ raise InvalidJID(f'Domain starting or ending with -: {domain}')
if not domain:
raise InvalidJID('Domain must not be 0 bytes')
@@ -368,7 +368,7 @@ class JID:
return self._node
@node.setter
- def node(self, value: str):
+ def node(self, value: Optional[str]):
self._node = _validate_node(value)
self._update_bare_full()
@@ -386,7 +386,7 @@ class JID:
return self._resource
@resource.setter
- def resource(self, value: str):
+ def resource(self, value: Optional[str]):
self._resource = _validate_resource(value)
self._update_bare_full()
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index 55627113..ac7482ee 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -1,4 +1,3 @@
-
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2010 Nathanael C. Fritz
# This file is part of Slixmpp.
@@ -24,6 +23,7 @@ __all__ = [
'xep_0049', # Private XML Storage
'xep_0050', # Ad-hoc Commands
'xep_0054', # vcard-temp
+ 'xep_0055', # Jabber Search
'xep_0059', # Result Set Management
'xep_0060', # Pubsub (Client)
'xep_0065', # SOCKS5 Bytestreams
@@ -93,6 +93,7 @@ __all__ = [
'xep_0335', # JSON Containers
'xep_0352', # Client State Indication
'xep_0353', # Jingle Message Initiation
+ 'xep_0356', # Privileged entity
'xep_0359', # Unique and Stable Stanza IDs
'xep_0363', # HTTP File Upload
'xep_0369', # MIX-CORE
diff --git a/slixmpp/plugins/xep_0009/binding.py b/slixmpp/plugins/xep_0009/binding.py
index ffec5431..4b64a010 100644
--- a/slixmpp/plugins/xep_0009/binding.py
+++ b/slixmpp/plugins/xep_0009/binding.py
@@ -22,7 +22,7 @@ def fault2xml(fault):
def xml2fault(params):
vals = []
- for value in params.xml.findall('{%s}value' % _namespace):
+ for value in params.findall('{%s}value' % _namespace):
vals.append(_xml2py(value))
fault = dict()
fault['code'] = vals[0]['faultCode']
diff --git a/slixmpp/plugins/xep_0009/rpc.py b/slixmpp/plugins/xep_0009/rpc.py
index 497303a4..b00432b0 100644
--- a/slixmpp/plugins/xep_0009/rpc.py
+++ b/slixmpp/plugins/xep_0009/rpc.py
@@ -53,8 +53,8 @@ class XEP_0009(BasePlugin):
def make_iq_method_call(self, pto, pmethod, params):
iq = self.xmpp.make_iq_set()
- iq.attrib['to'] = pto
- iq.attrib['from'] = self.xmpp.boundjid.full
+ iq['to'] = pto
+ iq['from'] = self.xmpp.boundjid.full
iq.enable('rpc_query')
iq['rpc_query']['method_call']['method_name'] = pmethod
iq['rpc_query']['method_call']['params'] = params
@@ -62,16 +62,16 @@ class XEP_0009(BasePlugin):
def make_iq_method_response(self, pid, pto, params):
iq = self.xmpp.make_iq_result(pid)
- iq.attrib['to'] = pto
- iq.attrib['from'] = self.xmpp.boundjid.full
+ iq['to'] = pto
+ iq['from'] = self.xmpp.boundjid.full
iq.enable('rpc_query')
iq['rpc_query']['method_response']['params'] = params
return iq
def make_iq_method_response_fault(self, pid, pto, params):
iq = self.xmpp.make_iq_result(pid)
- iq.attrib['to'] = pto
- iq.attrib['from'] = self.xmpp.boundjid.full
+ iq['to'] = pto
+ iq['from'] = self.xmpp.boundjid.full
iq.enable('rpc_query')
iq['rpc_query']['method_response']['params'] = None
iq['rpc_query']['method_response']['fault'] = params
diff --git a/slixmpp/plugins/xep_0030/disco.py b/slixmpp/plugins/xep_0030/disco.py
index 0fa09927..1169a50e 100644
--- a/slixmpp/plugins/xep_0030/disco.py
+++ b/slixmpp/plugins/xep_0030/disco.py
@@ -326,7 +326,6 @@ class XEP_0030(BasePlugin):
info_futures, _ = await asyncio.wait(
infos,
timeout=timeout,
- loop=self.xmpp.loop
)
self.domain_infos[domain] = [
@@ -386,6 +385,8 @@ class XEP_0030(BasePlugin):
local = True
ifrom = kwargs.pop('ifrom', None)
+ if self.xmpp.is_component and ifrom is None:
+ ifrom = self.xmpp.boundjid
if local:
log.debug("Looking up local disco#info data "
"for %s, node %s.", jid, node)
diff --git a/slixmpp/plugins/xep_0045/muc.py b/slixmpp/plugins/xep_0045/muc.py
index 21f5c896..90cb73d7 100644
--- a/slixmpp/plugins/xep_0045/muc.py
+++ b/slixmpp/plugins/xep_0045/muc.py
@@ -35,6 +35,7 @@ from slixmpp.plugins.xep_0045 import stanza
from slixmpp.plugins.xep_0045.stanza import (
MUCInvite,
MUCDecline,
+ MUCDestroy,
MUCPresence,
MUCJoin,
MUCMessage,
@@ -55,6 +56,7 @@ from slixmpp.types import (
PresenceArgs,
)
+JoinResult = Tuple[Presence, Message, List[Presence], List[Message]]
log = logging.getLogger(__name__)
@@ -70,7 +72,7 @@ class XEP_0045(BasePlugin):
name = 'xep_0045'
description = 'XEP-0045: Multi-User Chat'
- dependencies = {'xep_0030', 'xep_0004'}
+ dependencies = {'xep_0030', 'xep_0004', 'xep_0203'}
stanza = stanza
rooms: Dict[JID, Dict[str, MucRoomItem]]
@@ -88,6 +90,7 @@ class XEP_0045(BasePlugin):
register_stanza_plugin(MUCMessage, MUCStatus)
register_stanza_plugin(MUCPresence, MUCStatus)
register_stanza_plugin(Presence, MUCPresence)
+ register_stanza_plugin(MUCPresence, MUCDestroy)
register_stanza_plugin(Presence, MUCJoin)
register_stanza_plugin(MUCJoin, MUCHistory)
register_stanza_plugin(Message, MUCMessage)
@@ -252,6 +255,7 @@ class XEP_0045(BasePlugin):
if msg['body'] or msg['thread']:
return
self.xmpp.event('groupchat_subject', msg)
+ self.xmpp.event('muc::%s::groupchat_subject' % msg['from'].bare, msg)
async def join_muc_wait(self, room: JID, nick: str, *,
password: Optional[str] = None,
@@ -260,7 +264,7 @@ class XEP_0045(BasePlugin):
seconds: Optional[int] = None,
since: Optional[datetime] = None,
presence_options: Optional[PresenceArgs] = None,
- timeout: Optional[int] = None) -> Presence:
+ timeout: Optional[int] = None) -> JoinResult:
"""
Try to join a MUC and block until we are joined or get an error.
@@ -280,7 +284,8 @@ class XEP_0045(BasePlugin):
presence error.
:raises: An asyncio.TimeoutError if there is neither success nor
presence error when the timeout is reached.
- :return: Our own presence
+ :return: A tuple containing our own presence, the subject, a list
+ of occupants and a list of history messages.
"""
if presence_options is None:
presence_options = {}
@@ -303,23 +308,55 @@ class XEP_0045(BasePlugin):
self.rooms[room] = {}
self.our_nicks[room] = nick
stanza.send()
+ return await self._await_join(room, timeout)
- future: asyncio.Future = asyncio.Future()
- context1 = self.xmpp.event_handler("muc::%s::self-presence" % room, future.set_result)
- context2 = self.xmpp.event_handler("muc::%s::presence-error" % room, future.set_result)
- with context1, context2:
+ async def _await_join(self, room: JID, timeout: Optional[int] = None) -> JoinResult:
+ """Do the heavy lifting for awaiting a MUC join
+
+ A muc join, once the join stanza is sent, is:
+ occupant presences → self-presence → room history → room subject
+ """
+ presence_done: asyncio.Future = asyncio.Future()
+ topic_received: asyncio.Future = asyncio.Future()
+ history_buffer: List[Message] = []
+ occupant_buffer: List[Presence] = []
+
+ def add_message(msg: Message):
+ delay = msg.get_plugin('delay', check=True)
+ print(delay)
+ if delay is not None and delay['from'] == room:
+ history_buffer.append(msg)
+
+ def add_occupant(pres: Presence):
+ occupant_buffer.append(pres)
+
+ catch_occupants = self.xmpp.event_handler("muc::%s::got_online" % room, add_occupant)
+ catch_history = self.xmpp.event_handler("muc::%s::message" % room, add_message)
+ subject_handler = self.xmpp.event_handler("muc::%s::groupchat_subject" % room, topic_received.set_result)
+ self_presence = self.xmpp.event_handler("muc::%s::self-presence" % room, presence_done.set_result)
+ presence_error = self.xmpp.event_handler("muc::%s::presence-error" % room, presence_done.set_result)
+
+ with subject_handler, catch_history, catch_occupants:
+ with self_presence, presence_error:
+ done, pending = await asyncio.wait(
+ [presence_done],
+ timeout=timeout,
+ )
+ if pending:
+ raise asyncio.TimeoutError()
+ pres: Presence = presence_done.result()
+ if pres['type'] == 'error':
+ raise PresenceError(pres)
done, pending = await asyncio.wait(
- [future],
+ [topic_received],
timeout=timeout,
)
- if pending:
- raise asyncio.TimeoutError()
- pres = await future
- if pres['type'] == 'error':
- raise PresenceError(pres)
+ if pending:
+ raise asyncio.TimeoutError()
+ subject: Message = topic_received.result()
# update known nick in case it has changed
self.our_nicks[room] = pres['from'].resource
- return pres
+ return (pres, subject, occupant_buffer, history_buffer)
def join_muc(self, room: JID, nick: str, maxhistory="0", password='',
pstatus='', pshow='', pfrom='') -> asyncio.Future:
@@ -456,6 +493,8 @@ class XEP_0045(BasePlugin):
"""
if affiliation not in AFFILIATIONS:
raise ValueError('%s is not a valid affiliation' % affiliation)
+ if affiliation == 'outcast' and not jid:
+ raise ValueError('Outcast affiliation requires a using a jid')
if not any((jid, nick)):
raise ValueError('One of jid or nick must be set')
iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
diff --git a/slixmpp/plugins/xep_0045/stanza.py b/slixmpp/plugins/xep_0045/stanza.py
index 71eb3fb5..6c3d2244 100644
--- a/slixmpp/plugins/xep_0045/stanza.py
+++ b/slixmpp/plugins/xep_0045/stanza.py
@@ -179,6 +179,21 @@ class MUCInvite(ElementBase):
interfaces = {'to', 'from', 'reason'}
sub_interfaces = {'reason'}
+ def get_to(self) -> JID:
+ return JID(self._get_attr('to'))
+
+ def set_to(self, value: Union[JID, str]):
+ if not isinstance(value, JID):
+ value = JID(value)
+ self._set_attr('to', value)
+
+ def get_from(self) -> JID:
+ return JID(self._get_attr('from'))
+
+ def set_from(self, value: Union[JID, str]):
+ if not isinstance(value, JID):
+ value = JID(value)
+ self._set_attr('from', value)
class MUCDecline(ElementBase):
name = 'decline'
@@ -187,6 +202,22 @@ class MUCDecline(ElementBase):
interfaces = {'to', 'from', 'reason'}
sub_interfaces = {'reason'}
+ def get_to(self) -> JID:
+ return JID(self._get_attr('to'))
+
+ def set_to(self, value: Union[JID, str]):
+ if not isinstance(value, JID):
+ value = JID(value)
+ self._set_attr('to', value)
+
+ def get_from(self) -> JID:
+ return JID(self._get_attr('from'))
+
+ def set_from(self, value: Union[JID, str]):
+ if not isinstance(value, JID):
+ value = JID(value)
+ self._set_attr('from', value)
+
class MUCHistory(ElementBase):
name = 'history'
@@ -204,6 +235,7 @@ class MUCOwnerQuery(ElementBase):
class MUCOwnerDestroy(ElementBase):
name = 'destroy'
plugin_attrib = 'destroy'
+ namespace = NS_OWNER
interfaces = {'reason', 'jid'}
sub_interfaces = {'reason'}
@@ -257,3 +289,11 @@ class MUCActor(ElementBase):
if jid:
return JID(jid)
return jid
+
+
+class MUCDestroy(ElementBase):
+ name = 'destroy'
+ plugin_attrib = 'destroy'
+ namespace = NS_USER
+ interfaces = {'reason', 'jid'}
+ sub_interfaces = {'reason'}
diff --git a/slixmpp/plugins/xep_0047/stream.py b/slixmpp/plugins/xep_0047/stream.py
index f020ea68..29e70707 100644
--- a/slixmpp/plugins/xep_0047/stream.py
+++ b/slixmpp/plugins/xep_0047/stream.py
@@ -99,6 +99,13 @@ class IBBytestream(object):
:returns: All bytes accumulated in the stream.
"""
result = b''
+ while not self.recv_queue.empty():
+ result += self.recv_queue.get_nowait()
+ if max_data and len(result) > max_data:
+ return result
+ if self.stream_in_closed:
+ return result
+
end_future = asyncio.Future()
def on_close(stream):
@@ -115,7 +122,7 @@ class IBBytestream(object):
self.xmpp.add_event_handler('ibb_stream_end', on_close)
self.xmpp.add_event_handler('ibb_stream_data', on_data)
try:
- await asyncio.wait_for(end_future, timeout, loop=self.xmpp.loop)
+ await asyncio.wait_for(end_future, timeout)
except asyncio.TimeoutError:
raise IqTimeout(result)
finally:
diff --git a/slixmpp/plugins/xep_0055/__init__.py b/slixmpp/plugins/xep_0055/__init__.py
new file mode 100644
index 00000000..981cf960
--- /dev/null
+++ b/slixmpp/plugins/xep_0055/__init__.py
@@ -0,0 +1,6 @@
+from slixmpp.plugins.base import register_plugin
+
+from .search import XEP_0055
+
+
+register_plugin(XEP_0055)
diff --git a/slixmpp/plugins/xep_0055/search.py b/slixmpp/plugins/xep_0055/search.py
new file mode 100644
index 00000000..a45b52a7
--- /dev/null
+++ b/slixmpp/plugins/xep_0055/search.py
@@ -0,0 +1,89 @@
+import logging
+
+from slixmpp import CoroutineCallback, StanzaPath, Iq, register_stanza_plugin
+from slixmpp.plugins import BasePlugin
+from slixmpp.xmlstream import StanzaBase
+
+from . import stanza
+
+
+class XEP_0055(BasePlugin):
+ """
+ XEP-0055: Jabber Search
+
+ The config options are only useful for a "server-side" search feature,
+ and if the ``provide_search`` option is set to True.
+
+ API
+ ===
+
+ ``search_get_form``: customize the search form content (ie fields)
+
+ ``search_query``: return search results
+ """
+ name = "xep_0055"
+ description = "XEP-0055: Jabber search"
+ dependencies = {"xep_0004", "xep_0030"}
+ stanza = stanza
+ default_config = {
+ "form_fields": {"first", "last"},
+ "form_instructions": "",
+ "form_title": "",
+ "provide_search": True
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.Search)
+ register_stanza_plugin(stanza.Search, self.xmpp["xep_0004"].stanza.Form)
+
+ if self.provide_search:
+ self.xmpp["xep_0030"].add_feature(stanza.Search.namespace)
+ self.xmpp.register_handler(
+ CoroutineCallback(
+ "search",
+ StanzaPath("/iq/search"),
+ self._handle_search,
+ )
+ )
+ self.api.register(self._get_form, "search_get_form")
+ self.api.register(self._get_results, "search_query")
+
+ async def _handle_search(self, iq: StanzaBase):
+ if iq["search"]["form"].get_values():
+ reply = await self.api["search_query"](None, None, iq.get_from(), iq)
+ reply["search"]["form"]["type"] = "result"
+ else:
+ reply = await self.api["search_get_form"](None, None, iq.get_from(), iq)
+ reply["search"]["form"].add_field(
+ "FORM_TYPE", value=stanza.Search.namespace, ftype="hidden"
+ )
+ reply.send()
+
+ async def _get_form(self, jid, node, ifrom, iq):
+ reply = iq.reply()
+ form = reply["search"]["form"]
+ form["title"] = self.form_title
+ form["instructions"] = self.form_instructions
+ for field in self.form_fields:
+ form.add_field(field)
+ return reply
+
+ async def _get_results(self, jid, node, ifrom, iq):
+ reply = iq.reply()
+ form = reply["search"]["form"]
+ form["type"] = "result"
+
+ for field in self.form_fields:
+ form.add_reported(field)
+ return reply
+
+ def make_search_iq(self, **kwargs):
+ iq = self.xmpp.make_iq(itype="set", **kwargs)
+ iq["search"]["form"].set_type("submit")
+ iq["search"]["form"].add_field(
+ "FORM_TYPE", value=stanza.Search.namespace, ftype="hidden"
+ )
+ return iq
+
+
+log = logging.getLogger(__name__)
diff --git a/slixmpp/plugins/xep_0055/stanza.py b/slixmpp/plugins/xep_0055/stanza.py
new file mode 100644
index 00000000..18bccf7e
--- /dev/null
+++ b/slixmpp/plugins/xep_0055/stanza.py
@@ -0,0 +1,10 @@
+from typing import Set, ClassVar
+
+from slixmpp.xmlstream import ElementBase
+
+
+class Search(ElementBase):
+ namespace = "jabber:iq:search"
+ name = "query"
+ plugin_attrib = "search"
+ interfaces: ClassVar[Set[str]] = set()
diff --git a/slixmpp/plugins/xep_0082.py b/slixmpp/plugins/xep_0082.py
index e8050286..0cdee465 100644
--- a/slixmpp/plugins/xep_0082.py
+++ b/slixmpp/plugins/xep_0082.py
@@ -6,7 +6,6 @@
import datetime as dt
from slixmpp.plugins import BasePlugin, register_plugin
-from slixmpp.thirdparty import tzutc, tzoffset, parse_iso
# =====================================================================
@@ -21,7 +20,10 @@ def parse(time_str):
Arguments:
time_str -- A formatted timestamp string.
"""
- return parse_iso(time_str)
+ try:
+ return dt.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S.%f%z')
+ except ValueError:
+ return dt.datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S%z')
def format_date(time_obj):
@@ -52,7 +54,7 @@ def format_time(time_obj):
if isinstance(time_obj, dt.datetime):
time_obj = time_obj.timetz()
timestamp = time_obj.isoformat()
- if time_obj.tzinfo == tzutc():
+ if time_obj.tzinfo == dt.timezone.utc:
timestamp = timestamp[:-6]
return '%sZ' % timestamp
return timestamp
@@ -69,7 +71,7 @@ def format_datetime(time_obj):
time_obj -- A datetime object.
"""
timestamp = time_obj.isoformat('T')
- if time_obj.tzinfo == tzutc():
+ if time_obj.tzinfo == dt.timezone.utc:
timestamp = timestamp[:-6]
return '%sZ' % timestamp
return timestamp
@@ -128,9 +130,9 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False):
if micro is None:
micro = now.microsecond
if offset in (None, 0):
- offset = tzutc()
+ offset = dt.timezone.utc
elif not isinstance(offset, dt.tzinfo):
- offset = tzoffset(None, offset)
+ offset = dt.timezone(dt.timedelta(seconds=offset))
value = dt.time(hour, min, sec, micro, offset)
if obj:
return value
@@ -175,9 +177,9 @@ def datetime(year=None, month=None, day=None, hour=None,
if micro is None:
micro = now.microsecond
if offset in (None, 0):
- offset = tzutc()
+ offset = dt.timezone.utc
elif not isinstance(offset, dt.tzinfo):
- offset = tzoffset(None, offset)
+ offset = dt.timezone(dt.timedelta(seconds=offset))
value = dt.datetime(year, month, day, hour,
min, sec, micro, offset)
diff --git a/slixmpp/plugins/xep_0084/stanza.py b/slixmpp/plugins/xep_0084/stanza.py
index a2132c0b..ed83526e 100644
--- a/slixmpp/plugins/xep_0084/stanza.py
+++ b/slixmpp/plugins/xep_0084/stanza.py
@@ -80,16 +80,16 @@ class Info(ElementBase):
self._set_int('bytes', value)
def get_height(self) -> int:
- self._get_int('height')
+ return self._get_int('height')
def set_height(self, value: int):
self._set_int('height', value)
def get_width(self) -> int:
- self._get_int(self, 'width')
+ return self._get_int('width')
def set_width(self, value: int):
- self._set_int('with', value)
+ self._set_int('width', value)
class Pointer(ElementBase):
diff --git a/slixmpp/plugins/xep_0115/static.py b/slixmpp/plugins/xep_0115/static.py
index 74f2beb8..4bf77d75 100644
--- a/slixmpp/plugins/xep_0115/static.py
+++ b/slixmpp/plugins/xep_0115/static.py
@@ -60,7 +60,7 @@ class StaticCaps(object):
return False
if node in (None, ''):
- info = self.caps.get_caps(jid)
+ info = await self.caps.get_caps(jid)
if info and feature in info['features']:
return True
@@ -134,7 +134,7 @@ class StaticCaps(object):
def get_verstring(self, jid, node, ifrom, data):
return self.jid_vers.get(jid, None)
- def get_caps(self, jid, node, ifrom, data):
+ async def get_caps(self, jid, node, ifrom, data):
verstring = data.get('verstring', None)
if verstring is None:
return None
diff --git a/slixmpp/plugins/xep_0202/stanza.py b/slixmpp/plugins/xep_0202/stanza.py
index 7d09de50..faa230f9 100644
--- a/slixmpp/plugins/xep_0202/stanza.py
+++ b/slixmpp/plugins/xep_0202/stanza.py
@@ -8,7 +8,6 @@ import datetime as dt
from slixmpp.xmlstream import ElementBase
from slixmpp.plugins import xep_0082
-from slixmpp.thirdparty import tzutc, tzoffset
class EntityTime(ElementBase):
@@ -87,7 +86,7 @@ class EntityTime(ElementBase):
seconds (positive or negative) to offset.
"""
time = xep_0082.time(offset=value)
- if xep_0082.parse(time).tzinfo == tzutc():
+ if xep_0082.parse(time).tzinfo == dt.timezone.utc:
self._set_sub_text('tzo', 'Z')
else:
self._set_sub_text('tzo', time[-6:])
@@ -111,6 +110,6 @@ class EntityTime(ElementBase):
date = value
if not isinstance(value, dt.datetime):
date = xep_0082.parse(value)
- date = date.astimezone(tzutc())
+ date = date.astimezone(dt.timezone.utc)
value = xep_0082.format_datetime(date)
self._set_sub_text('utc', value)
diff --git a/slixmpp/plugins/xep_0203/stanza.py b/slixmpp/plugins/xep_0203/stanza.py
index f173d41c..a84cb52f 100644
--- a/slixmpp/plugins/xep_0203/stanza.py
+++ b/slixmpp/plugins/xep_0203/stanza.py
@@ -30,6 +30,10 @@ class Delay(ElementBase):
def set_stamp(self, value):
if isinstance(value, dt.datetime):
+ if value.tzinfo is None:
+ raise ValueError(f'Datetime provided without timezone information: {value}')
+ if value.tzinfo != dt.timezone.utc:
+ value = value.astimezone(dt.timezone.utc)
value = xep_0082.format_datetime(value)
self._set_attr('stamp', value)
diff --git a/slixmpp/plugins/xep_0313/mam.py b/slixmpp/plugins/xep_0313/mam.py
index 02efd3ce..f49ac637 100644
--- a/slixmpp/plugins/xep_0313/mam.py
+++ b/slixmpp/plugins/xep_0313/mam.py
@@ -149,6 +149,8 @@ class XEP_0313(BasePlugin):
"""
Iterate over each message of MAM query.
+ .. versionadded:: 1.8.0
+
:param jid: Entity holding the MAM records
:param start: MAM query start time
:param end: MAM query end time
@@ -239,7 +241,7 @@ class XEP_0313(BasePlugin):
async def get_fields(self, jid: Optional[JID] = None, **iqkwargs) -> Form:
"""Get MAM query fields.
- .. versionaddedd:: 1.8.0
+ .. versionadded:: 1.8.0
:param jid: JID to retrieve the policy from.
:return: The Form of allowed options
diff --git a/slixmpp/plugins/xep_0333/markers.py b/slixmpp/plugins/xep_0333/markers.py
index ca7a27da..86ddf612 100644
--- a/slixmpp/plugins/xep_0333/markers.py
+++ b/slixmpp/plugins/xep_0333/markers.py
@@ -1,4 +1,3 @@
-
# slixmpp: The Slick XMPP Library
# Copyright (C) 2016 Emmanuel Gil Peyrot
# This file is part of slixmpp.
@@ -68,11 +67,11 @@ class XEP_0333(BasePlugin):
:param JID mto: recipient of the marker
:param str id: Identifier of the marked message
:param str marker: Marker to send (one of
- displayed, retrieved, or acknowledged)
+ displayed, received, or acknowledged)
:param str thread: Message thread
:param str mfrom: Use a specific JID to send the message
"""
- if marker not in ('displayed', 'retrieved', 'acknowledged'):
+ if marker not in ('displayed', 'received', 'acknowledged'):
raise ValueError('Invalid marker: %s' % marker)
msg = self.xmpp.make_message(mto=mto, mfrom=mfrom)
if thread:
diff --git a/slixmpp/plugins/xep_0356/stanza.py b/slixmpp/plugins/xep_0356/stanza.py
index ef01ee3e..46f1523a 100644
--- a/slixmpp/plugins/xep_0356/stanza.py
+++ b/slixmpp/plugins/xep_0356/stanza.py
@@ -7,7 +7,7 @@ from slixmpp.plugins.xep_0297 import Forwarded
class Privilege(ElementBase):
- namespace = "urn:xmpp:privilege:1"
+ namespace = "urn:xmpp:privilege:2"
name = "privilege"
plugin_attrib = "privilege"
@@ -24,7 +24,10 @@ class Privilege(ElementBase):
def presence(self):
return self.permission("presence")
-
+
+ def iq(self):
+ return self.permission("iq")
+
def add_perm(self, access, type):
# This should only be needed for servers, so maybe out of scope for slixmpp
perm = Perm()
@@ -34,7 +37,7 @@ class Privilege(ElementBase):
class Perm(ElementBase):
- namespace = "urn:xmpp:privilege:1"
+ namespace = "urn:xmpp:privilege:2"
name = "perm"
plugin_attrib = "perm"
plugin_multi_attrib = "perms"
@@ -44,4 +47,4 @@ class Perm(ElementBase):
def register():
register_stanza_plugin(Message, Privilege)
register_stanza_plugin(Privilege, Forwarded)
- register_stanza_plugin(Privilege, Perm, iterable=True) \ No newline at end of file
+ register_stanza_plugin(Privilege, Perm, iterable=True)
diff --git a/slixmpp/plugins/xep_0363/http_upload.py b/slixmpp/plugins/xep_0363/http_upload.py
index bae3ee7d..c34be8ff 100644
--- a/slixmpp/plugins/xep_0363/http_upload.py
+++ b/slixmpp/plugins/xep_0363/http_upload.py
@@ -14,6 +14,8 @@ from typing import (
IO,
)
+from pathlib import Path
+
from slixmpp import JID, __version__
from slixmpp.stanza import Iq
from slixmpp.plugins import BasePlugin
@@ -99,12 +101,17 @@ class XEP_0363(BasePlugin):
:param domain: Domain to disco to find a service.
"""
+ if domain is None and self.xmpp.is_component:
+ domain = self.xmpp.server_host
+
results = await self.xmpp['xep_0030'].get_info_from_domain(
domain=domain, **iqkwargs
)
candidates = []
for info in results:
+ if not info['disco_info']:
+ continue
for identity in info['disco_info']['identities']:
if identity[0] == 'store' and identity[1] == 'file':
candidates.append(info)
@@ -113,7 +120,7 @@ class XEP_0363(BasePlugin):
if feature == Request.namespace:
return info
- def request_slot(self, jid: JID, filename: str, size: int,
+ def request_slot(self, jid: JID, filename: Path, size: int,
content_type: Optional[str] = None, *,
ifrom: Optional[JID] = None, **iqkwargs) -> Future:
"""Request an HTTP upload slot from a service.
@@ -125,12 +132,12 @@ class XEP_0363(BasePlugin):
"""
iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom)
request = iq['http_upload_request']
- request['filename'] = filename
+ request['filename'] = str(filename)
request['size'] = str(size)
request['content-type'] = content_type or self.default_content_type
return iq.send(**iqkwargs)
- async def upload_file(self, filename: str, size: Optional[int] = None,
+ async def upload_file(self, filename: Path, size: Optional[int] = None,
content_type: Optional[str] = None, *,
input_file: Optional[IO[bytes]]=None,
domain: Optional[JID] = None,
diff --git a/slixmpp/plugins/xep_0425/moderation.py b/slixmpp/plugins/xep_0425/moderation.py
index 053e18f6..3c1308fc 100644
--- a/slixmpp/plugins/xep_0425/moderation.py
+++ b/slixmpp/plugins/xep_0425/moderation.py
@@ -44,4 +44,5 @@ class XEP_0425(BasePlugin):
iq = self.xmpp.make_iq_set(ito=room.bare, ifrom=ifrom)
iq['apply_to']['id'] = id
iq['apply_to']['moderate']['reason'] = reason
+ iq['apply_to']['moderate'].enable('retract')
await iq.send(**iqkwargs)
diff --git a/slixmpp/plugins/xep_0444/stanza.py b/slixmpp/plugins/xep_0444/stanza.py
index 02684df1..c9ee07d7 100644
--- a/slixmpp/plugins/xep_0444/stanza.py
+++ b/slixmpp/plugins/xep_0444/stanza.py
@@ -6,9 +6,7 @@
from typing import Set, Iterable
from slixmpp.xmlstream import ElementBase
try:
- from emoji import UNICODE_EMOJI
- if UNICODE_EMOJI.get('en'):
- UNICODE_EMOJI = UNICODE_EMOJI['en']
+ from emoji import EMOJI_DATA as UNICODE_EMOJI
except ImportError:
UNICODE_EMOJI = None
diff --git a/slixmpp/plugins/xep_0454/__init__.py b/slixmpp/plugins/xep_0454/__init__.py
new file mode 100644
index 00000000..d537432d
--- /dev/null
+++ b/slixmpp/plugins/xep_0454/__init__.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8 et ts=4 sts=4 sw=4
+#
+# Copyright © 2022 Maxime “pep” Buquet <pep@bouah.net>
+#
+# See the LICENSE file for copying permissions.
+
+"""
+ XEP-0454: OMEMO Media Sharing
+"""
+
+from typing import IO, Optional, Tuple
+
+from os import urandom
+from pathlib import Path
+from io import BytesIO, SEEK_END
+
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+
+from slixmpp.plugins import BasePlugin
+from slixmpp.plugins.base import register_plugin
+
+
+class InvalidURL(Exception):
+ """Raised for URLs that either aren't HTTPS or already contain a fragment."""
+
+
+EXTENSIONS_MAP = {
+ 'jpeg': 'jpg',
+ 'text': 'txt',
+}
+
+class XEP_0454(BasePlugin):
+ """
+ XEP-0454: OMEMO Media Sharing
+ """
+
+ name = 'xep_0454'
+ description = 'XEP-0454: OMEMO Media Sharing'
+ dependencies = {'xep_0363'}
+
+ @staticmethod
+ def encrypt(input_file: Optional[IO[bytes]] = None, filename: Optional[Path] = None) -> Tuple[bytes, str]:
+ """
+ Encrypts file as specified in XEP-0454 for use in file sharing
+
+ :param input_file: Binary file stream on the file.
+ :param filename: Path to the file to upload.
+
+ One of input_file or filename must be specified. If both are
+ passed, input_file will be used and filename ignored.
+ """
+ if input_file is None and filename is None:
+ raise ValueError('Specify either filename or input_file parameter')
+
+ aes_gcm_iv = urandom(12)
+ aes_gcm_key = urandom(32)
+
+ aes_gcm = Cipher(
+ algorithms.AES(aes_gcm_key),
+ modes.GCM(aes_gcm_iv),
+ ).encryptor()
+
+ if input_file is None:
+ input_file = open(filename, 'rb')
+
+ payload = b''
+ while True:
+ buf = input_file.read(4096)
+ if not buf:
+ break
+ payload += aes_gcm.update(buf)
+
+ payload += aes_gcm.finalize() + aes_gcm.tag
+ fragment = aes_gcm_iv.hex() + aes_gcm_key.hex()
+ return (payload, fragment)
+
+ @staticmethod
+ def decrypt(input_file: IO[bytes], fragment: str) -> bytes:
+ """
+ Decrypts file-like.
+
+ :param input_file: Binary file stream on the file, containing the
+ tag (16 bytes) at the end.
+ :param fragment: 88 hex chars string composed of iv (24 chars)
+ + key (64 chars).
+ """
+
+ assert len(fragment) == 88
+ aes_gcm_iv = bytes.fromhex(fragment[:24])
+ aes_gcm_key = bytes.fromhex(fragment[24:])
+
+ # Find 16 bytes tag
+ input_file.seek(-16, SEEK_END)
+ tag = input_file.read()
+
+ aes_gcm = Cipher(
+ algorithms.AES(aes_gcm_key),
+ modes.GCM(aes_gcm_iv, tag),
+ ).decryptor()
+
+ size = input_file.seek(0, SEEK_END)
+ input_file.seek(0)
+
+ count = size - 16
+ plain = b''
+ while count >= 0:
+ buf = input_file.read(4096)
+ count -= len(buf)
+ if count <= 0:
+ buf += input_file.read()
+ buf = buf[:-16]
+ plain += aes_gcm.update(buf)
+ plain += aes_gcm.finalize()
+
+ return plain
+
+ @staticmethod
+ def format_url(url: str, fragment: str) -> str:
+ """Helper to format a HTTPS URL to an AESGCM URI"""
+ if not url.startswith('https://') or url.find('#') != -1:
+ raise InvalidURL
+ return 'aesgcm://' + url[len('https://'):] + '#' + fragment
+
+ @staticmethod
+ def map_extensions(ext: str) -> str:
+ """
+ Apply conversions to extensions to reduce the number of
+ variations, (e.g., JPEG -> jpg).
+ """
+ return EXTENSIONS_MAP.get(ext, ext).lower()
+
+ async def upload_file(
+ self,
+ filename: Path,
+ _size: Optional[int] = None,
+ content_type: Optional[str] = None,
+ **kwargs,
+ ) -> str:
+ """
+ Wrapper to xep_0363 (HTTP Upload)'s upload_file method.
+
+ :param input_file: Binary file stream on the file.
+ :param filename: Path to the file to upload.
+
+ Same as `XEP_0454.encrypt`, one of input_file or filename must be
+ specified. If both are passed, input_file will be used and
+ filename ignored.
+
+ Other arguments passed in are passed to the actual
+ `XEP_0363.upload_file` call.
+ """
+ input_file = kwargs.get('input_file')
+ payload, fragment = self.encrypt(input_file, filename)
+
+ # Prepare kwargs for upload_file call
+ new_filename = urandom(12).hex() # Random filename to hide user-provided path
+ if filename.suffix:
+ new_filename += self.map_extensions(filename.suffix)
+ kwargs['filename'] = new_filename
+
+ input_enc = BytesIO(payload)
+ kwargs['input_file'] = input_enc
+
+ # Size must also be overriden if provided
+ size = input_enc.seek(0, SEEK_END)
+ input_enc.seek(0)
+ kwargs['size'] = size
+
+ kwargs['content_type'] = content_type
+
+ url = await self.xmpp['xep_0363'].upload_file(**kwargs)
+ return self.format_url(url, fragment)
+
+register_plugin(XEP_0454)
diff --git a/slixmpp/plugins/xep_0461/__init__.py b/slixmpp/plugins/xep_0461/__init__.py
new file mode 100644
index 00000000..1e9b2829
--- /dev/null
+++ b/slixmpp/plugins/xep_0461/__init__.py
@@ -0,0 +1,6 @@
+from slixmpp.plugins.base import register_plugin
+
+from .reply import XEP_0461
+from . import stanza
+
+register_plugin(XEP_0461)
diff --git a/slixmpp/plugins/xep_0461/reply.py b/slixmpp/plugins/xep_0461/reply.py
new file mode 100644
index 00000000..6607012a
--- /dev/null
+++ b/slixmpp/plugins/xep_0461/reply.py
@@ -0,0 +1,48 @@
+from slixmpp.plugins import BasePlugin
+from slixmpp.types import JidStr
+from slixmpp.xmlstream import StanzaBase
+from slixmpp.xmlstream.handler import Callback
+from slixmpp.xmlstream.matcher import StanzaPath
+
+from . import stanza
+
+
+class XEP_0461(BasePlugin):
+ """XEP-0461: Message Replies"""
+
+ name = "xep_0461"
+ description = "XEP-0461: Message Replies"
+
+ dependencies = {"xep_0030"}
+ stanza = stanza
+ namespace = stanza.NS
+
+ def plugin_init(self) -> None:
+ stanza.register_plugins()
+ self.xmpp.register_handler(
+ Callback(
+ "Message replied to",
+ StanzaPath("message/reply"),
+ self._handle_reply_to_message,
+ )
+ )
+
+ def plugin_end(self):
+ self.xmpp.plugin["xep_0030"].del_feature(feature=stanza.NS)
+
+ def session_bind(self, jid):
+ self.xmpp.plugin["xep_0030"].add_feature(feature=stanza.NS)
+
+ def _handle_reply_to_message(self, msg: StanzaBase):
+ self.xmpp.event("message_reply", msg)
+
+ def send_reply(self, reply_to: JidStr, reply_id: str, **msg_kwargs):
+ """
+
+ :param reply_to: Full JID of the quoted author
+ :param reply_id: ID of the message to reply to
+ """
+ msg = self.xmpp.make_message(**msg_kwargs)
+ msg["reply"]["to"] = reply_to
+ msg["reply"]["id"] = reply_id
+ msg.send()
diff --git a/slixmpp/plugins/xep_0461/stanza.py b/slixmpp/plugins/xep_0461/stanza.py
new file mode 100644
index 00000000..b99b2745
--- /dev/null
+++ b/slixmpp/plugins/xep_0461/stanza.py
@@ -0,0 +1,47 @@
+from slixmpp.stanza import Message
+from slixmpp.xmlstream import ElementBase, register_stanza_plugin
+
+NS = "urn:xmpp:reply:0"
+
+
+class Reply(ElementBase):
+ namespace = NS
+ name = "reply"
+ plugin_attrib = "reply"
+ interfaces = {"id", "to"}
+
+
+class FeatureFallBack(ElementBase):
+ # should also be a multi attrib
+ namespace = "urn:xmpp:feature-fallback:0"
+ name = "fallback"
+ plugin_attrib = "feature_fallback"
+ interfaces = {"for"}
+
+ def get_stripped_body(self):
+ # only works for a single fallback_body attrib
+ start = self["fallback_body"]["start"]
+ end = self["fallback_body"]["end"]
+ body = self.parent()["body"]
+ try:
+ start = int(start)
+ end = int(end)
+ except ValueError:
+ return body
+ else:
+ return body[:start] + body[end:]
+
+
+class FallBackBody(ElementBase):
+ # According to https://xmpp.org/extensions/inbox/compatibility-fallback.html
+ # this should be a multi_attrib *but* since it's a protoXEP, we'll see...
+ namespace = FeatureFallBack.namespace
+ name = "body"
+ plugin_attrib = "fallback_body"
+ interfaces = {"start", "end"}
+
+
+def register_plugins():
+ register_stanza_plugin(Message, Reply)
+ register_stanza_plugin(Message, FeatureFallBack)
+ register_stanza_plugin(FeatureFallBack, FallBackBody)
diff --git a/slixmpp/stanza/message.py b/slixmpp/stanza/message.py
index 50d32ff0..eda11df6 100644
--- a/slixmpp/stanza/message.py
+++ b/slixmpp/stanza/message.py
@@ -64,9 +64,9 @@ class Message(RootStanza):
if self.stream:
use_ids = getattr(self.stream, 'use_message_ids', None)
if use_ids:
- self['id'] = self.stream.new_id()
+ self.set_id(self.stream.new_id())
else:
- del self['origin_id']
+ self.del_origin_id()
def get_type(self):
"""
@@ -96,8 +96,8 @@ class Message(RootStanza):
self.xml.attrib['id'] = value
if self.stream:
- use_orig_ids = getattr(self.stream, 'use_origin_id', None)
- if not use_orig_ids:
+ if not getattr(self.stream, 'use_origin_id', False):
+ self.del_origin_id()
return None
sub = self.xml.find(ORIGIN_NAME)
diff --git a/slixmpp/thirdparty/__init__.py b/slixmpp/thirdparty/__init__.py
index d950f4f9..216a7b79 100644
--- a/slixmpp/thirdparty/__init__.py
+++ b/slixmpp/thirdparty/__init__.py
@@ -3,5 +3,4 @@ try:
except:
from slixmpp.thirdparty.gnupg import GPG
-from slixmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso
from slixmpp.thirdparty.orderedset import OrderedSet
diff --git a/slixmpp/thirdparty/mini_dateutil.py b/slixmpp/thirdparty/mini_dateutil.py
deleted file mode 100644
index 882a531f..00000000
--- a/slixmpp/thirdparty/mini_dateutil.py
+++ /dev/null
@@ -1,273 +0,0 @@
-# This module is a very stripped down version of the dateutil
-# package for when dateutil has not been installed. As a replacement
-# for dateutil.parser.parse, the parsing methods from
-# http://blog.mfabrik.com/2008/06/30/relativity-of-time-shortcomings-in-python-datetime-and-workaround/
-
-#As such, the following copyrights and licenses applies:
-
-
-# dateutil - Extensions to the standard python 2.3+ datetime module.
-#
-# Copyright (c) 2003-2011 - Gustavo Niemeyer <gustavo@niemeyer.net>
-#
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# * Redistributions of source code must retain the above copyright notice,
-# this list of conditions and the following disclaimer.
-# * Redistributions in binary form must reproduce the above copyright notice,
-# this list of conditions and the following disclaimer in the documentation
-# and/or other materials provided with the distribution.
-# * Neither the name of the copyright holder nor the names of its
-# contributors may be used to endorse or promote products derived from
-# this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
-# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
-# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
-# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
-# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-# fixed_dateime
-#
-# Copyright (c) 2008, Red Innovation Ltd., Finland
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# * Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-# * Neither the name of Red Innovation nor the names of its contributors
-# may be used to endorse or promote products derived from this software
-# without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY RED INNOVATION ``AS IS'' AND ANY
-# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL RED INNOVATION BE LIABLE FOR ANY
-# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
-
-import re
-import math
-import datetime
-
-
-ZERO = datetime.timedelta(0)
-
-
-try:
- from dateutil.parser import parse as parse_iso
- from dateutil.tz import tzoffset, tzutc
-except:
- # As a stopgap, define the two timezones here based
- # on the dateutil code.
-
- class tzutc(datetime.tzinfo):
-
- def utcoffset(self, dt):
- return ZERO
-
- def dst(self, dt):
- return ZERO
-
- def tzname(self, dt):
- return "UTC"
-
- def __eq__(self, other):
- return (isinstance(other, tzutc) or
- (isinstance(other, tzoffset) and other._offset == ZERO))
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
- def __repr__(self):
- return "%s()" % self.__class__.__name__
-
- __reduce__ = object.__reduce__
-
- class tzoffset(datetime.tzinfo):
-
- def __init__(self, name, offset):
- self._name = name
- self._offset = datetime.timedelta(minutes=offset)
-
- def utcoffset(self, dt):
- return self._offset
-
- def dst(self, dt):
- return ZERO
-
- def tzname(self, dt):
- return self._name
-
- def __eq__(self, other):
- return (isinstance(other, tzoffset) and
- self._offset == other._offset)
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
- def __repr__(self):
- return "%s(%s, %s)" % (self.__class__.__name__,
- repr(self._name),
- self._offset.days*86400+self._offset.seconds)
-
- __reduce__ = object.__reduce__
-
-
- _fixed_offset_tzs = { }
- UTC = tzutc()
-
- def _get_fixed_offset_tz(offsetmins):
- """For internal use only: Returns a tzinfo with
- the given fixed offset. This creates only one instance
- for each offset; the zones are kept in a dictionary"""
-
- if offsetmins == 0:
- return UTC
-
- if not offsetmins in _fixed_offset_tzs:
- if offsetmins < 0:
- sign = '-'
- absoff = -offsetmins
- else:
- sign = '+'
- absoff = offsetmins
-
- name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
- inst = tzoffset(name,offsetmins)
- _fixed_offset_tzs[offsetmins] = inst
-
- return _fixed_offset_tzs[offsetmins]
-
-
- _iso8601_parser = re.compile(r"""
- ^
- (?P<year> [0-9]{4})?(?P<ymdsep>-?)?
- (?P<month>[0-9]{2})?(?P=ymdsep)?
- (?P<day> [0-9]{2})?
-
- (?P<time>
- (?: # time part... optional... at least hour must be specified
- (?:T|\s+)?
- (?P<hour>[0-9]{2})
- (?:
- # minutes, separated with :, or none, from hours
- (?P<hmssep>[:]?)
- (?P<minute>[0-9]{2})
- (?:
- # same for seconds, separated with :, or none, from hours
- (?P=hmssep)
- (?P<second>[0-9]{2})
- )?
- )?
-
- # fractions
- (?: [,.] (?P<frac>[0-9]{1,10}))?
-
- # timezone, Z, +-hh or +-hh:?mm. MUST BE, but complain if not there.
- (
- (?P<tzempty>Z)
- |
- (?P<tzh>[+-][0-9]{2})
- (?: :? # optional separator
- (?P<tzm>[0-9]{2})
- )?
- )?
- )
- )?
- $
- """, re.X) # """
-
- def parse_iso(timestamp):
- """Internal function for parsing a timestamp in
- ISO 8601 format"""
-
- timestamp = timestamp.strip()
-
- m = _iso8601_parser.match(timestamp)
- if not m:
- raise ValueError("Not a proper ISO 8601 timestamp!: %s" % timestamp)
-
- vals = m.groupdict()
- def_vals = {'year': 1970, 'month': 1, 'day': 1}
- for key in vals:
- if vals[key] is None:
- vals[key] = def_vals.get(key, 0)
- elif key not in ['time', 'ymdsep', 'hmssep', 'tzempty']:
- vals[key] = int(vals[key])
-
- year = vals['year']
- month = vals['month']
- day = vals['day']
-
- if m.group('time') is None:
- return datetime.date(year, month, day)
-
- h, min, s, us = None, None, None, 0
- frac = 0
- if m.group('tzempty') == None and m.group('tzh') == None:
- raise ValueError("Not a proper ISO 8601 timestamp: " +
- "missing timezone (Z or +hh[:mm])!")
-
- if m.group('frac'):
- frac = m.group('frac')
- power = len(frac)
- frac = int(frac) / 10.0 ** power
-
- if m.group('hour'):
- h = vals['hour']
-
- if m.group('minute'):
- min = vals['minute']
-
- if m.group('second'):
- s = vals['second']
-
- if frac != None:
- # ok, fractions of hour?
- if min == None:
- frac, min = math.modf(frac * 60.0)
- min = int(min)
-
- # fractions of second?
- if s == None:
- frac, s = math.modf(frac * 60.0)
- s = int(s)
-
- # and extract microseconds...
- us = int(frac * 1000000)
-
- if m.group('tzempty') == 'Z':
- offsetmins = 0
- else:
- # timezone: hour diff with sign
- offsetmins = vals['tzh'] * 60
- tzm = m.group('tzm')
-
- # add optional minutes
- if tzm != None:
- tzm = int(tzm)
- offsetmins += tzm if offsetmins > 0 else -tzm
-
- tz = _get_fixed_offset_tz(offsetmins)
- return datetime.datetime(year, month, day, h, min, s, us, tz)
diff --git a/slixmpp/version.py b/slixmpp/version.py
index 7cff2e74..bf67f729 100644
--- a/slixmpp/version.py
+++ b/slixmpp/version.py
@@ -1,4 +1,3 @@
-
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2010 Nathanael C. Fritz
# This file is part of Slixmpp.
@@ -6,5 +5,5 @@
# We don't want to have to import the entire library
# just to get the version info for setup.py
-__version__ = '1.7.0'
-__version_info__ = (1, 7, 0)
+__version__ = '1.8.2'
+__version_info__ = (1, 8, 2)
diff --git a/slixmpp/xmlstream/handler/waiter.py b/slixmpp/xmlstream/handler/waiter.py
index dde49754..599004b5 100644
--- a/slixmpp/xmlstream/handler/waiter.py
+++ b/slixmpp/xmlstream/handler/waiter.py
@@ -80,7 +80,7 @@ class Waiter(BaseHandler):
try:
await wait_for(
- self._event.wait(), timeout, loop=stream.loop
+ self._event.wait(), timeout,
)
except TimeoutError:
log.warning("Timed out waiting for %s", self.name)
diff --git a/slixmpp/xmlstream/resolver.py b/slixmpp/xmlstream/resolver.py
index e524da3b..3de6629d 100644
--- a/slixmpp/xmlstream/resolver.py
+++ b/slixmpp/xmlstream/resolver.py
@@ -15,7 +15,13 @@ from slixmpp.types import Protocol
log = logging.getLogger(__name__)
-class AnswerProtocol(Protocol):
+class GetHostByNameAnswerProtocol(Protocol):
+ name: str
+ aliases: List[str]
+ addresses: List[str]
+
+
+class QueryAnswerProtocol(Protocol):
host: str
priority: int
weight: int
@@ -23,6 +29,9 @@ class AnswerProtocol(Protocol):
class ResolverProtocol(Protocol):
+ def gethostbyname(self, host: str, socket_family: socket.AddressFamily) -> Future:
+ ...
+
def query(self, query: str, querytype: str) -> Future:
...
@@ -147,11 +156,6 @@ async def resolve(host: str, port: int, *, loop: AbstractEventLoop,
results = []
for host, port in hosts:
- if host == 'localhost':
- if use_ipv6:
- results.append((host, '::1', port))
- results.append((host, '127.0.0.1', port))
-
if use_ipv6:
aaaa = await get_AAAA(host, resolver=resolver,
use_aiodns=use_aiodns, loop=loop)
@@ -201,13 +205,13 @@ async def get_A(host: str, *, loop: AbstractEventLoop,
return []
# Using aiodns:
- future = resolver.query(host, 'A')
+ future = resolver.gethostbyname(host, socket.AF_INET)
try:
- recs = cast(Iterable[AnswerProtocol], await future)
+ recs = cast(GetHostByNameAnswerProtocol, await future)
except Exception as e:
log.debug('DNS: Exception while querying for %s A records: %s', host, e)
- recs = []
- return [rec.host for rec in recs]
+ return []
+ return [addr for addr in recs.addresses]
async def get_AAAA(host: str, *, loop: AbstractEventLoop,
@@ -249,13 +253,13 @@ async def get_AAAA(host: str, *, loop: AbstractEventLoop,
return []
# Using aiodns:
- future = resolver.query(host, 'AAAA')
+ future = resolver.gethostbyname(host, socket.AF_INET6)
try:
- recs = cast(Iterable[AnswerProtocol], await future)
+ recs = cast(GetHostByNameAnswerProtocol, await future)
except Exception as e:
log.debug('DNS: Exception while querying for %s AAAA records: %s', host, e)
- recs = []
- return [rec.host for rec in recs]
+ return []
+ return [addr for addr in recs.addresses]
async def get_SRV(host: str, port: int, service: str,
@@ -295,12 +299,12 @@ async def get_SRV(host: str, port: int, service: str,
try:
future = resolver.query('_%s._%s.%s' % (service, proto, host),
'SRV')
- recs = cast(Iterable[AnswerProtocol], await future)
+ recs = cast(Iterable[QueryAnswerProtocol], await future)
except Exception as e:
log.debug('DNS: Exception while querying for %s SRV records: %s', host, e)
return []
- answers: Dict[int, List[AnswerProtocol]] = {}
+ answers: Dict[int, List[QueryAnswerProtocol]] = {}
for rec in recs:
if rec.priority not in answers:
answers[rec.priority] = []
diff --git a/slixmpp/xmlstream/xmlstream.py b/slixmpp/xmlstream/xmlstream.py
index 30f99071..19c4ddcc 100644
--- a/slixmpp/xmlstream/xmlstream.py
+++ b/slixmpp/xmlstream/xmlstream.py
@@ -15,6 +15,7 @@ from typing import (
Coroutine,
Callable,
Iterator,
+ Iterable,
List,
Optional,
Set,
@@ -31,8 +32,10 @@ import functools
import logging
import socket as Socket
import ssl
-import weakref
import uuid
+import warnings
+import weakref
+import collections
from contextlib import contextmanager
import xml.etree.ElementTree as ET
@@ -46,6 +49,7 @@ from asyncio import (
iscoroutinefunction,
wait,
)
+from pathlib import Path
from slixmpp.types import FilterString
from slixmpp.xmlstream.tostring import tostring
@@ -74,6 +78,15 @@ class NotConnectedError(Exception):
"""
+class InvalidCABundle(Exception):
+ """
+ Exception raised when the CA Bundle file hasn't been found.
+ """
+
+ def __init__(self, path: Optional[Union[Path, Iterable[Path]]]):
+ self.path = path
+
+
_T = TypeVar('_T', str, ElementBase, StanzaBase)
@@ -161,7 +174,7 @@ class XMLStream(asyncio.BaseProtocol):
#:
#: On Mac OS X, certificates in the system keyring will
#: be consulted, even if they are not in the provided file.
- ca_certs: Optional[str]
+ ca_certs: Optional[Union[Path, Iterable[Path]]]
#: Path to a file containing a client certificate to use for
#: authenticating via SASL EXTERNAL. If set, there must also
@@ -449,7 +462,7 @@ class XMLStream(asyncio.BaseProtocol):
if self._connect_loop_wait > 0:
self.event('reconnect_delay', self._connect_loop_wait)
- await asyncio.sleep(self._connect_loop_wait, loop=self.loop)
+ await asyncio.sleep(self._connect_loop_wait)
record = await self._pick_dns_answer(self.default_domain)
if record is not None:
@@ -480,16 +493,11 @@ class XMLStream(asyncio.BaseProtocol):
except Socket.gaierror as e:
self.event('connection_failed',
'No DNS record available for %s' % self.default_domain)
+ self.reschedule_connection_attempt()
except OSError as e:
log.debug('Connection failed: %s', e)
self.event("connection_failed", e)
- if self._current_connection_attempt is None:
- return
- self._connect_loop_wait = self._connect_loop_wait * 2 + 1
- self._current_connection_attempt = asyncio.ensure_future(
- self._connect_routine(),
- loop=self.loop,
- )
+ self.reschedule_connection_attempt()
def process(self, *, forever: bool = True, timeout: Optional[int] = None) -> None:
"""Process all the available XMPP events (receiving or sending data on the
@@ -497,17 +505,27 @@ class XMLStream(asyncio.BaseProtocol):
timers, handling signal events, etc). If timeout is None, this
function will run forever. If timeout is a number, this function
will return after the given time in seconds.
+
+ Will be removed in slixmpp 1.9.0
+
+ :deprecated: 1.8.0
"""
+ warnings.warn(
+ 'This function will be removed in slixmpp 1.9 and above.'
+ ' Use the asyncio normal functions instead.',
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
if timeout is None:
if forever:
self.loop.run_forever()
else:
self.loop.run_until_complete(self.disconnected)
else:
- tasks: List[Future] = [asyncio.sleep(timeout, loop=self.loop)]
+ tasks: List[Awaitable] = [asyncio.sleep(timeout)]
if not forever:
tasks.append(self.disconnected)
- self.loop.run_until_complete(asyncio.wait(tasks, loop=self.loop))
+ self.loop.run_until_complete(asyncio.wait(tasks))
def init_parser(self) -> None:
"""init the XML parser. The parser must always be reset for each new
@@ -556,7 +574,7 @@ class XMLStream(asyncio.BaseProtocol):
stream=self,
top_level=True,
open_only=True))
- self.start_stream_handler(self.xml_root)
+ self.start_stream_handler(self.xml_root) # type:ignore
self.xml_depth += 1
if event == 'end':
self.xml_depth -= 1
@@ -615,6 +633,20 @@ class XMLStream(asyncio.BaseProtocol):
self._set_disconnected_future()
self.event("disconnected", self.disconnect_reason or exception)
+ def reschedule_connection_attempt(self) -> None:
+ """
+ Increase the exponential back-off and initate another background
+ _connect_routine call to connect to the server.
+ """
+ # abort if there is no ongoing connection attempt
+ if self._current_connection_attempt is None:
+ return
+ self._connect_loop_wait = min(300, self._connect_loop_wait * 2 + 1)
+ self._current_connection_attempt = asyncio.ensure_future(
+ self._connect_routine(),
+ loop=self.loop,
+ )
+
def cancel_connection_attempt(self) -> None:
"""
Immediately cancel the current create_connection() Future.
@@ -715,7 +747,7 @@ class XMLStream(asyncio.BaseProtocol):
log.debug("reconnecting...")
async def handler(event: Any) -> None:
# We yield here to allow synchronous handlers to work first
- await asyncio.sleep(0, loop=self.loop)
+ await asyncio.sleep(0)
self.connect()
self.add_event_handler('disconnected', handler, disposable=True)
self.disconnect(wait, reason)
@@ -759,8 +791,26 @@ class XMLStream(asyncio.BaseProtocol):
log.debug('Loaded cert file %s and key file %s',
self.certfile, self.keyfile)
if self.ca_certs is not None:
+ ca_cert: Optional[Path] = None
+ # XXX: Compat before d733c54518.
+ if isinstance(self.ca_certs, str):
+ self.ca_certs = Path(self.ca_certs)
+ if isinstance(self.ca_certs, Path):
+ if self.ca_certs.is_file():
+ ca_cert = self.ca_certs
+ else:
+ for bundle in self.ca_certs:
+ if bundle.is_file():
+ ca_cert = bundle
+ break
+ if ca_cert is None and \
+ isinstance(self.ca_certs, (Path, collections.abc.Iterable)):
+ raise InvalidCABundle(self.ca_certs)
+
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
- self.ssl_context.load_verify_locations(cafile=self.ca_certs)
+ self.ssl_context.load_verify_locations(cafile=ca_cert)
+ else:
+ self.ssl_context.set_default_verify_paths()
return self.ssl_context
@@ -1202,7 +1252,7 @@ class XMLStream(asyncio.BaseProtocol):
else:
self.send_raw(data)
- async def run_filters(self) -> NoReturn:
+ async def run_filters(self) -> None:
"""
Background loop that processes stanzas to send.
"""
@@ -1217,7 +1267,7 @@ class XMLStream(asyncio.BaseProtocol):
already_run_filters.add(filter)
if iscoroutinefunction(filter):
filter = cast(AsyncFilter, filter)
- task = asyncio.create_task(filter(data))
+ task = asyncio.create_task(filter(data)) # type:ignore
completed, pending = await wait(
{task},
timeout=1,
@@ -1258,6 +1308,9 @@ class XMLStream(asyncio.BaseProtocol):
self.send_raw(data)
except ContinueQueue as exc:
log.debug('Stanza in send queue not sent: %s', exc)
+ except asyncio.CancelledError:
+ log.debug('Send coroutine received cancel(), stopping')
+ return
except Exception:
log.error('Exception raised in send queue:', exc_info=True)
self.waiting_queue.task_done()
@@ -1278,10 +1331,16 @@ class XMLStream(asyncio.BaseProtocol):
# Avoid circular imports
from slixmpp.stanza.rootstanza import RootStanza
from slixmpp.stanza import Iq, Handshake
- passthrough = (
- (isinstance(data, Iq) and data.get_plugin('bind', check=True))
- or isinstance(data, Handshake)
- )
+
+ passthrough = False
+ if isinstance(data, Iq):
+ if data.get_plugin('bind', check=True):
+ passthrough = True
+ elif data.get_plugin('session', check=True):
+ passthrough = True
+ elif isinstance(data, Handshake):
+ passthrough = True
+
if isinstance(data, (RootStanza, str)) and not passthrough:
self.__queued_stanzas.append((data, use_filters))
log.debug('NOT SENT: %s %s', type(data), data)
diff --git a/tests/test_stanza_xep_0055.py b/tests/test_stanza_xep_0055.py
new file mode 100644
index 00000000..9ff45efa
--- /dev/null
+++ b/tests/test_stanza_xep_0055.py
@@ -0,0 +1,59 @@
+import unittest
+
+from slixmpp import register_stanza_plugin, Iq
+from slixmpp.test import SlixTest
+
+from slixmpp.plugins.xep_0055 import stanza
+
+
+class TestJabberSearch(SlixTest):
+ def setUp(self):
+ register_stanza_plugin(Iq, stanza.Search)
+ self.stream_start(plugins={"xep_0055"})
+
+ def testRequestSearchFields(self):
+ iq = self.Iq()
+ iq.set_from("juliet@capulet.com/balcony")
+ iq.set_to("characters.shakespeare.lit")
+ iq.set_type("get")
+ iq.enable("search")
+ iq["id"] = "0"
+ self.check(
+ iq,
+ """
+ <iq type='get'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'>
+ <query xmlns='jabber:iq:search'/>
+ </iq>
+ """,
+ )
+
+ def testSendSearch(self):
+ iq = self.xmpp["xep_0055"].make_search_iq(
+ ifrom="juliet@capulet.com/balcony", ito="characters.shakespeare.lit"
+ )
+ iq["search"]["form"].add_field(var="x-gender", value="male")
+ self.check(
+ iq,
+ """
+ <iq type='set'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <field var='x-gender'>
+ <value>male</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """,
+ use_values=False,
+ )
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberSearch)
diff --git a/tests/test_stanza_xep_0356.py b/tests/test_stanza_xep_0356.py
index ef116db2..cf14ccba 100644
--- a/tests/test_stanza_xep_0356.py
+++ b/tests/test_stanza_xep_0356.py
@@ -13,7 +13,7 @@ class TestPermissions(SlixTest):
def testAdvertisePermission(self):
xmlstring = """
<message from='capulet.net' to='pubub.capulet.lit'>
- <privilege xmlns='urn:xmpp:privilege:1'>
+ <privilege xmlns='urn:xmpp:privilege:2'>
<perm access='roster' type='both'/>
<perm access='message' type='outgoing'/>
<perm access='presence' type='managed_entity'/>
diff --git a/tests/test_stanza_xep_0444.py b/tests/test_stanza_xep_0444.py
index 0fe3f6fc..48d569e3 100644
--- a/tests/test_stanza_xep_0444.py
+++ b/tests/test_stanza_xep_0444.py
@@ -23,7 +23,7 @@ class TestReactions(SlixTest):
def setUp(self):
register_stanza_plugin(Message, stanza.Reactions)
- register_stanza_plugin(stanza.Reactions, stanza.Reaction)
+ register_stanza_plugin(stanza.Reactions, stanza.Reaction, iterable=True)
def testCreateReactions(self):
"""Testing creating Reactions."""
diff --git a/tests/test_stanza_xep_0461.py b/tests/test_stanza_xep_0461.py
new file mode 100644
index 00000000..b9550481
--- /dev/null
+++ b/tests/test_stanza_xep_0461.py
@@ -0,0 +1,48 @@
+import unittest
+from slixmpp import Message
+from slixmpp.test import SlixTest
+from slixmpp.plugins.xep_0461 import stanza
+
+
+class TestReply(SlixTest):
+ def setUp(self):
+ stanza.register_plugins()
+
+ def testReply(self):
+ message = Message()
+ message["reply"]["id"] = "some-id"
+ message["body"] = "some-body"
+
+ self.check(
+ message,
+ """
+ <message>
+ <reply xmlns="urn:xmpp:reply:0" id="some-id" />
+ <body>some-body</body>
+ </message>
+ """,
+ )
+
+ def testFallback(self):
+ message = Message()
+ message["body"] = "12345\nrealbody"
+ message["feature_fallback"]["for"] = "NS"
+ message["feature_fallback"]["fallback_body"]["start"] = "0"
+ message["feature_fallback"]["fallback_body"]["end"] = "6"
+
+ self.check(
+ message,
+ """
+ <message xmlns="jabber:client">
+ <body>12345\nrealbody</body>
+ <fallback xmlns='urn:xmpp:feature-fallback:0' for='NS'>
+ <body start="0" end="6" />
+ </fallback>
+ </message>
+ """,
+ )
+
+ assert message["feature_fallback"].get_stripped_body() == "realbody"
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestReply)
diff --git a/tests/test_stream_xep_0055.py b/tests/test_stream_xep_0055.py
new file mode 100644
index 00000000..fa028d8b
--- /dev/null
+++ b/tests/test_stream_xep_0055.py
@@ -0,0 +1,170 @@
+import unittest
+from slixmpp.test import SlixTest
+
+
+class TestJabberSearch(SlixTest):
+ def setUp(self):
+ self.stream_start(
+ mode="component",
+ plugin_config={
+ "xep_0055": {
+ "form_fields": {"first", "last"},
+ "form_instructions": "INSTRUCTIONS",
+ "form_title": "User Directory Search",
+ }
+ },
+ jid="characters.shakespeare.lit",
+ plugins={"xep_0055"}
+ )
+ self.xmpp["xep_0055"].api.register(get_results, "search_query")
+ self.xmpp["xep_0055"].api.register(get_results, "search_query")
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testRequestingSearchFields(self):
+ self.recv(
+ """
+ <iq type='get'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'
+ id='search3'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'/>
+ </iq>
+ """
+ )
+ self.send(
+ """
+ <iq type='result'
+ from='characters.shakespeare.lit'
+ to='juliet@capulet.com/balcony'
+ id='search3'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='form'>
+ <title>User Directory Search</title>
+ <instructions>INSTRUCTIONS</instructions>
+ <field type='hidden'
+ var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <field var='first'/>
+ <field var='last'/>
+ </x>
+ </query>
+ </iq>
+ """,
+ use_values=False,
+ )
+
+ def testSearchResult(self):
+ self.recv(
+ """
+ <iq type='get'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'
+ id='search2'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <field var='last'>
+ <value>Montague</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """
+ )
+ self.send(
+ """
+ <iq type='result'
+ from='characters.shakespeare.lit'
+ to='juliet@capulet.com/balcony'
+ id='search2'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='result'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <reported>
+ <field var='first' label='Given Name' />
+ <field var='last' label='Family Name' />
+ </reported>
+ <item>
+ <field var='first'><value>Benvolio</value></field>
+ <field var='last'><value>Montague</value></field>
+ </item>
+ </x>
+ </query>
+ </iq>
+ """,
+ use_values=False, # TypeError: element indices must be integers without that
+ )
+
+ def testSearchNoResult(self):
+ self.xmpp["xep_0055"].api.register(get_results, "search_query")
+ self.recv(
+ """
+ <iq type='get'
+ from='juliet@capulet.com/balcony'
+ to='characters.shakespeare.lit'
+ id='search2'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='submit'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <field var='last'>
+ <value>Capulet</value>
+ </field>
+ </x>
+ </query>
+ </iq>
+ """
+ )
+ self.send(
+ """
+ <iq type='result'
+ from='characters.shakespeare.lit'
+ to='juliet@capulet.com/balcony'
+ id='search2'
+ xml:lang='en'>
+ <query xmlns='jabber:iq:search'>
+ <x xmlns='jabber:x:data' type='result'>
+ <field type='hidden' var='FORM_TYPE'>
+ <value>jabber:iq:search</value>
+ </field>
+ <reported>
+ <field var='first' label='Given Name' />
+ <field var='last' label='Family Name' />
+ </reported>
+ </x>
+ </query>
+ </iq>
+ """,
+ use_values=False, # TypeError: element indices must be integers without that
+ )
+
+async def get_results(jid, node, ifrom, iq):
+ reply = iq.reply()
+ form = reply["search"]["form"]
+ form["type"] = "result"
+
+ form.add_reported("first", label="Given Name")
+ form.add_reported("last", label="Family Name")
+
+ d = iq["search"]["form"].get_values()
+
+ if d["last"] == "Montague":
+ form.add_item({"first": "Benvolio", "last": "Montague"})
+
+ return reply
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberSearch)
diff --git a/tests/test_stream_xep_0356.py b/tests/test_stream_xep_0356.py
index 2949daad..e2ce9569 100644
--- a/tests/test_stream_xep_0356.py
+++ b/tests/test_stream_xep_0356.py
@@ -31,7 +31,7 @@ class TestPermissions(SlixTest):
self.recv(
"""
<message from='capulet.net' to='pubub.capulet.lit' id='54321'>
- <privilege xmlns='urn:xmpp:privilege:1'>
+ <privilege xmlns='urn:xmpp:privilege:2'>
<perm access='roster' type='both'/>
<perm access='message' type='outgoing'/>
</privilege>
@@ -95,7 +95,7 @@ class TestPermissions(SlixTest):
def testMakeOutgoingMessage(self):
xmlstring = """
<message xmlns="jabber:component:accept" from='pubsub.capulet.lit' to='capulet.net'>
- <privilege xmlns='urn:xmpp:privilege:1'>
+ <privilege xmlns='urn:xmpp:privilege:2'>
<forwarded xmlns='urn:xmpp:forward:0'>
<message from="juliet@capulet.lit" to="romeo@montague.lit" xmlns="jabber:client">
<body>I do not hate you</body>
diff --git a/tests/test_stream_xep_0461.py b/tests/test_stream_xep_0461.py
new file mode 100644
index 00000000..b73a9964
--- /dev/null
+++ b/tests/test_stream_xep_0461.py
@@ -0,0 +1,48 @@
+import logging
+import unittest
+from slixmpp.test import SlixTest
+
+
+class TestReply(SlixTest):
+ def setUp(self):
+ self.stream_start(plugins=["xep_0461"])
+
+ def tearDown(self):
+ self.stream_close()
+
+ def testFallBackBody(self):
+ async def on_reply(msg):
+ start = msg["feature_fallback"]["fallback_body"]["start"]
+ end = msg["feature_fallback"]["fallback_body"]["end"]
+ self.xmpp["xep_0461"].send_reply(
+ reply_to=msg.get_from(),
+ reply_id=msg.get_id(),
+ mto="test@test.com",
+ mbody=f"{start} to {end}",
+ )
+
+ self.xmpp.add_event_handler("message_reply", on_reply)
+
+ self.recv(
+ """
+ <message id="other-id" from="from@from.com/res">
+ <reply xmlns="urn:xmpp:reply:0" id="some-id" />
+ <body>&gt; quoted\nsome-body</body>
+ <fallback xmlns='urn:xmpp:feature-fallback:0' for='urn:xmpp:reply:0'>
+ <body start="0" end="8" />
+ </fallback>
+ </message>
+ """
+ )
+ self.send(
+ """
+ <message xmlns="jabber:client" to="test@test.com" type="normal">
+ <reply xmlns="urn:xmpp:reply:0" id="other-id" to="from@from.com/res" />
+ <body>0 to 8</body>
+ </message>
+ """
+ )
+
+
+logging.basicConfig(level=logging.DEBUG)
+suite = unittest.TestLoader().loadTestsFromTestCase(TestReply)
diff --git a/tests/test_xep_0454.py b/tests/test_xep_0454.py
new file mode 100644
index 00000000..8d43c7e3
--- /dev/null
+++ b/tests/test_xep_0454.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8 et ts=4 sts=4 sw=4
+#
+# Copyright © 2022 Maxime “pep” Buquet <pep@bouah.net>
+#
+# Distributed under terms of the GPLv3+ license.
+
+"""
+ Tests for XEP-0454 (OMEMO Media Sharing) plugin.
+"""
+
+import unittest
+from io import BytesIO
+from slixmpp.test import SlixTest
+from slixmpp.plugins.xep_0454 import XEP_0454
+
+
+class TestMediaSharing(SlixTest):
+
+ def testEncryptDecryptSmall(self):
+ plain = b'qwertyuiop'
+ ciphertext, fragment = XEP_0454.encrypt(input_file=BytesIO(plain))
+ result = XEP_0454.decrypt(BytesIO(ciphertext), fragment)
+
+ self.assertEqual(plain, result)
+
+ def testEncryptDecrypt(self):
+ plain = b'a' * 4096 + b'qwertyuiop'
+ ciphertext, fragment = XEP_0454.encrypt(input_file=BytesIO(plain))
+ result = XEP_0454.decrypt(BytesIO(ciphertext), fragment)
+
+ self.assertEqual(plain, result)
+
+ def testFormatURL(self):
+ url = 'https://foo.bar'
+ fragment = 'a' * 88
+ result = XEP_0454.format_url(url, fragment)
+ self.assertEqual('aesgcm://foo.bar#' + 'a' * 88, result)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestMediaSharing)