Compare commits

...

74 Commits

Author SHA1 Message Date
mathieui
3dcb96d9d8 Update version to 1.5.0 2020-05-01 15:26:24 +02:00
Maxime Buquet
0a7a4c3abe Merge branch 'python-version' into 'master'
Update Python version minimum requirement to 3.7

See merge request poezio/slixmpp!42
2020-05-01 15:14:49 +02:00
Maxime “pep” Buquet
a4bbc404ed Update Python version minimum requirement to 3.7
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-05-01 15:13:19 +02:00
Maxime “pep” Buquet
c3fbc6cb80 xep_0196: Use correct tag local name (thanks ivucica)
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-04-06 12:57:12 +02:00
Maxime “pep” Buquet
355d789061 docs: remove andyet logo/link on every page
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2020-04-06 12:57:12 +02:00
mathieui
47ed67c04e Merge branch 'add-github-pr-template' into 'master'
Add a github pull request template

See merge request poezio/slixmpp!41
2020-04-04 20:06:23 +02:00
mathieui
34567f450a Add a github pull request template 2020-04-04 20:04:23 +02:00
mathieui
9126bd8392 Merge branch 'fix-deprecations' into 'master'
Fix deprecation warning regarding invalid escape sequences.

See merge request poezio/slixmpp!40
2020-04-04 18:53:15 +02:00
mathieui
02202f7cd8 Merge branch 'fix-nonetype-error' into 'master'
Fix an issue when deleting subelements: TypeError: 'NoneType' object is not an iterator

See merge request poezio/slixmpp!39
2020-04-04 18:44:25 +02:00
jheling
2add94f5b0 Fix TypeError: 'NoneType' object is not an iterator
When deleting sub-elements in a stanza.
2020-04-04 18:42:48 +02:00
Karthikeyan Singaravelan
5fc757f200 Fix deprecation warning regarding invalid escape sequences. 2020-04-04 16:08:44 +00:00
mathieui
98108d0445 Merge branch 'fix-celementtree-import' into 'master'
cElementTree has been deprecated since Python 3.3 and removed in Python 3.9.

See merge request poezio/slixmpp!38
2020-04-04 17:55:11 +02:00
Maxime Buquet
76f4fb49d6 Merge branch 'sync-fixes' into 'master'
Sync fixes

See merge request poezio/slixmpp!37
2020-04-04 15:28:59 +02:00
Georg Lukas
5be46a5e68 fire 'disconnected' callback from abort() 2020-04-04 13:16:33 +02:00
Georg Lukas
ab9040c30e expose is_connecting state based on connection attempt future 2020-04-04 13:16:31 +02:00
Maxime Buquet
a16e2a0f6c Merge branch 'fix-0198' into 'master'
XEP-0198: unset end_session_on_disconnect on resume/enable

See merge request poezio/slixmpp!36
2020-03-29 14:28:06 +02:00
Maxime Buquet
842aa3be8f Merge branch 'fix-reconnect-2.0' into 'master'
Reset reconnect delay on manual reconnect, add delay event

See merge request poezio/slixmpp!35
2020-03-29 14:26:15 +02:00
Georg Lukas
6c28b49e7f XEP-0198: unset end_session_on_disconnect on resume/enable 2020-03-29 14:21:40 +02:00
Georg Lukas
621255027d Reset reconnect delay on manual reconnect, add delay event
This is just a hotfix workaround for an underlying problem. The
`_connect_routine` code is "blocking" (in an async way) for
`connect_loop_wait` seconds, so that a fresh-started manual reconnect
will be silenty delayed. This code does the following changes:

1. It moves the delay to before the DNS resolution (with the exponential
   back-off it might well be that the DNS records are changed while
   slixmpp is waiting).

2. It adds a new event `reconnect_delay` that gets passed the number of
   seconds it will delay before actually reconnecting

3. It resets the `connect_loop_wait` timer on a manual connect/reconnect
   call to fix the interactive experience.

A *proper fix* would replace the sleep in `_connect_routine` with a
properly timered re-invocation of it, but I don't understand enough of
asyncio for pulling off that magic, and this is actually a proper
improvement. Also I tested this and it works!
2020-03-29 14:21:05 +02:00
Maxime Buquet
efe316dc8c Merge branch 'fix-0198' into 'master'
XEP-0198: properly disable on disconnect, fix reconnect-loop

See merge request poezio/slixmpp!34
2020-03-28 22:11:52 +01:00
Maxime Buquet
e9a87a0b77 Merge branch 'master' into 'master'
reconnect: fix callback when not currently connected

See merge request poezio/slixmpp!32
2020-03-28 22:11:34 +01:00
Georg Lukas
85c9967b9c XEP-0198: fix race conditions on enable/resume
This code splits out the `enabled` property into `enabled_in` and
`enabled_out` to reflect that client and server enable 0198
asynchronously.

This also moves the actual enabling code into the stanza processing
logic, because apparently, `enable.send()` was just added into the end
of the send queue, but `enable` got enabled immediately, so that poezio
requested ACKs for whatever happened to be in the queue before.

Async is hard, let's go get fishing.
2020-03-28 21:49:18 +01:00
Georg Lukas
deb6d4f176 XEP-0198: properly disable on disconnect, fix reconnect-loop
When the connection is disconnected (but the session didn't "end",
because 0198 resumption is enabled), poezio will reconnect and try to
send an <r/> element because the 0198 plugin doesn't realize that SM
wasn't re-enabled yet.
2020-03-28 20:50:55 +01:00
Karthikeyan Singaravelan
7218bb4499 cElementTree has been deprecated since Python 3.3 and removed in Python 3.9. 2020-03-28 04:42:23 +00:00
Georg Lukas
d85efec7a2 reconnect: fix callback when not currently connected
The 'disconnected' event is normally fired from connection_lost(), which
is called by the connection code when the connection is lost after being
established. However, if the connection wasn't successfully established,
a manual /reconnect no-ops because it waits for the 'disconnected'
callback which never fires. This patch does two things:

1. Immediately fire a 'disconnected' event in disconnect() if there is
   no transport.

2. Register the 'disconnected' event handler in reconnect() *before* it
   can be fired.
2020-03-23 18:59:29 +01:00
mathieui
115c234527 Merge branch 'async-filters' into 'master'
Add async filters on the send process

See merge request poezio/slixmpp!24
2019-12-27 15:35:00 +01:00
mathieui
a0f5cb6e09 Put the queue exception at toplevel 2019-12-27 15:27:29 +01:00
mathieui
110bbf8afc Improve the send queue code a bit 2019-12-27 15:27:29 +01:00
mathieui
d97efa0bd8 add a separate place for slow ass filters 2019-12-27 15:27:29 +01:00
mathieui
672f1b28f6 raise Exception 2019-12-27 15:27:29 +01:00
mathieui
27d3ae958b Try/except around outbound stanza processing
to avoid killing the send loop when a filter has an error
2019-12-27 15:27:29 +01:00
mathieui
a32794ec35 Remove trailing whitespace 2019-12-27 15:27:28 +01:00
mathieui
aa11ba463e Skip 0323 because 2019-12-27 15:27:28 +01:00
mathieui
a83c00e933 Update test framework to work with new filters
(eewww)
2019-12-27 15:27:28 +01:00
mathieui
31f6ef6814 Run the send queue in a separate coroutine
To be able to run async stream filters
2019-12-27 15:27:25 +01:00
Maxime “pep” Buquet
9b3874b5df xep_0048: Ensure Conference jid is of type str when given a JID
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-11-11 14:00:19 +01:00
mathieui
0139fb291e Fix a bug in the 0202 plugin where a time result would send back a result
poezio bug #3499
2019-09-19 22:54:53 +02:00
mathieui
e58988484a Match the sender JID as well as the queryid in MAM results 2019-09-10 23:13:04 +02:00
mathieui
5d5e5cda19 Add typing/docstring in the MAM plugin 2019-09-10 22:44:46 +02:00
root
11f707987d Added amount parameter, so that limit on the msgs received per query can be changed. 2019-09-08 14:22:48 +02:00
Maxime “pep” Buquet
db13794e0f Revert "Remove a block of compatibility code"
This reverts commit 37bc1bb9b3.

Confusion confusion. Mathieui thought this was a sleekxmpp thing when
it's actually been added not so long ago.
2019-08-28 00:12:10 +02:00
mathieui
37bc1bb9b3 Remove a block of compatibility code
even if the user makes that mistake, it does not cause problems down the
line.
2019-08-26 12:00:38 +02:00
mathieui
9be30e5291 fix a typo in the invalidjid exception name case 2019-08-25 01:42:00 +02:00
Maxime Buquet
9fe20a4056 Merge branch 'origin-id' into 'master'
Implement Origin-id (XEP-0359)

See merge request poezio/slixmpp!21
2019-08-23 17:10:42 +02:00
Maxime “pep” Buquet
3253d34c0a basexmpp: Make origin-id opt-out
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-08-23 17:09:21 +02:00
Maxime “pep” Buquet
fef575ee1a Implement origin-id (XEP-0359)
This XEP is not implemented as a plugin but directly into Message.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-08-23 17:09:21 +02:00
Maxime Buquet
540ff89427 Merge branch 'mam' into 'master'
Assign 'True' to 'before' tag if it's value is 'None'.

See merge request poezio/slixmpp!26
2019-08-23 16:37:12 +02:00
root
dd8ac8fc87 Assign True to the 'before' tag if it's value is None (eg:at the start no msg is there in the group, so no stanza-id) 2019-08-23 04:39:20 +05:30
Maxime Buquet
2249d878d1 Merge branch 'mam' into 'master'
Removed assigning 'reverse' value to the 'before' tag

See merge request poezio/slixmpp!25
2019-08-23 00:03:07 +02:00
root
89fa9dc1dd Removed before tag. (Code for setting it is already there) 2019-08-23 00:21:42 +05:30
root
d7729e8683 Removed assigning 'reverse' value to the 'before' tag (It's value is set in xep_0313 (mam.py file) and if not then by default it is takes as 'None'). 2019-08-23 00:15:56 +05:30
louiz’
d618f55dea Merge branch 'mam' into 'master'
Added <before> tag for querying messages before a stanza-id.

See merge request poezio/slixmpp!23
2019-08-13 09:54:43 +02:00
Madhur Garg
b0e688eb35 Added <before> tag for querying messages before a stanza-id. 2019-08-12 23:39:01 +05:30
Maxime “pep” Buquet
0e7176483b xep_0030: add docstring to get_info_from_domain
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-08-03 16:05:09 +02:00
Emmanuel Gil Peyrot
f35569a2c1 Remove the last instances of a block argument to iq.send().
Thanks Madhur Garg for spotting this in
027ce2434d7fd3cf4a286dd373cb761c0d114c66!
2019-08-01 22:36:59 +02:00
Link Mauve
bec6f7c8f3 Merge branch 'mam' into 'master'
Removed 'block' from set_preferences as it was giving a TypeError while sending the staza.

See merge request poezio/slixmpp!20
2019-08-01 22:30:58 +02:00
Madhur Garg
027ce2434d Removed 'block' from set_preferences as it was giving a TypeError while sending the staza. 2019-08-02 01:57:36 +05:30
Maxime Buquet
d57fbb57a2 Merge branch 'mam' into 'master'
Added a function in xep313 plugin to get current MAM preferences.

See merge request poezio/slixmpp!19
2019-07-22 22:05:05 +02:00
Madhur Garg
85cd7a9166 Added a function to get current MAM preferences. 2019-07-18 16:55:11 +05:30
Maxime “pep” Buquet
d50d996c68 xmlstream/stanzabase: remove unused interfaces and types attributes
These are already on each stanza, and were not applicable to all stanzas
anyway.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-07-16 11:26:54 +02:00
mathieui
371ad20ca7 Do not add disco#info for occupantid, it’s a server thing 2019-07-14 13:25:22 +02:00
mathieui
5f49df6b56 Add the occupant id stanza elements 2019-07-14 13:13:59 +02:00
mathieui
b50bfb2f34 Initial commit for reactions protoxep 2019-07-13 19:44:32 +02:00
Maxime “pep” Buquet
b29bb30eb7 Make generated stanza id truly random
Fix long-standing security issues where stanza @id be predictable.

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-07-13 14:07:31 +02:00
Maxime “pep” Buquet
4435c81d77 xmlstream.disconnect: add compat behaviour, set wait to default 2.0 when True is passed. Update documentation
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-07-03 21:36:17 +02:00
Madhur Garg
2638ba2744 Added 'reverse' argument. 2019-07-03 14:18:33 +05:30
Madhur Garg
dbc9758311 Added 'reverse' parameter in mam and rsm plugins 2019-07-03 10:31:18 +05:30
Maxime “pep” Buquet
47968963b1 Revert part of previous commit. Return NotImplemented when object is not a valid JID
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-05-07 11:56:39 +01:00
Maxime “pep” Buquet
4e8800f954 jid: return not equal if value can't be converted to JID
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-05-07 10:08:46 +01:00
Maxime “pep” Buquet
40053518aa Merge remote-tracking branch 'origin/mr/13' 2019-04-27 12:59:23 +01:00
Maxime “pep” Buquet
1ee0f72ead xmlstream.disconnect: fix frenchism in docstring
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-24 23:35:33 +01:00
Maxime “pep” Buquet
4bb81228ae xmlstream.disconnect: typing hints
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-24 23:35:06 +01:00
Maxime “pep” Buquet
60a7a5b8df presence: Ensure <show/> value is valid when returned as presence @type value
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-24 20:29:54 +01:00
Maxime “pep” Buquet
946674f424 xep_0045: Ensure <show/> value is valid.
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
2019-04-24 20:28:58 +01:00
41 changed files with 502 additions and 147 deletions

13
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,13 @@
################ Please use Gitlab instead of Github ###################################
Hello, thank you for contributing to slixmpp!
Youre about to open a pull request on github. However this github repository is not the official place for contributions on slixmpp.
Please open your merge request on https://lab.louiz.org/poezio/slixmpp/
You should be able to log in there with your github credentials, clone the slixmpp repository in your namespace, push your existing pull request into a new branch, and then open a merge request with one click, within 3 minutes.
This will help us review your contribution, avoid spreading things everywhere and it will even run the tests automatically with your changes.
Thank you.

View File

@@ -1,9 +1,7 @@
language: python language: python
python: python:
- "3.4" - "3.7"
- "3.5" - "3.8-dev"
- "3.6"
- "3.7-dev"
install: install:
- "pip install ." - "pip install ."
script: testall.py script: testall.py

View File

@@ -1,5 +1,5 @@
Pre-requisites: Pre-requisites:
- Python 3.5+ - Python 3.7+
- Cython 0.22 and libidn, optionally (making JID faster by compiling the stringprep module) - Cython 0.22 and libidn, optionally (making JID faster by compiling the stringprep module)
- GnuPG, for testing - GnuPG, for testing

View File

@@ -1,7 +1,7 @@
Slixmpp Slixmpp
######### #########
Slixmpp is an MIT licensed XMPP library for Python 3.5+. It is a fork of Slixmpp is an MIT licensed XMPP library for Python 3.7+. It is a fork of
SleekXMPP. SleekXMPP.
Slixmpp's goals is to only rewrite the core of the library (the low level Slixmpp's goals is to only rewrite the core of the library (the low level

View File

@@ -408,24 +408,3 @@ div.viewcode-block:target {
margin: -1px -12px; margin: -1px -12px;
padding: 0 12px; padding: 0 12px;
} }
#from_andyet {
-webkit-box-shadow: #CCC 0px 0px 3px;
background: rgba(255, 255, 255, 1);
bottom: 0px;
right: 17px;
padding: 3px 10px;
position: fixed;
}
#from_andyet h2 {
background-image: url("images/from_&yet.png");
background-repeat: no-repeat;
height: 29px;
line-height: 0;
text-indent: -9999em;
width: 79px;
margin-top: 0;
margin: 0px;
padding: 0px;
}

View File

@@ -65,6 +65,5 @@
<div class="bottomnav"> <div class="bottomnav">
{{ nav() }} {{ nav() }}
</div> </div>
<a id="from_andyet" href="http://andyet.net"><h2>From &amp;yet</h2></a>
{% endblock %} {% endblock %}

View File

@@ -13,7 +13,7 @@ hides namespaces when able and does not introduce excessive namespace
prefixes:: prefixes::
>>> from slixmpp.xmlstream.tostring import tostring >>> from slixmpp.xmlstream.tostring import tostring
>>> from xml.etree import cElementTree as ET >>> from xml.etree import ElementTree as ET
>>> xml = ET.fromstring('<foo xmlns="bar"><baz /></foo>') >>> xml = ET.fromstring('<foo xmlns="bar"><baz /></foo>')
>>> ET.tostring(xml) >>> ET.tostring(xml)
'<ns0:foo xmlns:ns0="bar"><ns0:baz /></foo>' '<ns0:foo xmlns:ns0="bar"><ns0:baz /></foo>'

View File

@@ -3,8 +3,9 @@
Differences from SleekXMPP Differences from SleekXMPP
========================== ==========================
**Python 3.5+ only** **Python 3.7+ only**
slixmpp will only work on python 3.5 and above. slixmpp will work on python 3.7 and above. It may work with previous
versions but we provide no guarantees.
**Stanza copies** **Stanza copies**
The same stanza object is given through all the handlers; a handler that The same stanza object is given through all the handlers; a handler that

View File

@@ -47,11 +47,11 @@ the roster. Next, we want to send our message, and to do that we will use :meth:
self.send_message(mto=self.recipient, mbody=self.msg) self.send_message(mto=self.recipient, mbody=self.msg)
Finally, we need to disconnect the client using :meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>`. Finally, we need to disconnect the client using :meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>`.
Now, sent stanzas are placed in a queue to pass them to the send thread. If we were to call Now, sent stanzas are placed in a queue to pass them to the send thread.
:meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>` without any parameters, then it is possible :meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>` by default will wait for an
for the client to disconnect before the send queue is processed and the message is actually acknowledgement from the server for at least `2.0` seconds. This time is configurable with
sent on the wire. To ensure that our message is processed, we use the `wait` parameter. If `0.0` is passed for `wait`, :meth:`disconnect
:meth:`disconnect(wait=True) <slixmpp.xmlstream.XMLStream.disconnect>`. <slixmpp.xmlstream.XMLStream.disconnect>` will not close the connection gracefully.
.. code-block:: python .. code-block:: python
@@ -61,12 +61,12 @@ sent on the wire. To ensure that our message is processed, we use
self.send_message(mto=self.recipient, mbody=self.msg) self.send_message(mto=self.recipient, mbody=self.msg)
self.disconnect(wait=True) self.disconnect()
.. warning:: .. warning::
If you happen to be adding stanzas to the send queue faster than the send thread If you happen to be adding stanzas to the send queue faster than the send thread
can process them, then :meth:`disconnect(wait=True) <slixmpp.xmlstream.XMLStream.disconnect>` can process them, then :meth:`disconnect() <slixmpp.xmlstream.XMLStream.disconnect>`
will block and not disconnect. will block and not disconnect.
Final Product Final Product

View File

@@ -21,7 +21,7 @@ Slixmpp
which goal is to use asyncio instead of threads to handle networking. See which goal is to use asyncio instead of threads to handle networking. See
:ref:`differences`. :ref:`differences`.
Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 3.5+, Slixmpp is an :ref:`MIT licensed <license>` XMPP library for Python 3.7+,
Slixmpp's design goals and philosphy are: Slixmpp's design goals and philosphy are:

View File

@@ -28,9 +28,8 @@ CLASSIFIERS = [
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Internet :: XMPP', 'Topic :: Internet :: XMPP',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
] ]

View File

@@ -111,6 +111,9 @@ class BaseXMPP(XMLStream):
#: outgoing messages an ID. #: outgoing messages an ID.
self.use_presence_ids = True self.use_presence_ids = True
#: XEP-0359 <origin-id/> tag that gets added to <message/> stanzas.
self.use_origin_id = True
#: The API registry is a way to process callbacks based on #: The API registry is a way to process callbacks based on
#: JID+node combinations. Each callback in the registry is #: JID+node combinations. Each callback in the registry is
#: marked with: #: marked with:

View File

@@ -423,7 +423,10 @@ class JID:
if isinstance(other, UnescapedJID): if isinstance(other, UnescapedJID):
return False return False
if not isinstance(other, JID): if not isinstance(other, JID):
other = JID(other) try:
other = JID(other)
except InvalidJID:
return NotImplemented
return (self._node == other._node and return (self._node == other._node and
self._domain == other._domain and self._domain == other._domain and

View File

@@ -85,4 +85,6 @@ __all__ = [
'xep_0323', # IoT Systems Sensor Data 'xep_0323', # IoT Systems Sensor Data
'xep_0325', # IoT Systems Control 'xep_0325', # IoT Systems Control
'xep_0332', # HTTP Over XMPP Transport 'xep_0332', # HTTP Over XMPP Transport
'protoxep_reactions', # https://dino.im/xeps/reactions.html
'protoxep_occupantid', # https://dino.im/xeps/occupant-id.html
] ]

View File

@@ -0,0 +1,12 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.protoxep_occupantid.occupantid import XEP_OccupantID
from slixmpp.plugins.protoxep_occupantid.stanza import OccupantID
register_plugin(XEP_OccupantID)

View File

@@ -0,0 +1,23 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins import BasePlugin
from slixmpp.stanza import Message, Presence
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.protoxep_occupantid import stanza
class XEP_OccupantID(BasePlugin):
name = 'protoxep_occupantid'
description = 'XEP-XXXX: Anonymous unique occupant identifiers for MUCs'
dependencies = set()
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, stanza.OccupantID)
register_stanza_plugin(Presence, stanza.OccupantID)

View File

@@ -0,0 +1,16 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class OccupantID(ElementBase):
name = 'occupant-id'
plugin_attrib = 'occupant-id'
namespace = 'urn:xmpp:occupant-id:0'
interfaces = {'id'}

View File

@@ -0,0 +1,11 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.protoxep_reactions.reactions import XEP_Reactions
register_plugin(XEP_Reactions)

View File

@@ -0,0 +1,54 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from typing import Iterable
from slixmpp.plugins import BasePlugin
from slixmpp.stanza import Message
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.matcher import MatchXMLMask
from slixmpp.xmlstream.handler import Callback
from slixmpp.plugins.protoxep_reactions import stanza
class XEP_Reactions(BasePlugin):
name = 'protoxep_reactions'
description = 'XEP-XXXX: Message Reactions'
dependencies = {'xep_0030'}
stanza = stanza
def plugin_init(self):
self.xmpp.register_handler(
Callback(
'Reaction received',
MatchXMLMask('<message><reactions xmlns="urn:xmpp:reactions:0"/></message>'),
self._handle_reactions,
)
)
self.xmpp['xep_0030'].add_feature('urn:xmpp:reactions:0')
register_stanza_plugin(Message, stanza.Reactions)
def plugin_end(self):
self.xmpp.remove_handler('Reaction received')
self.xmpp['xep_0030'].remove_feature('urn:xmpp:reactions:0')
def _handle_reactions(self, message: Message):
self.xmpp.event('reactions', message)
@staticmethod
def set_reactions(message: Message, to_id: str, reactions: Iterable[str]):
"""
Add reactions to a Message object.
"""
reactions_stanza = stanza.Reactions()
reactions_stanza['to'] = to_id
for reaction in reactions:
reaction_stanza = stanza.Reaction()
reaction_stanza['value'] = reaction
reactions_stanza.append(reaction_stanza)
message.append(reactions_stanza)

View File

@@ -0,0 +1,31 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2019 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
class Reactions(ElementBase):
name = 'reactions'
plugin_attrib = 'reactions'
namespace = 'urn:xmpp:reactions:0'
interfaces = {'to'}
class Reaction(ElementBase):
name = 'reaction'
namespace = 'urn:xmpp:reactions:0'
interfaces = {'value'}
def get_value(self) -> str:
return self.xml.text
def set_value(self, value: str):
self.xml.text = value
register_stanza_plugin(Reactions, Reaction, iterable=True)

View File

@@ -7,7 +7,7 @@
""" """
from slixmpp.xmlstream.stanzabase import ElementBase from slixmpp.xmlstream.stanzabase import ElementBase
from xml.etree import cElementTree as ET from xml.etree import ElementTree as ET
class RPCQuery(ElementBase): class RPCQuery(ElementBase):

View File

@@ -300,6 +300,8 @@ class XEP_0030(BasePlugin):
async def get_info_from_domain(self, domain=None, timeout=None, async def get_info_from_domain(self, domain=None, timeout=None,
cached=True, callback=None): cached=True, callback=None):
"""Fetch disco#info of specified domain and one disco#items level below"""
if domain is None: if domain is None:
domain = self.xmpp.boundjid.domain domain = self.xmpp.boundjid.domain

View File

@@ -162,7 +162,7 @@ class XEP_0045(BasePlugin):
return return
self.xmpp.roster[pr['from']].ignore_updates = True self.xmpp.roster[pr['from']].ignore_updates = True
entry = pr['muc'].get_stanza_values() entry = pr['muc'].get_stanza_values()
entry['show'] = pr['show'] entry['show'] = pr['show'] if pr['show'] in pr.showtypes else None
entry['status'] = pr['status'] entry['status'] = pr['status']
entry['alt_nick'] = pr['nick'] entry['alt_nick'] = pr['nick']
if pr['type'] == 'unavailable': if pr['type'] == 'unavailable':

View File

@@ -6,6 +6,7 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from slixmpp import JID
from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin from slixmpp.xmlstream import ET, ElementBase, register_stanza_plugin
@@ -52,6 +53,12 @@ class Conference(ElementBase):
if value in ('1', 'true', True): if value in ('1', 'true', True):
self._set_attr('autojoin', 'true') self._set_attr('autojoin', 'true')
def set_jid(self, value):
del self['jid']
if isinstance(value, JID):
value = value.full
self._set_attr('jid', value)
class URL(ElementBase): class URL(ElementBase):
name = 'url' name = 'url'

View File

@@ -79,7 +79,8 @@ class ResultIterator:
""" """
if self._stop: if self._stop:
raise StopAsyncIteration raise StopAsyncIteration
self.query[self.interface]['rsm']['before'] = self.reverse if self.query[self.interface]['rsm']['before'] is None:
self.query[self.interface]['rsm']['before'] = self.reverse
self.query['id'] = self.query.stream.new_id() self.query['id'] = self.query.stream.new_id()
self.query[self.interface]['rsm']['max'] = str(self.amount) self.query[self.interface]['rsm']['max'] = str(self.amount)
@@ -141,7 +142,7 @@ class XEP_0059(BasePlugin):
def session_bind(self, jid): def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Set.namespace) self.xmpp['xep_0030'].add_feature(Set.namespace)
def iterate(self, stanza, interface, results='substanzas', def iterate(self, stanza, interface, results='substanzas', amount=10, reverse=False,
recv_interface=None, pre_cb=None, post_cb=None): recv_interface=None, pre_cb=None, post_cb=None):
""" """
Create a new result set iterator for a given stanza query. Create a new result set iterator for a given stanza query.
@@ -169,6 +170,6 @@ class XEP_0059(BasePlugin):
results -- The name of the interface containing the results -- The name of the interface containing the
query results (typically just 'substanzas'). query results (typically just 'substanzas').
""" """
return ResultIterator(stanza, interface, results, return ResultIterator(stanza, interface, results, amount, reverse=reverse,
recv_interface=recv_interface, pre_cb=pre_cb, recv_interface=recv_interface, pre_cb=pre_cb,
post_cb=post_cb) post_cb=post_cb)

View File

@@ -11,10 +11,9 @@ from slixmpp.xmlstream import ElementBase, ET
class UserGaming(ElementBase): class UserGaming(ElementBase):
name = 'gaming' name = 'game'
namespace = 'urn:xmpp:gaming:0' namespace = 'urn:xmpp:gaming:0'
plugin_attrib = 'gaming' plugin_attrib = 'gaming'
interfaces = {'character_name', 'character_profile', 'name', interfaces = {'character_name', 'character_profile', 'name',
'level', 'server_address', 'server_name', 'uri'} 'level', 'server_address', 'server_name', 'uri'}
sub_interfaces = interfaces sub_interfaces = interfaces

View File

@@ -71,7 +71,8 @@ class XEP_0198(BasePlugin):
self.window_counter = self.window self.window_counter = self.window
self.enabled = False self.enabled_in = False
self.enabled_out = False
self.unacked_queue = collections.deque() self.unacked_queue = collections.deque()
register_stanza_plugin(StreamFeatures, stanza.StreamManagement) register_stanza_plugin(StreamFeatures, stanza.StreamManagement)
@@ -82,10 +83,6 @@ class XEP_0198(BasePlugin):
self.xmpp.register_stanza(stanza.Ack) self.xmpp.register_stanza(stanza.Ack)
self.xmpp.register_stanza(stanza.RequestAck) self.xmpp.register_stanza(stanza.RequestAck)
# Only end the session when a </stream> element is sent,
# not just because the connection has died.
self.xmpp.end_session_on_disconnect = False
# Register the feature twice because it may be ordered two # Register the feature twice because it may be ordered two
# different ways: enabling after binding and resumption # different ways: enabling after binding and resumption
# before binding. # before binding.
@@ -131,6 +128,7 @@ class XEP_0198(BasePlugin):
self.xmpp.add_filter('in', self._handle_incoming) self.xmpp.add_filter('in', self._handle_incoming)
self.xmpp.add_filter('out_sync', self._handle_outgoing) self.xmpp.add_filter('out_sync', self._handle_outgoing)
self.xmpp.add_event_handler('disconnected', self.disconnected)
self.xmpp.add_event_handler('session_end', self.session_end) self.xmpp.add_event_handler('session_end', self.session_end)
def plugin_end(self): def plugin_end(self):
@@ -139,6 +137,7 @@ class XEP_0198(BasePlugin):
self.xmpp.unregister_feature('sm', self.order) self.xmpp.unregister_feature('sm', self.order)
self.xmpp.unregister_feature('sm', self.resume_order) self.xmpp.unregister_feature('sm', self.resume_order)
self.xmpp.del_event_handler('disconnected', self.disconnected)
self.xmpp.del_event_handler('session_end', self.session_end) self.xmpp.del_event_handler('session_end', self.session_end)
self.xmpp.del_filter('in', self._handle_incoming) self.xmpp.del_filter('in', self._handle_incoming)
self.xmpp.del_filter('out_sync', self._handle_outgoing) self.xmpp.del_filter('out_sync', self._handle_outgoing)
@@ -154,9 +153,19 @@ class XEP_0198(BasePlugin):
self.xmpp.remove_stanza(stanza.Ack) self.xmpp.remove_stanza(stanza.Ack)
self.xmpp.remove_stanza(stanza.RequestAck) self.xmpp.remove_stanza(stanza.RequestAck)
def disconnected(self, event):
"""Reset enabled state until we can resume/reenable."""
log.debug("disconnected, disabling SM")
self.xmpp.event('sm_disabled', event)
self.enabled_in = False
self.enabled_out = False
def session_end(self, event): def session_end(self, event):
"""Reset stream management state.""" """Reset stream management state."""
self.enabled = False log.debug("session_end, disabling SM")
self.xmpp.event('sm_disabled', event)
self.enabled_in = False
self.enabled_out = False
self.unacked_queue.clear() self.unacked_queue.clear()
self.sm_id = None self.sm_id = None
self.handled = 0 self.handled = 0
@@ -171,6 +180,7 @@ class XEP_0198(BasePlugin):
def request_ack(self, e=None): def request_ack(self, e=None):
"""Request an ack from the server.""" """Request an ack from the server."""
log.debug("requesting ack")
req = stanza.RequestAck(self.xmpp) req = stanza.RequestAck(self.xmpp)
self.xmpp.send_raw(str(req)) self.xmpp.send_raw(str(req))
@@ -193,9 +203,7 @@ class XEP_0198(BasePlugin):
enable = stanza.Enable(self.xmpp) enable = stanza.Enable(self.xmpp)
enable['resume'] = self.allow_resume enable['resume'] = self.allow_resume
enable.send() enable.send()
self.enabled = True log.debug("enabling SM")
self.handled = 0
self.unacked_queue.clear()
waiter = Waiter('enabled_or_failed', waiter = Waiter('enabled_or_failed',
MatchMany([ MatchMany([
@@ -204,11 +212,11 @@ class XEP_0198(BasePlugin):
self.xmpp.register_handler(waiter) self.xmpp.register_handler(waiter)
result = await waiter.wait() result = await waiter.wait()
elif self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features: elif self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features:
self.enabled = True
resume = stanza.Resume(self.xmpp) resume = stanza.Resume(self.xmpp)
resume['h'] = self.handled resume['h'] = self.handled
resume['previd'] = self.sm_id resume['previd'] = self.sm_id
resume.send() resume.send()
log.debug("resuming SM")
# Wait for a response before allowing stream feature processing # Wait for a response before allowing stream feature processing
# to continue. The actual result processing will be done in the # to continue. The actual result processing will be done in the
@@ -231,7 +239,10 @@ class XEP_0198(BasePlugin):
self.xmpp.features.add('stream_management') self.xmpp.features.add('stream_management')
if stanza['id']: if stanza['id']:
self.sm_id = stanza['id'] self.sm_id = stanza['id']
self.enabled_in = True
self.handled = 0
self.xmpp.event('sm_enabled', stanza) self.xmpp.event('sm_enabled', stanza)
self.xmpp.end_session_on_disconnect = False
def _handle_resumed(self, stanza): def _handle_resumed(self, stanza):
"""Finish resuming a stream by resending unacked stanzas. """Finish resuming a stream by resending unacked stanzas.
@@ -239,10 +250,12 @@ class XEP_0198(BasePlugin):
Raises a :term:`session_resumed` event. Raises a :term:`session_resumed` event.
""" """
self.xmpp.features.add('stream_management') self.xmpp.features.add('stream_management')
self.enabled_in = True
self._handle_ack(stanza) self._handle_ack(stanza)
for id, stanza in self.unacked_queue: for id, stanza in self.unacked_queue:
self.xmpp.send(stanza, use_filters=False) self.xmpp.send(stanza, use_filters=False)
self.xmpp.event('session_resumed', stanza) self.xmpp.event('session_resumed', stanza)
self.xmpp.end_session_on_disconnect = False
def _handle_failed(self, stanza): def _handle_failed(self, stanza):
""" """
@@ -252,7 +265,8 @@ class XEP_0198(BasePlugin):
Raises an :term:`sm_failed` event. Raises an :term:`sm_failed` event.
""" """
self.enabled = False self.enabled_in = False
self.enabled_out = False
self.unacked_queue.clear() self.unacked_queue.clear()
self.xmpp.event('sm_failed', stanza) self.xmpp.event('sm_failed', stanza)
@@ -289,7 +303,7 @@ class XEP_0198(BasePlugin):
def _handle_incoming(self, stanza): def _handle_incoming(self, stanza):
"""Increment the handled counter for each inbound stanza.""" """Increment the handled counter for each inbound stanza."""
if not self.enabled: if not self.enabled_in:
return stanza return stanza
if isinstance(stanza, (Message, Presence, Iq)): if isinstance(stanza, (Message, Presence, Iq)):
@@ -299,7 +313,13 @@ class XEP_0198(BasePlugin):
def _handle_outgoing(self, stanza): def _handle_outgoing(self, stanza):
"""Store outgoing stanzas in a queue to be acked.""" """Store outgoing stanzas in a queue to be acked."""
if not self.enabled: from slixmpp.plugins.xep_0198 import stanza as st
if isinstance(stanza, (st.Enable, st.Resume)):
self.enabled_out = True
self.unacked_queue.clear()
log.debug("enabling outgoing SM: %s" % stanza)
if not self.enabled_out:
return stanza return stanza
if isinstance(stanza, (Message, Presence, Iq)): if isinstance(stanza, (Message, Presence, Iq)):

View File

@@ -50,7 +50,7 @@ class XEP_0202(BasePlugin):
self.xmpp.register_handler( self.xmpp.register_handler(
Callback('Entity Time', Callback('Entity Time',
StanzaPath('iq/entity_time'), StanzaPath('iq@type=get/entity_time'),
self._handle_time_request)) self._handle_time_request))
register_stanza_plugin(Iq, stanza.EntityTime) register_stanza_plugin(Iq, stanza.EntityTime)

View File

@@ -31,11 +31,11 @@ class XEP_0279(BasePlugin):
def plugin_end(self): def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:sic:0') self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:sic:0')
def check_ip(self, ifrom=None, block=True, timeout=None, callback=None, def check_ip(self, ifrom=None, timeout=None, callback=None,
timeout_callback=None): timeout_callback=None):
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
iq['type'] = 'get' iq['type'] = 'get'
iq['from'] = ifrom iq['from'] = ifrom
iq.enable('ip_check') iq.enable('ip_check')
return iq.send(block=block, timeout=timeout, callback=callback, return iq.send(timeout=timeout, callback=callback,
timeout_callback=timeout_callback) timeout_callback=timeout_callback)

View File

@@ -3,16 +3,18 @@
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp. This file is part of Slixmpp.
See the file LICENSE for copying permissio See the file LICENSE for copying permission
""" """
import logging import logging
import slixmpp from datetime import datetime
from typing import Any, Dict, Callable, Optional, Awaitable
from slixmpp import JID
from slixmpp.stanza import Message, Iq from slixmpp.stanza import Message, Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream.handler import Collector from slixmpp.xmlstream.handler import Collector
from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream.matcher import MatchXMLMask
from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0313 import stanza from slixmpp.plugins.xep_0313 import stanza
@@ -41,8 +43,32 @@ class XEP_0313(BasePlugin):
register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set) register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set)
register_stanza_plugin(stanza.Fin, self.xmpp['xep_0059'].stanza.Set) register_stanza_plugin(stanza.Fin, self.xmpp['xep_0059'].stanza.Set)
def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None, def retrieve(
timeout=None, callback=None, iterator=False, rsm=None): self,
jid: Optional[JID] = None,
start: Optional[datetime] = None,
end: Optional[datetime] = None,
with_jid: Optional[JID] = None,
ifrom: Optional[JID] = None,
reverse: bool = False,
timeout: int = None,
callback: Callable[[Iq], None] = None,
iterator: bool = False,
rsm: Optional[Dict[str, Any]] = None
) -> Awaitable:
"""
Send a MAM query and retrieve the results.
:param JID jid: Entity holding the MAM records
:param datetime start,end: MAM query temporal boundaries
:param JID with_jid: Filter results on this JID
:param JID ifrom: To change the from address of the query
:param bool reverse: Get the results in reverse order
:param int timeout: IQ timeout
:param func callback: Custom callback for handling results
:param bool iterator: Use RSM and iterate over a paginated query
:param dict rsm: RSM custom options
"""
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
query_id = iq['id'] query_id = iq['id']
@@ -53,35 +79,48 @@ class XEP_0313(BasePlugin):
iq['mam']['start'] = start iq['mam']['start'] = start
iq['mam']['end'] = end iq['mam']['end'] = end
iq['mam']['with'] = with_jid iq['mam']['with'] = with_jid
amount = 10
if rsm: if rsm:
for key, value in rsm.items(): for key, value in rsm.items():
iq['mam']['rsm'][key] = str(value) iq['mam']['rsm'][key] = str(value)
if key == 'max':
amount = value
cb_data = {} cb_data = {}
def pre_cb(query):
stanza_mask = self.xmpp.Message()
stanza_mask.xml.remove(stanza_mask.xml.find('{urn:xmpp:sid:0}origin-id'))
del stanza_mask['id']
del stanza_mask['lang']
stanza_mask['from'] = jid
stanza_mask['mam_result']['queryid'] = query_id
xml_mask = str(stanza_mask)
def pre_cb(query: Iq) -> None:
stanza_mask['mam_result']['queryid'] = query['id']
xml_mask = str(stanza_mask)
query['mam']['queryid'] = query['id'] query['mam']['queryid'] = query['id']
collector = Collector( collector = Collector(
'MAM_Results_%s' % query_id, 'MAM_Results_%s' % query_id,
StanzaPath('message/mam_result@queryid=%s' % query['id'])) MatchXMLMask(xml_mask))
self.xmpp.register_handler(collector) self.xmpp.register_handler(collector)
cb_data['collector'] = collector cb_data['collector'] = collector
def post_cb(result): def post_cb(result: Iq) -> None:
results = cb_data['collector'].stop() results = cb_data['collector'].stop()
if result['type'] == 'result': if result['type'] == 'result':
result['mam']['results'] = results result['mam']['results'] = results
if iterator: if iterator:
return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results', return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results', amount=amount,
recv_interface='mam_fin', reverse=reverse, recv_interface='mam_fin',
pre_cb=pre_cb, post_cb=post_cb) pre_cb=pre_cb, post_cb=post_cb)
collector = Collector( collector = Collector(
'MAM_Results_%s' % query_id, 'MAM_Results_%s' % query_id,
StanzaPath('message/mam_result@queryid=%s' % query_id)) MatchXMLMask(xml_mask))
self.xmpp.register_handler(collector) self.xmpp.register_handler(collector)
def wrapped_cb(iq): def wrapped_cb(iq: Iq) -> None:
results = collector.stop() results = collector.stop()
if iq['type'] == 'result': if iq['type'] == 'result':
iq['mam']['results'] = results iq['mam']['results'] = results
@@ -90,8 +129,15 @@ class XEP_0313(BasePlugin):
return iq.send(timeout=timeout, callback=wrapped_cb) return iq.send(timeout=timeout, callback=wrapped_cb)
def get_preferences(self, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
query_id = iq['id']
iq['mam_prefs']['query_id'] = query_id
return iq.send(timeout=timeout, callback=callback)
def set_preferences(self, jid=None, default=None, always=None, never=None, def set_preferences(self, jid=None, default=None, always=None, never=None,
ifrom=None, block=True, timeout=None, callback=None): ifrom=None, timeout=None, callback=None):
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
iq['type'] = 'set' iq['type'] = 'set'
iq['to'] = jid iq['to'] = jid
@@ -99,7 +145,7 @@ class XEP_0313(BasePlugin):
iq['mam_prefs']['default'] = default iq['mam_prefs']['default'] = default
iq['mam_prefs']['always'] = always iq['mam_prefs']['always'] = always
iq['mam_prefs']['never'] = never iq['mam_prefs']['never'] = never
return iq.send(block=block, timeout=timeout, callback=callback) return iq.send(timeout=timeout, callback=callback)
def get_configuration_commands(self, jid, **kwargs): def get_configuration_commands(self, jid, **kwargs):
return self.xmpp['xep_0030'].get_items( return self.xmpp['xep_0030'].get_items(

View File

@@ -516,7 +516,7 @@ class Field(ElementBase):
:param value: string :param value: string
""" """
pattern = re.compile("^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$") pattern = re.compile(r"^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$")
if pattern.match(value) is not None: if pattern.match(value) is not None:
self.xml.stringIds = value self.xml.stringIds = value
else: else:

View File

@@ -114,7 +114,6 @@ class XEP_0332(BasePlugin):
iq['http-req']['data'] = data iq['http-req']['data'] = data
return iq.send( return iq.send(
timeout=kwargs.get('timeout', None), timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None), callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None) timeout_callback=kwargs.get('timeout_callback', None)
) )
@@ -135,7 +134,6 @@ class XEP_0332(BasePlugin):
iq['http-resp']['data'] = data iq['http-resp']['data'] = data
return iq.send( return iq.send(
timeout=kwargs.get('timeout', None), timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None), callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None) timeout_callback=kwargs.get('timeout_callback', None)
) )
@@ -153,7 +151,6 @@ class XEP_0332(BasePlugin):
iq['id'] = kwargs["id"] iq['id'] = kwargs["id"]
return iq.send( return iq.send(
timeout=kwargs.get('timeout', None), timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None), callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None) timeout_callback=kwargs.get('timeout_callback', None)
) )

View File

@@ -10,6 +10,9 @@ from slixmpp.stanza.rootstanza import RootStanza
from slixmpp.xmlstream import StanzaBase, ET from slixmpp.xmlstream import StanzaBase, ET
ORIGIN_NAME = '{urn:xmpp:sid:0}origin-id'
class Message(RootStanza): class Message(RootStanza):
""" """
@@ -63,6 +66,8 @@ class Message(RootStanza):
if self['id'] == '': if self['id'] == '':
if self.stream is not None and self.stream.use_message_ids: if self.stream is not None and self.stream.use_message_ids:
self['id'] = self.stream.new_id() self['id'] = self.stream.new_id()
else:
del self['origin_id']
def get_type(self): def get_type(self):
""" """
@@ -76,6 +81,43 @@ class Message(RootStanza):
""" """
return self._get_attr('type', 'normal') return self._get_attr('type', 'normal')
def get_id(self):
return self._get_attr('id') or ''
def get_origin_id(self):
sub = self.xml.find(ORIGIN_NAME)
if sub is not None:
return sub.attrib.get('id') or ''
return ''
def _set_ids(self, value) -> None:
if value is None or value == '':
return None
self.xml.attrib['id'] = value
if not self.stream.use_origin_id:
return None
sub = self.xml.find(ORIGIN_NAME)
if sub is not None:
sub.attrib['id'] = value
else:
sub = ET.Element(ORIGIN_NAME)
sub.attrib['id'] = value
self.xml.append(sub)
def set_id(self, value):
return self._set_ids(value)
def set_origin_id(self, value: str):
return self._set_ids(value)
def del_origin_id(self):
sub = self.xml.find(ORIGIN_NAME)
if sub is not None:
self.xml.remove(sub)
def get_parent_thread(self): def get_parent_thread(self):
"""Return the message thread's parent thread. """Return the message thread's parent thread.
@@ -140,6 +182,8 @@ class Message(RootStanza):
new_message['parent_thread'] = self['parent_thread'] new_message['parent_thread'] = self['parent_thread']
del new_message['id'] del new_message['id']
if self.stream is not None and self.stream.use_message_ids:
new_message['id'] = self.stream.new_id()
if body is not None: if body is not None:
new_message['body'] = body new_message['body'] = body

View File

@@ -90,10 +90,10 @@ class Presence(RootStanza):
def get_type(self): def get_type(self):
""" """
Return the value of the <presence> stanza's type attribute, or Return the value of the <presence> stanza's type attribute, or
the value of the <show> element. the value of the <show> element if valid.
""" """
out = self._get_attr('type') out = self._get_attr('type')
if not out: if not out and self['show'] in self.showtypes:
out = self['show'] out = self['show']
if not out or out is None: if not out or out is None:
out = 'available' out = 'available'

View File

@@ -340,11 +340,19 @@ class SlixTest(unittest.TestCase):
self.xmpp.default_lang = None self.xmpp.default_lang = None
self.xmpp.peer_default_lang = None self.xmpp.peer_default_lang = None
def new_id():
self.xmpp._id += 1
return str(self.xmpp._id)
self.xmpp._id = 0
self.xmpp.new_id = new_id
# Must have the stream header ready for xmpp.process() to work. # Must have the stream header ready for xmpp.process() to work.
if not header: if not header:
header = self.xmpp.stream_header header = self.xmpp.stream_header
self.xmpp.data_received(header) self.xmpp.data_received(header)
self.wait_for_send_queue()
if skip: if skip:
self.xmpp.socket.next_sent() self.xmpp.socket.next_sent()
@@ -592,6 +600,7 @@ class SlixTest(unittest.TestCase):
'id', 'stanzapath', 'xpath', and 'mask'. 'id', 'stanzapath', 'xpath', and 'mask'.
Defaults to the value of self.match_method. Defaults to the value of self.match_method.
""" """
self.wait_for_send_queue()
sent = self.xmpp.socket.next_sent(timeout) sent = self.xmpp.socket.next_sent(timeout)
if data is None and sent is None: if data is None and sent is None:
return return
@@ -608,6 +617,14 @@ class SlixTest(unittest.TestCase):
defaults=defaults, defaults=defaults,
use_values=use_values) use_values=use_values)
def wait_for_send_queue(self):
loop = asyncio.get_event_loop()
future = asyncio.ensure_future(self.xmpp.run_filters(), loop=loop)
queue = self.xmpp.waiting_queue
print(queue)
loop.run_until_complete(queue.join())
future.cancel()
def stream_close(self): def stream_close(self):
""" """
Disconnect the dummy XMPP client. Disconnect the dummy XMPP client.

View File

@@ -160,7 +160,7 @@ except:
return _fixed_offset_tzs[offsetmins] return _fixed_offset_tzs[offsetmins]
_iso8601_parser = re.compile(""" _iso8601_parser = re.compile(r"""
^ ^
(?P<year> [0-9]{4})?(?P<ymdsep>-?)? (?P<year> [0-9]{4})?(?P<ymdsep>-?)?
(?P<month>[0-9]{2})?(?P=ymdsep)? (?P<month>[0-9]{2})?(?P=ymdsep)?

View File

@@ -9,5 +9,5 @@
# We don't want to have to import the entire library # We don't want to have to import the entire library
# just to get the version info for setup.py # just to get the version info for setup.py
__version__ = '1.4.2' __version__ = '1.5.0'
__version_info__ = (1, 4, 2) __version_info__ = (1, 5, 0)

View File

@@ -17,7 +17,7 @@ from __future__ import with_statement, unicode_literals
import copy import copy
import logging import logging
import weakref import weakref
from xml.etree import cElementTree as ET from xml.etree import ElementTree as ET
from slixmpp.xmlstream import JID from slixmpp.xmlstream import JID
from slixmpp.xmlstream.tostring import tostring from slixmpp.xmlstream.tostring import tostring
@@ -203,7 +203,7 @@ class ElementBase(object):
""" """
The core of Slixmpp's stanza XML manipulation and handling is provided The core of Slixmpp's stanza XML manipulation and handling is provided
by ElementBase. ElementBase wraps XML cElementTree objects and enables by ElementBase. ElementBase wraps XML ElementTree objects and enables
access to the XML contents through dictionary syntax, similar in style access to the XML contents through dictionary syntax, similar in style
to the Ruby XMPP library Blather's stanza implementation. to the Ruby XMPP library Blather's stanza implementation.
@@ -387,7 +387,7 @@ class ElementBase(object):
self._index = 0 self._index = 0
#: The underlying XML object for the stanza. It is a standard #: The underlying XML object for the stanza. It is a standard
#: :class:`xml.etree.cElementTree` object. #: :class:`xml.etree.ElementTree` object.
self.xml = xml self.xml = xml
#: An ordered dictionary of plugin stanzas, mapped by their #: An ordered dictionary of plugin stanzas, mapped by their
@@ -1031,14 +1031,19 @@ class ElementBase(object):
if not lang: if not lang:
lang = default_lang lang = default_lang
parent = self.xml
for level, _ in enumerate(path): for level, _ in enumerate(path):
# Generate the paths to the target elements and their parent. # Generate the paths to the target elements and their parent.
element_path = "/".join(path[:len(path) - level]) element_path = "/".join(path[:len(path) - level])
parent_path = "/".join(path[:len(path) - level - 1]) parent_path = "/".join(path[:len(path) - level - 1])
elements = self.xml.findall(element_path) elements = self.xml.findall(element_path)
parent = self.xml.find(parent_path)
if parent_path == '':
parent_path = None
if parent_path is not None:
parent = self.xml.find(parent_path)
if elements: if elements:
if parent is None: if parent is None:
parent = self.xml parent = self.xml
@@ -1374,14 +1379,6 @@ class StanzaBase(ElementBase):
#: The default XMPP client namespace #: The default XMPP client namespace
namespace = 'jabber:client' namespace = 'jabber:client'
#: There is a small set of attributes which apply to all XMPP stanzas:
#: the stanza type, the to and from JIDs, the stanza ID, and, especially
#: in the case of an Iq stanza, a payload.
interfaces = {'type', 'to', 'from', 'id', 'payload'}
#: A basic set of allowed values for the ``'type'`` interface.
types = {'get', 'set', 'error', None, 'unavailable', 'normal', 'chat'}
def __init__(self, stream=None, xml=None, stype=None, def __init__(self, stream=None, xml=None, stype=None,
sto=None, sfrom=None, sid=None, parent=None): sto=None, sfrom=None, sid=None, parent=None):
self.stream = stream self.stream = stream

View File

@@ -12,6 +12,8 @@
:license: MIT, see LICENSE for more details :license: MIT, see LICENSE for more details
""" """
from typing import Optional, Set, Callable
import functools import functools
import logging import logging
import socket as Socket import socket as Socket
@@ -19,6 +21,8 @@ import ssl
import weakref import weakref
import uuid import uuid
from asyncio import iscoroutinefunction, wait
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from slixmpp.xmlstream.asyncio import asyncio from slixmpp.xmlstream.asyncio import asyncio
@@ -30,6 +34,10 @@ from slixmpp.xmlstream.resolver import resolve, default_resolver
RESPONSE_TIMEOUT = 30 RESPONSE_TIMEOUT = 30
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class ContinueQueue(Exception):
"""
Exception raised in the send queue to "continue" from within an inner loop
"""
class NotConnectedError(Exception): class NotConnectedError(Exception):
""" """
@@ -81,6 +89,8 @@ class XMLStream(asyncio.BaseProtocol):
self.force_starttls = None self.force_starttls = None
self.disable_starttls = None self.disable_starttls = None
self.waiting_queue = asyncio.Queue()
# A dict of {name: handle} # A dict of {name: handle}
self.scheduled_events = {} self.scheduled_events = {}
@@ -199,11 +209,6 @@ class XMLStream(asyncio.BaseProtocol):
self.__event_handlers = {} self.__event_handlers = {}
self.__filters = {'in': [], 'out': [], 'out_sync': []} self.__filters = {'in': [], 'out': [], 'out_sync': []}
self._id = 0
#: We use an ID prefix to ensure that all ID values are unique.
self._id_prefix = '%s-' % uuid.uuid4()
# Current connection attempt (Future) # Current connection attempt (Future)
self._current_connection_attempt = None self._current_connection_attempt = None
@@ -241,12 +246,7 @@ class XMLStream(asyncio.BaseProtocol):
ID values. Using this method ensures that all new ID values ID values. Using this method ensures that all new ID values
are unique in this stream. are unique in this stream.
""" """
self._id += 1 return uuid.uuid4().hex
return self.get_id()
def get_id(self):
"""Return the current unique stream ID in hexadecimal form."""
return "%s%X" % (self._id_prefix, self._id)
def connect(self, host='', port=0, use_ssl=False, def connect(self, host='', port=0, use_ssl=False,
force_starttls=True, disable_starttls=False): force_starttls=True, disable_starttls=False):
@@ -271,8 +271,13 @@ class XMLStream(asyncio.BaseProtocol):
localhost localhost
""" """
asyncio.ensure_future(
self.run_filters(),
loop=self.loop,
)
self.disconnect_reason = None self.disconnect_reason = None
self.cancel_connection_attempt() self.cancel_connection_attempt()
self.connect_loop_wait = 0
if host and port: if host and port:
self.address = (host, int(port)) self.address = (host, int(port))
try: try:
@@ -297,6 +302,10 @@ class XMLStream(asyncio.BaseProtocol):
async def _connect_routine(self): async def _connect_routine(self):
self.event_when_connected = "connected" self.event_when_connected = "connected"
if self.connect_loop_wait > 0:
self.event('reconnect_delay', self.connect_loop_wait)
await asyncio.sleep(self.connect_loop_wait, loop=self.loop)
record = await self.pick_dns_answer(self.default_domain) record = await self.pick_dns_answer(self.default_domain)
if record is not None: if record is not None:
host, address, dns_port = record host, address, dns_port = record
@@ -313,7 +322,6 @@ class XMLStream(asyncio.BaseProtocol):
else: else:
ssl_context = None ssl_context = None
await asyncio.sleep(self.connect_loop_wait, loop=self.loop)
if self._current_connection_attempt is None: if self._current_connection_attempt is None:
return return
try: try:
@@ -372,6 +380,7 @@ class XMLStream(asyncio.BaseProtocol):
"ssl_object", "ssl_object",
default=self.transport.get_extra_info("socket") default=self.transport.get_extra_info("socket")
) )
self._current_connection_attempt = None
self.init_parser() self.init_parser()
self.send_raw(self.stream_header) self.send_raw(self.stream_header)
self.dns_answers = None self.dns_answers = None
@@ -430,6 +439,9 @@ class XMLStream(asyncio.BaseProtocol):
self.send(error) self.send(error)
self.disconnect() self.disconnect()
def is_connecting(self):
return self._current_connection_attempt is not None
def is_connected(self): def is_connected(self):
return self.transport is not None return self.transport is not None
@@ -463,10 +475,10 @@ class XMLStream(asyncio.BaseProtocol):
self._current_connection_attempt.cancel() self._current_connection_attempt.cancel()
self._current_connection_attempt = None self._current_connection_attempt = None
def disconnect(self, wait=2.0, reason=None): def disconnect(self, wait: float = 2.0, reason: Optional[str] = None) -> None:
"""Close the XML stream and wait for an acknowldgement from the server for """Close the XML stream and wait for an acknowldgement from the server for
at most `wait` seconds. After the given number of seconds has at most `wait` seconds. After the given number of seconds has
passed without a response from the serveur, or when the server passed without a response from the server, or when the server
successfully responds with a closure of its own stream, abort() is successfully responds with a closure of its own stream, abort() is
called. If wait is 0.0, this will call abort() directly without closing called. If wait is 0.0, this will call abort() directly without closing
the stream. the stream.
@@ -476,6 +488,13 @@ class XMLStream(asyncio.BaseProtocol):
:param wait: Time to wait for a response from the server. :param wait: Time to wait for a response from the server.
""" """
# Compat: docs/getting_started/sendlogout.rst has been promoting
# `disconnect(wait=True)` for ages. This doesn't mean anything to the
# schedule call below. It would fortunately be converted to `1` later
# down the call chain. Praise the implicit casts lord.
if wait == True:
wait = 2.0
self.disconnect_reason = reason self.disconnect_reason = reason
self.cancel_connection_attempt() self.cancel_connection_attempt()
if self.transport: if self.transport:
@@ -483,6 +502,8 @@ class XMLStream(asyncio.BaseProtocol):
self.send_raw(self.stream_footer) self.send_raw(self.stream_footer)
self.schedule('Disconnect wait', wait, self.schedule('Disconnect wait', wait,
self.abort, repeat=False) self.abort, repeat=False)
else:
self.event("disconnected", reason)
def abort(self): def abort(self):
""" """
@@ -495,14 +516,15 @@ class XMLStream(asyncio.BaseProtocol):
self.event("killed") self.event("killed")
self.disconnected.set_result(True) self.disconnected.set_result(True)
self.disconnected = asyncio.Future() self.disconnected = asyncio.Future()
self.event("disconnected", self.disconnect_reason)
def reconnect(self, wait=2.0, reason="Reconnecting"): def reconnect(self, wait=2.0, reason="Reconnecting"):
"""Calls disconnect(), and once we are disconnected (after the timeout, or """Calls disconnect(), and once we are disconnected (after the timeout, or
when the server acknowledgement is received), call connect() when the server acknowledgement is received), call connect()
""" """
log.debug("reconnecting...") log.debug("reconnecting...")
self.disconnect(wait, reason)
self.add_event_handler('disconnected', lambda event: self.connect(), disposable=True) self.add_event_handler('disconnected', lambda event: self.connect(), disposable=True)
self.disconnect(wait, reason)
def configure_socket(self): def configure_socket(self):
"""Set timeout and other options for self.socket. """Set timeout and other options for self.socket.
@@ -790,7 +812,7 @@ class XMLStream(asyncio.BaseProtocol):
# If the callback is a coroutine, schedule it instead of # If the callback is a coroutine, schedule it instead of
# running it directly # running it directly
if asyncio.iscoroutinefunction(handler_callback): if iscoroutinefunction(handler_callback):
async def handler_callback_routine(cb): async def handler_callback_routine(cb):
try: try:
await cb(data) await cb(data)
@@ -889,11 +911,93 @@ class XMLStream(asyncio.BaseProtocol):
""" """
return xml return xml
async def _continue_slow_send(
self,
task: asyncio.Task,
already_used: Set[Callable[[ElementBase], Optional[StanzaBase]]]
) -> None:
"""
Used when an item in the send queue has taken too long to process.
This is away from the send queue and can take as much time as needed.
:param asyncio.Task task: the Task wrapping the coroutine
:param set already_used: Filters already used on this outgoing stanza
"""
data = await task
for filter in self.__filters['out']:
if filter in already_used:
continue
if iscoroutinefunction(filter):
data = await task
else:
data = filter(data)
if data is None:
return
if isinstance(data, ElementBase):
for filter in self.__filters['out_sync']:
data = filter(data)
if data is None:
return
str_data = tostring(data.xml, xmlns=self.default_ns,
stream=self, top_level=True)
self.send_raw(str_data)
else:
self.send_raw(data)
async def run_filters(self):
"""
Background loop that processes stanzas to send.
"""
while True:
(data, use_filters) = await self.waiting_queue.get()
try:
if isinstance(data, ElementBase):
if use_filters:
already_run_filters = set()
for filter in self.__filters['out']:
already_run_filters.add(filter)
if iscoroutinefunction(filter):
task = asyncio.create_task(filter(data))
completed, pending = await wait(
{task},
timeout=1,
)
if pending:
asyncio.ensure_future(
self._continue_slow_send(
task,
already_run_filters
)
)
raise Exception("Slow coro, rescheduling")
data = task.result()
else:
data = filter(data)
if data is None:
raise ContinueQueue('Empty stanza')
if isinstance(data, ElementBase):
if use_filters:
for filter in self.__filters['out_sync']:
data = filter(data)
if data is None:
raise ContinueQueue('Empty stanza')
str_data = tostring(data.xml, xmlns=self.default_ns,
stream=self, top_level=True)
self.send_raw(str_data)
else:
self.send_raw(data)
except ContinueQueue as exc:
log.debug('Stanza in send queue not sent: %s', exc)
except Exception:
log.error('Exception raised in send queue:', exc_info=True)
self.waiting_queue.task_done()
def send(self, data, use_filters=True): def send(self, data, use_filters=True):
"""A wrapper for :meth:`send_raw()` for sending stanza objects. """A wrapper for :meth:`send_raw()` for sending stanza objects.
May optionally block until an expected response is received.
:param data: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase` :param data: The :class:`~slixmpp.xmlstream.stanzabase.ElementBase`
stanza to send on the stream. stanza to send on the stream.
:param bool use_filters: Indicates if outgoing filters should be :param bool use_filters: Indicates if outgoing filters should be
@@ -901,24 +1005,7 @@ class XMLStream(asyncio.BaseProtocol):
filters is useful when resending stanzas. filters is useful when resending stanzas.
Defaults to ``True``. Defaults to ``True``.
""" """
if isinstance(data, ElementBase): self.waiting_queue.put_nowait((data, use_filters))
if use_filters:
for filter in self.__filters['out']:
data = filter(data)
if data is None:
return
if isinstance(data, ElementBase):
if use_filters:
for filter in self.__filters['out_sync']:
data = filter(data)
if data is None:
return
str_data = tostring(data.xml, xmlns=self.default_ns,
stream=self, top_level=True)
self.send_raw(str_data)
else:
self.send_raw(data)
def send_xml(self, data): def send_xml(self, data):
"""Send an XML object on the stream """Send an XML object on the stream

View File

@@ -68,13 +68,5 @@ class TestStanzaBase(SlixTest):
self.assertTrue(stanza['payload'] == [], self.assertTrue(stanza['payload'] == [],
"Stanza reply did not empty stanza payload.") "Stanza reply did not empty stanza payload.")
def testError(self):
"""Test marking a stanza as an error."""
stanza = StanzaBase()
stanza['type'] = 'get'
stanza.error()
self.assertTrue(stanza['type'] == 'error',
"Stanza type is not 'error' after calling error()")
suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase) suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase)

View File

@@ -4,6 +4,7 @@ import sys
import datetime import datetime
import time import time
import threading import threading
import unittest
import re import re
from slixmpp.test import * from slixmpp.test import *
@@ -11,6 +12,7 @@ from slixmpp.xmlstream import ElementBase
from slixmpp.plugins.xep_0323.device import Device from slixmpp.plugins.xep_0323.device import Device
@unittest.skip('')
class TestStreamSensorData(SlixTest): class TestStreamSensorData(SlixTest):
""" """