Compare commits

..

53 Commits

Author SHA1 Message Date
mathieui
ee671dfb29 Merge branch 'next-version-1.7.0' into 'master'
Increment version to 1.7.0

See merge request poezio/slixmpp!107
2021-01-29 21:57:26 +01:00
mathieui
d954283fb6 version: update to 1.7.0 2021-01-29 21:50:43 +01:00
mathieui
ed2c03fade DOAP: add an 1.7.0 entry
also remove some duplicate description tags
2021-01-29 21:50:22 +01:00
Link Mauve
a381267d21 Merge branch 'connect-basic-itests' into 'master'
tests: add basic reconnect/connect integration tests

See merge request poezio/slixmpp!106
2021-01-29 16:51:05 +01:00
mathieui
1e1576473b tests: add basic reconnect/connect integration tests 2021-01-29 16:48:30 +01:00
Link Mauve
dbcd0c6050 Merge branch 'reconnect-logic-doomed' into 'master'
fix reconnect logic

See merge request poezio/slixmpp!104
2021-01-29 16:11:29 +01:00
mathieui
f93af07882 XEP-0198: do not send acks when disconnected 2021-01-29 16:07:44 +01:00
mathieui
3f739e513b xmlstream: keep value of "end_session_on_disconnect"
That value should be set statically. Worst case is we fail to resume the
session.
2021-01-29 15:33:44 +01:00
mathieui
fc7d7b4eb7 XEP-0198: Enable SM even if we failed resuming the session
And trigger session_end only after we fail the resuming.
2021-01-29 15:33:44 +01:00
mathieui
3642e2c7f4 xmlstream: ensure slow futures are scheduled on this loop 2021-01-29 15:33:44 +01:00
mathieui
f15311bda8 xmlstream: Make the reconnect handler a coroutine 2021-01-29 15:33:44 +01:00
mathieui
b2dfb4c1f3 xmlstream: do not touch connection state on abort()
leave it to the connection_lost handler
2021-01-29 15:33:44 +01:00
mathieui
d227579d56 xmlstream: set disconnected future on event 2021-01-29 15:33:44 +01:00
mathieui
571774edb4 xmlstream: end the parser when the stream has ended 2021-01-29 15:33:44 +01:00
mathieui
456dff0b61 xmlstream: rename run_filters 2021-01-29 15:33:44 +01:00
mathieui
a0b6bfcefe xmlstream: change the connection logic
* use asyncio wait_for to wait for a disconnected event
* abort the connection if the timeout is not enough
2021-01-29 15:33:44 +01:00
mathieui
9fbd40578c xmlstream: purge send queue and pending tasks on session end
and keep track of slow tasks
2021-01-29 15:33:44 +01:00
mathieui
8700f8d162 xmlstream: do not cancel the send filter task
it does not make sense to cancel it, it does not do anything when the
sending queue is empty, and clients should not fill the send queue when
not connected anyway.
2021-01-28 17:54:40 +01:00
mathieui
efdcd396d8 xmlstream: fix race conditions on handlers 2021-01-28 17:54:40 +01:00
mathieui
0eed84d0b2 xmlstream: handle done tasks in wait_until
and handle other loops properly
2021-01-28 17:54:40 +01:00
mathieui
370abb1d98 Merge branch 'block-threaded-examples-docs' into 'master'
Remove the remaining block and threaded from examples

See merge request poezio/slixmpp!105
2021-01-27 00:20:31 +01:00
mathieui
51866f0d46 docs: update the tutorials a bit 2021-01-27 00:16:56 +01:00
mathieui
9390794401 examples: updates to reflect asyncio 2021-01-27 00:09:26 +01:00
mathieui
70b5081018 Merge branch 'xep-0382-spoiler-messages' into 'master'
XEP-0382: Spoiler Messages

See merge request poezio/slixmpp!100
2021-01-27 00:07:36 +01:00
mathieui
4cb679ae2a Merge branch 'fix-emoji-update' into 'master'
XEP-0444: Fix emoji detection

See merge request poezio/slixmpp!103
2021-01-24 11:33:57 +01:00
mathieui
ab280b44cc XEP-0444: Fix emoji detection
the emoji lib just released a major release after 5 years, which breaks
the API. This new code is compatible with both.
2021-01-24 11:31:17 +01:00
mathieui
0193667ace Merge branch 'ping-cancel-iqs-on-session-end' into 'master'
Cancel  0199 pings on session end

See merge request poezio/slixmpp!102
2021-01-24 11:30:51 +01:00
mathieui
9cb5131f1c XEP-0199: Fix handler default parameter, add typing
Clear futures when disabling the keepalive, and do it on every
disconnect instead of only at session end.
2021-01-24 11:20:43 +01:00
mathieui
0bf1b96859 Merge branch 'handle-connection-errors-in-starttls' into 'master'
Handle connection errors in start_tls (fix #3449)

Closes #3449

See merge request poezio/slixmpp!101
2021-01-24 10:30:45 +01:00
mathieui
c6a0da63ae XEP-0199: cancel ongoing handlers on session end
and keep track of them but be careful to not store too many

fix for #3442
2021-01-22 22:57:15 +01:00
mathieui
3f10dfe138 iq: only update the future if it is not done 2021-01-22 22:55:39 +01:00
mathieui
49577e6c84 Handle connection errors in start_tls (fix #3449) 2021-01-22 22:04:41 +01:00
mathieui
04dcc8628d XEP-0382: update DOAP file 2021-01-22 19:24:11 +01:00
mathieui
81ebf4e8ba XEP-0382: Spoiler Messages 2021-01-22 18:40:37 +01:00
mathieui
b784b68bcd Merge branch 'disconnect-event-after-cleanup' into 'master'
XMLStream: Only fire "disconnected" after removal of related objects

See merge request poezio/slixmpp!99
2021-01-19 21:12:52 +01:00
mathieui
f38c61a6b9 XMLStream: Only fire "disconnected" after removal of related objects
Otherwise we could end up reconnecting and getting some useful things
like the XML parser or other stuff removed afterwards.

Also, move 'session_end' before 'disconnected', it makes more sense that
way.
2021-01-17 18:30:57 +01:00
Link Mauve
2631b25e3e Merge branch 'ad-mucjoin-component-event' into 'master'
XEP-0045: Add a groupchat_join MUC event for components

See merge request poezio/slixmpp!98
2021-01-12 20:36:36 +01:00
mathieui
2b11d81b86 XEP-0045: Add a groupchat_join MUC event for components 2021-01-12 20:29:27 +01:00
Link Mauve
ca465032e7 Merge branch 'xep-0045-fixes-misc' into 'master'
Misc fixes for XEP-0045

See merge request poezio/slixmpp!97
2021-01-10 15:12:20 +01:00
mathieui
6369ee0e5f XEP-0045: Better component handling 2021-01-10 15:07:48 +01:00
mathieui
1e23167ce4 XEP-0045: Better "groupchat_presence" targeting
(do not make EVERY SINGLE presence go through the 0045 handler)
2021-01-10 15:07:48 +01:00
mathieui
cccc1253aa XEP-0045: add more elements (<actor/>) 2021-01-10 15:07:48 +01:00
mathieui
fb31e9c1fd Merge branch 'test-skip-dependency' into 'master'
CI: Skip test if the emoji dep is not here

See merge request poezio/slixmpp!96
2021-01-10 11:27:48 +01:00
mathieui
b4dd1e0132 CI: Skip test if the emoji dep is not here 2021-01-10 11:12:00 +01:00
Link Mauve
525855c17b Fix homepage in DOAP. Thanks mathieui! 2021-01-01 14:35:55 +01:00
mathieui
ce0d615786 Merge branch 'muc-mypy-fixes' into 'master'
XEP-0045: Fix issues found by mypy

See merge request poezio/slixmpp!95
2020-12-27 19:44:31 +01:00
Emmanuel Gil Peyrot
1e08c90018 XEP-0045: Add a set_subject() helper 2020-12-27 02:59:43 +01:00
Emmanuel Gil Peyrot
c05cafc963 XEP-0045: Add missing reason for affiliation and role changes
This is especially useful for ban/kick reasons.
2020-12-27 02:59:09 +01:00
Emmanuel Gil Peyrot
166b265de0 XEP-0045: Fix issues found by mypy 2020-12-27 01:14:17 +01:00
mathieui
d91eea3a3a Merge branch 'fix-moderation-stanzaid' into 'master'
XEP-0425: Use stanzaid in integration test

See merge request poezio/slixmpp!93
2020-12-14 19:04:50 +01:00
mathieui
569b9c5ee2 XEP-0425: Use stanzaid in integration test
It only worked due to a prosody quirk
2020-12-14 18:59:51 +01:00
Maxime Buquet
a3ca4c11c3 Merge branch 'retract-in-groupchats' into 'master'
XEP-0424: fire event even with groupchat messages

See merge request poezio/slixmpp!92
2020-12-13 23:39:12 +01:00
mathieui
489e419e38 XEP-0424: fire event even with groupchat messages 2020-12-13 22:48:17 +01:00
23 changed files with 524 additions and 195 deletions

View File

@@ -8,10 +8,7 @@
<shortdesc xml:lang="en">Elegant Python library for XMPP</shortdesc>
<shortdesc xml:lang="fr">Bibliothèque pour XMPP élégante, en Python</shortdesc>
<description xml:lang="en">Add description</description>
<description xml:lang="fr">Ajouter une description</description>
<homepage rdf:resource="https:/lab.louiz.org/poezio/slixmpp/"/>
<homepage rdf:resource="https://lab.louiz.org/poezio/slixmpp/"/>
<download-page rdf:resource="https://lab.louiz.org/poezio/slixmpp/tags"/>
<bug-database rdf:resource="https://lab.louiz.org/poezio/slixmpp/issues"/>
<developer-forum rdf:resource="xmpp:slixmpp@muc.poez.io?join"/>
@@ -791,6 +788,14 @@
<xmpp:since>1.3.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0382.html"/>
<xmpp:status>complete</xmpp:status>
<xmpp:version>0.2.0</xmpp:version>
<xmpp:since>1.7.0</xmpp:since>
</xmpp:SupportedXep>
</implements>
<implements>
<xmpp:SupportedXep>
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0394.html"/>
@@ -993,5 +998,12 @@
<file-release rdf:resource="https://lab.louiz.org/poezio/slixmpp/-/archive/slix-1.6.0/slixmpp-slix-1.6.0.tar.gz"/>
</Version>
</release>
<release>
<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"/>
</Version>
</release>
</Project>
</rdf:RDF>

View File

@@ -18,7 +18,7 @@ messages sent to it. We will also go through adding some basic command line conf
for enabling or disabling debug log outputs and setting the username and password
for the bot.
For the command line options processing, we will use the built-in ``optparse``
For the command line options processing, we will use the built-in ``argparse``
module and the ``getpass`` module for reading in passwords.
TL;DR Just Give Me the Code
@@ -39,7 +39,8 @@ To get started, here is a brief outline of the structure that the final project
import asyncio
import logging
import getpass
from optparse import OptionParser
from argparse import ArgumentParser
import slixmpp
@@ -93,9 +94,9 @@ we also need to define the ``self.start`` handler.
.. code-block:: python
def start(self, event):
async def start(self, event):
self.send_presence()
self.get_roster()
await self.get_roster()
.. warning::
@@ -144,6 +145,11 @@ The XMPP stanzas from the roster retrieval process could look like this:
</query>
</iq>
Additionally, since :meth:`get_roster <slixmpp.clientxmpp.ClientXMPP.get_roster>` is using
``<iq/>`` stanzas, which will always receive an answer, it should be awaited on, to keep
a synchronous flow.
Responding to Messages
~~~~~~~~~~~~~~~~~~~~~~
Now that an ``EchoBot`` instance handles :term:`session_start`, we can begin receiving and
@@ -212,8 +218,7 @@ Command Line Arguments and Logging
While this isn't part of Slixmpp itself, we do want our echo bot program to be able
to accept a JID and password from the command line instead of hard coding them. We will
use the ``optparse`` module for this, though there are several alternative methods, including
the newer ``argparse`` module.
use the ``argparse`` module for this.
We want to accept three parameters: the JID for the echo bot, its password, and a flag for
displaying the debugging logs. We also want these to be optional parameters, since passing
@@ -222,22 +227,29 @@ a password directly through the command line can be a security risk.
.. code-block:: python
if __name__ == '__main__':
optp = OptionParser()
# Setup the command line arguments.
parser = ArgumentParser(description=EchoBot.__doc__)
optp.add_option('-d', '--debug', help='set logging to DEBUG',
action='store_const', dest='loglevel',
const=logging.DEBUG, default=logging.INFO)
optp.add_option("-j", "--jid", dest="jid",
help="JID to use")
optp.add_option("-p", "--password", dest="password",
help="password to use")
# Output verbosity options.
parser.add_argument("-q", "--quiet", help="set logging to ERROR",
action="store_const", dest="loglevel",
const=logging.ERROR, default=logging.INFO)
parser.add_argument("-d", "--debug", help="set logging to DEBUG",
action="store_const", dest="loglevel",
const=logging.DEBUG, default=logging.INFO)
opts, args = optp.parse_args()
# JID and password options.
parser.add_argument("-j", "--jid", dest="jid",
help="JID to use")
parser.add_argument("-p", "--password", dest="password",
help="password to use")
if opts.jid is None:
opts.jid = raw_input("Username: ")
if opts.password is None:
opts.password = getpass.getpass("Password: ")
args = parser.parse_args()
if args.jid is None:
args.jid = input("Username: ")
if args.password is None:
args.password = getpass("Password: ")
Since we included a flag for enabling debugging logs, we need to configure the
``logging`` module to behave accordingly.
@@ -248,7 +260,7 @@ Since we included a flag for enabling debugging logs, we need to configure the
# .. option parsing from above ..
logging.basicConfig(level=opts.loglevel,
logging.basicConfig(level=args.loglevel,
format='%(levelname)-8s %(message)s')
@@ -276,52 +288,36 @@ at this stage. For example, let's say we want our bot to support `service discov
If the ``EchoBot`` class had a hard dependency on a plugin, we could register that plugin in
the ``EchoBot.__init__`` method instead.
.. note::
If you are using the OpenFire server, you will need to include an additional
configuration step. OpenFire supports a different version of SSL than what
most servers and Slixmpp support.
.. code-block:: python
import ssl
xmpp.ssl_version = ssl.PROTOCOL_SSLv3
Now we're ready to connect and begin echoing messages. If you have the package
``aiodns`` installed, then the :meth:`slixmpp.clientxmpp.ClientXMPP` method
``aiodns`` installed, then the :meth:`slixmpp.clientxmpp.ClientXMPP.connect` method
will perform a DNS query to find the appropriate server to connect to for the
given JID. If you do not have ``aiodns``, then Slixmpp will attempt to
connect to the hostname used by the JID, unless an address tuple is supplied
to :meth:`slixmpp.clientxmpp.ClientXMPP`.
to :meth:`slixmpp.clientxmpp.ClientXMPP.connect`.
.. code-block:: python
if __name__ == '__main__':
# .. option parsing & echo bot configuration
xmpp.connect():
xmpp.process(forever=True)
if xmpp.connect():
xmpp.process(block=True)
else:
print('Unable to connect')
To begin responding to messages, you'll see we called :meth:`slixmpp.basexmpp.BaseXMPP.process`
which will start the event handling, send queue, and XML reader threads. It will also call
the :meth:`slixmpp.plugins.base.BasePlugin.post_init` method on all registered plugins. By
passing ``block=True`` to :meth:`slixmpp.basexmpp.BaseXMPP.process` we are running the
main processing loop in the main thread of execution. The :meth:`slixmpp.basexmpp.BaseXMPP.process`
call will not return until after Slixmpp disconnects. If you need to run the client in the background
for another program, use ``block=False`` to spawn the processing loop in its own thread.
The :meth:`slixmpp.basexmpp.BaseXMPP.connect` will only schedule a connection
asynchronously. To actually connect, you need to let the event loop take over.
This is done with the :meth:`slixmpp.basexmpp.BaseXMPP.process` method,
which can either run forever (``forever=True``, the default), run for a (maximum)
duration of time (``timeout=n``), and/or run until it gets disconnected (``forever=False``).
However, calling ``process()`` is not required if you already have an event loop
running, so you can handle the logic around it however you like.
.. note::
Before 1.0, controlling the blocking behaviour of :meth:`slixmpp.basexmpp.BaseXMPP.process` was
done via the ``threaded`` argument. This arrangement was a source of confusion because some users
interpreted that as controlling whether or not Slixmpp used threads at all, instead of how
the processing loop itself was spawned.
The statements ``xmpp.process(threaded=False)`` and ``xmpp.process(block=True)`` are equivalent.
Before slixmpp, :meth:slixmpp.basexmpp.BaseXMPP.process` took ``block`` and ``threaded``
arguments. These do not make sense anymore and have been removed. Slixmpp does not use
threads at all.
.. _echobot_complete:

View File

@@ -31,23 +31,23 @@ for the JID that will receive our message, and the string content of the message
self.add_event_handler('session_start', self.start)
def start(self, event):
async def start(self, event):
self.send_presence()
self.get_roster()
await self.get_roster()
Note that as in :ref:`echobot`, we need to include send an initial presence and request
the roster. Next, we want to send our message, and to do that we will use :meth:`send_message <slixmpp.basexmpp.BaseXMPP.send_message>`.
.. code-block:: python
def start(self, event):
async def start(self, event):
self.send_presence()
self.get_roster()
await self.get_roster()
self.send_message(mto=self.recipient, mbody=self.msg)
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.
Now, sent stanzas are placed in a queue to pass them to the send routine.
:meth:`disconnect <slixmpp.xmlstream.XMLStream.disconnect>` by default will wait for an
acknowledgement from the server for at least `2.0` seconds. This time is configurable with
the `wait` parameter. If `0.0` is passed for `wait`, :meth:`disconnect
@@ -55,9 +55,9 @@ the `wait` parameter. If `0.0` is passed for `wait`, :meth:`disconnect
.. code-block:: python
def start(self, event):
async def start(self, event):
self.send_presence()
self.get_roster()
await self.get_roster()
self.send_message(mto=self.recipient, mbody=self.msg)

View File

@@ -9,10 +9,8 @@ Glossary
stream handler
A callback function that accepts stanza objects pulled directly
from the XML stream. A stream handler is encapsulated in a
object that includes a :class:`Matcher <.MatcherBase>` object, and
which provides additional semantics. For example, the
:class:`.Waiter` handler wrapper blocks thread execution until a
matching stanza is received.
object that includes a :class:`Matcher <.MatcherBase>` object
which provides additional semantics.
event handler
A callback function that responds to events raised by

View File

@@ -168,13 +168,13 @@ if __name__ == '__main__':
xmpp.beClientOrServer(server=True)
while not(xmpp.testForRelease()):
xmpp.connect()
xmpp.process(block=True)
xmpp.process(forever=False)
logging.debug("lost connection")
if args.sensorjid:
logging.debug("will try to call another device for data")
xmpp.beClientOrServer(server=False,clientJID=args.sensorjid)
xmpp.connect()
xmpp.process(block=True)
xmpp.process(forever=False)
logging.debug("ready ending")
else:

View File

@@ -73,21 +73,21 @@ old_xmpp = slixmpp.ClientXMPP(args.old_jid, args.old_password)
roster = []
def on_session(event):
roster.append(old_xmpp.get_roster())
async def on_session(event):
roster.append(await old_xmpp.get_roster())
old_xmpp.disconnect()
old_xmpp.add_event_handler('session_start', on_session)
if old_xmpp.connect():
old_xmpp.process(block=True)
old_xmpp.process(forever=False)
if not roster:
print('No roster to migrate')
sys.exit()
new_xmpp = slixmpp.ClientXMPP(args.new_jid, args.new_password)
def on_session2(event):
new_xmpp.get_roster()
async def on_session2(event):
await new_xmpp.get_roster()
new_xmpp.send_presence()
logging.info(roster[0])
@@ -97,9 +97,11 @@ def on_session2(event):
for jid, item in data.items():
if item['subscription'] != 'none':
new_xmpp.send_presence(ptype='subscribe', pto=jid)
new_xmpp.update_roster(jid,
name = item['name'],
groups = item['groups'])
await new_xmpp.update_roster(
jid,
name=item['name'],
groups=item['groups']
)
new_xmpp.disconnect()
new_xmpp.add_event_handler('session_start', on_session2)

View File

@@ -56,7 +56,7 @@ class TestModerate(SlixIntegration):
iqres, new_msg = await asyncio.gather(
self.clients[0]['xep_0425'].moderate(
self.muc,
id=msg_recv['id'],
id=msg_recv['stanza_id']['id'],
reason='Your message is bad.',
),
self.clients[1].wait_until('moderated_message')

25
itests/test_reconnect.py Normal file
View File

@@ -0,0 +1,25 @@
import unittest
from slixmpp.test.integration import SlixIntegration
class TestReconnect(SlixIntegration):
async def asyncSetUp(self):
await super().asyncSetUp()
self.add_client(
self.envjid('CI_ACCOUNT1'),
self.envstr('CI_ACCOUNT1_PASSWORD'),
)
await self.connect_clients()
async def test_disconnect_connect(self):
"""Check we can disconnect and connect again"""
await self.clients[0].disconnect()
self.clients[0].connect()
await self.clients[0].wait_until('session_start')
async def test_reconnect(self):
"""Check we can reconnect()"""
self.clients[0].reconnect()
await self.clients[0].wait_until("session_start")
suite = unittest.TestLoader().loadTestsFromTestCase(TestReconnect)

View File

@@ -100,6 +100,7 @@ __all__ = [
'xep_0369', # MIX-CORE
'xep_0377', # Spam reporting
'xep_0380', # Explicit Message Encryption
'xep_0382', # Spoiler Messages
'xep_0394', # Message Markup
'xep_0403', # MIX-Presence
'xep_0404', # MIX-Anon

View File

@@ -42,6 +42,8 @@ from slixmpp.plugins.xep_0045.stanza import (
MUCOwnerQuery,
MUCOwnerDestroy,
MUCStatus,
MUCActor,
MUCUserItem,
)
@@ -66,6 +68,9 @@ class XEP_0045(BasePlugin):
self.rooms = {}
self.our_nicks = {}
# load MUC support in presence stanzas
register_stanza_plugin(MUCMessage, MUCUserItem)
register_stanza_plugin(MUCPresence, MUCUserItem)
register_stanza_plugin(MUCUserItem, MUCActor)
register_stanza_plugin(MUCMessage, MUCInvite)
register_stanza_plugin(MUCMessage, MUCDecline)
register_stanza_plugin(MUCMessage, MUCStatus)
@@ -83,9 +88,17 @@ class XEP_0045(BasePlugin):
self.xmpp.register_handler(
Callback(
'MUCPresence',
MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns),
StanzaPath("presence/muc"),
self.handle_groupchat_presence,
))
if self.xmpp.is_component:
self.xmpp.register_handler(
Callback(
'MUCPresenceJoin',
StanzaPath("presence/muc_join"),
self.handle_groupchat_join,
))
self.xmpp.register_handler(
Callback(
'MUCError',
@@ -131,22 +144,27 @@ class XEP_0045(BasePlugin):
def handle_groupchat_invite(self, inv):
""" Handle an invite into a muc. """
if inv['from'] not in self.rooms.keys():
self.xmpp.event("groupchat_invite", inv)
if self.xmpp.is_component:
self.xmpp.event('groupchat_invite', inv)
else:
if inv['from'] not in self.rooms.keys():
self.xmpp.event("groupchat_invite", inv)
def handle_groupchat_decline(self, decl):
"""Handle an invitation decline."""
if decl['from'] in self.room.keys():
self.xmpp.event('groupchat_decline', decl)
if self.xmpp.is_component:
self.xmpp.event('groupchat_invite', decl)
else:
if decl['from'] in self.room.keys():
self.xmpp.event('groupchat_decline', decl)
def handle_config_change(self, msg):
"""Handle a MUC configuration change (with status code)."""
self.xmpp.event('groupchat_config_status', msg)
self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
def handle_groupchat_presence(self, pr):
""" Handle a presence in a muc.
"""
def client_handle_presence(self, pr: Presence):
"""As a client, handle a presence stanza"""
got_offline = False
got_online = False
if pr['muc']['room'] not in self.rooms.keys():
@@ -172,6 +190,17 @@ class XEP_0045(BasePlugin):
if got_online:
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
def handle_groupchat_presence(self, pr: Presence):
""" Handle a presence in a muc."""
if self.xmpp.is_component:
self.xmpp.event('groupchat_presence', pr)
else:
self.client_handle_presence(pr)
def handle_groupchat_join(self, pr: Presence):
"""Received a join presence (as a component)"""
self.xmpp.event('groupchat_join', pr)
def handle_groupchat_message(self, msg: Message) -> None:
""" Handle a message event in a muc.
"""
@@ -207,6 +236,7 @@ class XEP_0045(BasePlugin):
entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid:
return nick
return None
def join_muc(self, room: JID, nick: str, maxhistory="0", password='',
pstatus='', pshow='', pfrom=''):
@@ -228,8 +258,15 @@ class XEP_0045(BasePlugin):
self.rooms[room] = {}
self.our_nicks[room] = nick
def set_subject(self, room: JID, subject: str, *, mfrom: Optional[JID] = None):
"""Set a rooms subject."""
msg = self.xmpp.make_message(room, mfrom=mfrom)
msg['type'] = 'groupchat'
msg['subject'] = subject
msg.send()
async def destroy(self, room: JID, reason='', altroom='', *,
ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
ifrom: Optional[JID] = None, **iqkwargs):
"""Destroy a room."""
iq = self.xmpp.make_iq_set(ifrom=ifrom, ito=room)
iq.enable('mucowner_query')
@@ -240,7 +277,8 @@ class XEP_0045(BasePlugin):
iq['mucowner_query']['destroy']['reason'] = reason
await iq.send(**iqkwargs)
async def set_affiliation(self, room: JID, jid: Optional[JID] = None, nick: Optional[str] = None, *, affiliation: str,
async def set_affiliation(self, room: JID, affiliation: str, *, jid: Optional[JID] = None,
nick: Optional[str] = None, reason: str = '',
ifrom: Optional[JID] = None, **iqkwargs):
""" Change room affiliation."""
if affiliation not in AFFILIATIONS:
@@ -248,18 +286,17 @@ class XEP_0045(BasePlugin):
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)
iq.enable('mucadmin_query')
item = MUCAdminItem()
item['affiliation'] = affiliation
iq['mucadmin_query']['item']['affiliation'] = affiliation
if nick:
item['nick'] = nick
iq['mucadmin_query']['item']['nick'] = nick
if jid:
item['jid'] = jid
iq['mucadmin_query'].append(item)
iq['mucadmin_query']['item']['jid'] = jid
if reason:
iq['mucadmin_query']['item']['reason'] = reason
await iq.send(**iqkwargs)
async def set_role(self, room: JID, nick: str, role: str, *,
ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
reason: str = '', ifrom: Optional[JID] = None, **iqkwargs):
""" Change role property of a nick in a room.
Typically, roles are temporary (they last only as long as you are in the
room), whereas affiliations are permanent (they last across groupchat
@@ -268,11 +305,10 @@ class XEP_0045(BasePlugin):
if role not in ROLES:
raise ValueError("Role %s does not exist" % role)
iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
iq.enable('mucadmin_query')
item = MUCAdminItem()
item['role'] = role
item['nick'] = nick
iq['mucadmin_query'].append(item)
iq['mucadmin_query']['item']['role'] = role
iq['mucadmin_query']['item']['nick'] = nick
if reason:
iq['mucadmin_query']['item']['reason'] = reason
await iq.send(**iqkwargs)
def invite(self, room: JID, jid: JID, reason: str = '', *,
@@ -389,11 +425,11 @@ class XEP_0045(BasePlugin):
""" Get the list of nicks in a room.
"""
if room not in self.rooms.keys():
return None
raise ValueError("Room %s is not joined" % room)
return self.rooms[room].keys()
def get_users_by_affiliation(self, room: JID, affiliation='member', *, ifrom: Optional[JID] = None):
# Preserve old API
if affiliation not in AFFILIATIONS:
raise TypeError
raise ValueError("Affiliation %s does not exist" % affiliation)
return self.get_affiliation_list(room, affiliation, ifrom=ifrom)

View File

@@ -7,7 +7,12 @@
See the file LICENSE for copying permission.
"""
from typing import Iterable, Set
from typing import (
Iterable,
Set,
Optional,
Union,
)
import logging
from slixmpp.xmlstream import ElementBase, ET, JID
@@ -45,24 +50,21 @@ class MUCBase(ElementBase):
status['code'] = code
self.append(status)
def get_item_attr(self, attr, default: str):
def get_item_attr(self, attr: str, default):
item = self.xml.find(f'{{{NS_USER}}}item')
if item is None:
return default
return item.get(attr)
return self['item'][attr]
def set_item_attr(self, attr, value: str):
item = self.xml.find(f'{{{NS_USER}}}item')
if item is None:
item = ET.Element(f'{{{NS_USER}}}item')
self.xml.append(item)
item.attrib[attr] = value
def set_item_attr(self, attr: str, value: str):
item = self['item']
item[attr] = value
return item
def del_item_attr(self, attr):
item = self.xml.find(f'{{{NS_USER}}}item')
if item is not None and attr in item.attrib:
del item.attrib[attr]
if item is not None:
del self['item'][attr]
def get_affiliation(self):
return self.get_item_attr('affiliation', '')
@@ -71,13 +73,12 @@ class MUCBase(ElementBase):
self.set_item_attr('affiliation', value)
def del_affiliation(self):
# TODO: set default affiliation
self.del_item_attr('affiliation')
def get_jid(self):
def get_jid(self) -> JID:
return JID(self.get_item_attr('jid', ''))
def set_jid(self, value):
def set_jid(self, value: Union[JID, str]):
if not isinstance(value, str):
value = str(value)
self.set_item_attr('jid', value)
@@ -85,10 +86,10 @@ class MUCBase(ElementBase):
def del_jid(self):
self.del_item_attr('jid')
def get_role(self):
def get_role(self) -> str:
return self.get_item_attr('role', '')
def set_role(self, value):
def set_role(self, value: str):
# TODO: check for valid role
self.set_item_attr('role', value)
@@ -96,10 +97,10 @@ class MUCBase(ElementBase):
# TODO: set default role
self.del_item_attr('role')
def get_nick(self):
def get_nick(self) -> str:
return self.parent()['from'].resource
def get_room(self):
def get_room(self) -> str:
return self.parent()['from'].bare
def set_nick(self, value):
@@ -220,7 +221,8 @@ class MUCAdminItem(ElementBase):
namespace = NS_ADMIN
name = 'item'
plugin_attrib = 'item'
interfaces = {'role', 'affiliation', 'nick', 'jid'}
interfaces = {'role', 'affiliation', 'nick', 'jid', 'reason'}
sub_interfaces = {'reason'}
class MUCStatus(ElementBase):
@@ -231,3 +233,30 @@ class MUCStatus(ElementBase):
def set_code(self, code: int):
self.xml.attrib['code'] = str(code)
class MUCUserItem(ElementBase):
namespace = NS_USER
name = 'item'
plugin_attrib = 'item'
interfaces = {'role', 'affiliation', 'jid', 'reason', 'nick'}
sub_interfaces = {'reason'}
def get_jid(self) -> Optional[JID]:
jid = self.xml.attrib.get('jid', None)
if jid:
return JID(jid)
return jid
class MUCActor(ElementBase):
namespace = NS_USER
name = 'actor'
plugin_attrib = 'actor'
interfaces = {'jid', 'nick'}
def get_jid(self) -> Optional[JID]:
jid = self.xml.attrib.get('jid', None)
if jid:
return JID(jid)
return jid

View File

@@ -174,6 +174,9 @@ class XEP_0198(BasePlugin):
def send_ack(self):
"""Send the current ack count to the server."""
if not self.xmpp.transport:
log.debug('Disconnected: not sending ack')
return
ack = stanza.Ack(self.xmpp)
ack['h'] = self.handled
self.xmpp.send_raw(str(ack))
@@ -198,20 +201,7 @@ class XEP_0198(BasePlugin):
# We've already negotiated stream management,
# so no need to do it again.
return False
if not self.sm_id:
if 'bind' in self.xmpp.features:
enable = stanza.Enable(self.xmpp)
enable['resume'] = self.allow_resume
enable.send()
log.debug("enabling SM")
waiter = Waiter('enabled_or_failed',
MatchMany([
MatchXPath(stanza.Enabled.tag_name()),
MatchXPath(stanza.Failed.tag_name())]))
self.xmpp.register_handler(waiter)
result = await waiter.wait()
elif self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features:
if self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features:
resume = stanza.Resume(self.xmpp)
resume['h'] = self.handled
resume['previd'] = self.sm_id
@@ -229,6 +219,19 @@ class XEP_0198(BasePlugin):
result = await waiter.wait()
if result is not None and result.name == 'resumed':
return True
self.xmpp.event("session_end")
if 'bind' in self.xmpp.features:
enable = stanza.Enable(self.xmpp)
enable['resume'] = self.allow_resume
enable.send()
log.debug("enabling SM")
waiter = Waiter('enabled_or_failed',
MatchMany([
MatchXPath(stanza.Enabled.tag_name()),
MatchXPath(stanza.Failed.tag_name())]))
self.xmpp.register_handler(waiter)
result = await waiter.wait()
return False
def _handle_enabled(self, stanza):

View File

@@ -9,7 +9,8 @@
import time
import logging
from typing import Optional, Callable
from asyncio import Future
from typing import Optional, Callable, List
from slixmpp.jid import JID
from slixmpp.stanza import Iq
@@ -64,9 +65,10 @@ class XEP_0199(BasePlugin):
"""
Start the XEP-0199 plugin.
"""
register_stanza_plugin(Iq, Ping)
self.__pending_futures: List[Future] = []
self.xmpp.register_handler(
Callback('Ping',
StanzaPath('iq@type=get/ping'),
@@ -75,7 +77,9 @@ class XEP_0199(BasePlugin):
if self.keepalive:
self.xmpp.add_event_handler('session_start',
self.enable_keepalive)
self.xmpp.add_event_handler('session_end',
self.xmpp.add_event_handler('session_resumed',
self.enable_keepalive)
self.xmpp.add_event_handler('disconnected',
self.disable_keepalive)
def plugin_end(self):
@@ -84,12 +88,23 @@ class XEP_0199(BasePlugin):
if self.keepalive:
self.xmpp.del_event_handler('session_start',
self.enable_keepalive)
self.xmpp.del_event_handler('session_end',
self.xmpp.del_event_handler('session_resumed',
self.enable_keepalive)
self.xmpp.del_event_handler('disconnected',
self.disable_keepalive)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Ping.namespace)
def _clear_pending_futures(self):
"""Cancel all pending ping futures"""
if self.__pending_futures:
log.debug('Clearing %s pdnding pings', len(self.__pending_futures))
for future in self.__pending_futures:
future.cancel()
self.__pending_futures.clear()
def enable_keepalive(self, interval=None, timeout=None):
if interval:
self.interval = interval
@@ -97,18 +112,31 @@ class XEP_0199(BasePlugin):
self.timeout = timeout
self.keepalive = True
handler = lambda event=None: asyncio.ensure_future(
self._keepalive(event),
loop=self.xmpp.loop,
)
def handler(event=None):
# Cleanup futures
if self.__pending_futures:
tmp_futures = []
for future in self.__pending_futures[:]:
if not future.done():
tmp_futures.append(future)
self.__pending_futures = tmp_futures
future = asyncio.ensure_future(
self._keepalive(event),
loop=self.xmpp.loop,
)
self.__pending_futures.append(future)
self.xmpp.schedule('Ping keepalive',
self.interval,
handler,
repeat=True)
def disable_keepalive(self, event=None):
self._clear_pending_futures()
self.xmpp.cancel_schedule('Ping keepalive')
session_end = disable_keepalive
async def _keepalive(self, event=None):
log.debug("Keepalive ping...")
try:

View File

@@ -0,0 +1,13 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2021 Mathieu Pasquet <mathieui@mathieui.net>
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0382.stanza import *
from slixmpp.plugins.xep_0382.spoiler import XEP_0382
register_plugin(XEP_0382)

View File

@@ -0,0 +1,32 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2021 Mathieu Pasquet <mathieui@mathieui.net>
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp import JID
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0382 import stanza
from slixmpp.stanza import Message
class XEP_0382(BasePlugin):
'''XEP-0382: Spoiler Messages'''
name = 'xep_0382'
description = 'Spoiler Messages'
dependencies = {'xep_0030'}
stanza = stanza
namespace = stanza.NS
def plugin_init(self) -> None:
stanza.register_plugins()
Message.sub_interfaces.add('spoiler')
def session_bind(self, jid: JID):
self.xmpp['xep_0030'].add_feature(stanza.NS)
def plugin_end(self):
self.xmpp.plugin['xep_0030'].del_feature(feature=stanza.NS)
Message.sub_interfaces.remove('spoiler')

View File

@@ -0,0 +1,26 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2021 Mathieu Pasquet <mathieui@mathieui.net>
This file is part of Slixmpp.
See the file LICENSE for copying permissio
"""
from slixmpp.stanza import Message
from slixmpp.xmlstream import (
register_stanza_plugin,
ElementBase,
)
NS = 'urn:xmpp:spoiler:0'
class Spoiler(ElementBase):
namespace = NS
name = 'spoiler'
plugin_attrib = 'spoiler'
def register_plugins():
register_stanza_plugin(Message, Spoiler)

View File

@@ -45,8 +45,7 @@ class XEP_0424(BasePlugin):
self.xmpp.plugin['xep_0030'].del_feature(feature=stanza.NS)
def _handle_retract_message(self, message: Message):
if message['type'] != 'groupchat':
self.xmpp.event('message_retract', message)
self.xmpp.event('message_retract', message)
def send_retraction(self, mto: JID, id: str, mtype: str = 'chat',
include_fallback: bool = True,

View File

@@ -10,6 +10,8 @@ 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']
except ImportError:
UNICODE_EMOJI = None

View File

@@ -194,9 +194,11 @@ class Iq(RootStanza):
def callback_success(result):
type_ = result['type']
if type_ == 'result':
future.set_result(result)
if not future.done():
future.set_result(result)
elif type_ == 'error':
future.set_exception(IqError(result))
if not future.done():
future.set_exception(IqError(result))
else:
# Most likely an iq addressed to ourself, rearm the callback.
handler = constr(handler_name,
@@ -212,7 +214,8 @@ class Iq(RootStanza):
callback(result)
def callback_timeout():
future.set_exception(IqTimeout(self))
if not future.done():
future.set_exception(IqTimeout(self))
self.stream.remove_handler('IqCallback_%s' % self['id'])
if timeout_callback is not None:
timeout_callback(self)

View File

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

View File

@@ -12,7 +12,15 @@
:license: MIT, see LICENSE for more details
"""
from typing import Optional, Set, Callable, Any
from typing import (
Any,
Callable,
Iterable,
List,
Optional,
Set,
Union,
)
import functools
import logging
@@ -21,7 +29,7 @@ import ssl
import weakref
import uuid
from asyncio import iscoroutinefunction, wait
from asyncio import iscoroutinefunction, wait, Future
import xml.etree.ElementTree as ET
@@ -224,12 +232,13 @@ class XMLStream(asyncio.BaseProtocol):
self.disconnect_reason = None
#: An asyncio Future being done when the stream is disconnected.
self.disconnected = asyncio.Future()
self.disconnected: Future = Future()
self.add_event_handler('disconnected', self._remove_schedules)
self.add_event_handler('session_start', self._start_keepalive)
self._run_filters = None
self._run_out_filters: Optional[Future] = None
self.__slow_tasks: List[Future] = []
@property
def loop(self):
@@ -250,6 +259,12 @@ class XMLStream(asyncio.BaseProtocol):
"""
return uuid.uuid4().hex
def _set_disconnected_future(self):
"""Set the self.disconnected future on disconnect"""
if not self.disconnected.done():
self.disconnected.set_result(True)
self.disconnected = asyncio.Future()
def connect(self, host='', port=0, use_ssl=False,
force_starttls=True, disable_starttls=False):
"""Create a new socket and connect to the server.
@@ -272,8 +287,8 @@ class XMLStream(asyncio.BaseProtocol):
localhost
"""
if self._run_filters is None:
self._run_filters = asyncio.ensure_future(
if self._run_out_filters is None or self._run_out_filters.done():
self._run_out_filters = asyncio.ensure_future(
self.run_filters(),
loop=self.loop,
)
@@ -418,10 +433,10 @@ class XMLStream(asyncio.BaseProtocol):
if self.xml_depth == 0:
# The stream's root element has closed,
# terminating the stream.
self.end_session_on_disconnect = True
log.debug("End of stream received")
self.disconnect_reason = "End of stream"
self.abort()
return
elif self.xml_depth == 1:
# A stanza is an XML element that is a direct child of
# the root element, hence the check of depth == 1
@@ -458,16 +473,17 @@ class XMLStream(asyncio.BaseProtocol):
closure of the TCP connection
"""
log.info("connection_lost: %s", (exception,))
self.event("disconnected", self.disconnect_reason or exception and exception.strerror)
if self.end_session_on_disconnect:
self.event('session_end')
# All these objects are associated with one TCP connection. Since
# we are not connected anymore, destroy them
self.parser = None
self.transport = None
self.socket = None
if self._run_filters:
self._run_filters.cancel()
# Fire the events after cleanup
if self.end_session_on_disconnect:
self._reset_sendq()
self.event('session_end')
self._set_disconnected_future()
self.event("disconnected", self.disconnect_reason or exception and exception.strerror)
def cancel_connection_attempt(self):
"""
@@ -479,11 +495,8 @@ class XMLStream(asyncio.BaseProtocol):
if self._current_connection_attempt:
self._current_connection_attempt.cancel()
self._current_connection_attempt = None
if self._run_filters:
self._run_filters.cancel()
def disconnect(self, wait: float = 2.0, reason: Optional[str] = None, ignore_send_queue: bool = False) -> None:
def disconnect(self, wait: Union[float, int] = 2.0, reason: Optional[str] = None, ignore_send_queue: bool = False) -> Future:
"""Close the XML stream and wait for an acknowldgement from the server for
at most `wait` seconds. After the given number of seconds has
passed without a response from the server, or when the server
@@ -491,10 +504,13 @@ class XMLStream(asyncio.BaseProtocol):
called. If wait is 0.0, this will call abort() directly without closing
the stream.
Does nothing if we are not connected.
Does nothing but trigger the disconnected event if we are not connected.
:param wait: Time to wait for a response from the server.
:param reason: An optional reason for the disconnect.
:param ignore_send_queue: Boolean to toggle if we want to ignore
the in-flight stanzas and disconnect immediately.
:return: A future that ends when all code involved in the disconnect has ended
"""
# Compat: docs/getting_started/sendlogout.rst has been promoting
# `disconnect(wait=True)` for ages. This doesn't mean anything to the
@@ -504,50 +520,75 @@ class XMLStream(asyncio.BaseProtocol):
wait = 2.0
if self.transport:
self.disconnect_reason = reason
if self.waiting_queue.empty() or ignore_send_queue:
self.disconnect_reason = reason
self.cancel_connection_attempt()
if wait > 0.0:
self.send_raw(self.stream_footer)
self.schedule('Disconnect wait', wait,
self.abort, repeat=False)
return asyncio.ensure_future(
self._end_stream_wait(wait, reason=reason),
loop=self.loop,
)
else:
asyncio.ensure_future(
return asyncio.ensure_future(
self._consume_send_queue_before_disconnecting(reason, wait),
loop=self.loop,
)
else:
self._set_disconnected_future()
self.event("disconnected", reason)
future = Future()
future.set_result(None)
return future
async def _consume_send_queue_before_disconnecting(self, reason: Optional[str], wait: float):
"""Wait until the send queue is empty before disconnecting"""
await self.waiting_queue.join()
try:
await asyncio.wait_for(
self.waiting_queue.join(),
wait,
loop=self.loop
)
except asyncio.TimeoutError:
wait = 0 # we already consumed the timeout
self.disconnect_reason = reason
self.cancel_connection_attempt()
if wait > 0.0:
await self._end_stream_wait(wait)
async def _end_stream_wait(self, wait: Union[int, float] = 2, reason: Optional[str] = None):
"""
Run abort() if we do not received the disconnected event
after a waiting time.
:param wait: The waiting time (defaults to 2)
"""
try:
self.send_raw(self.stream_footer)
self.schedule('Disconnect wait', wait,
self.abort, repeat=False)
await self.wait_until('disconnected', wait)
except asyncio.TimeoutError:
self.abort()
except NotConnectedError:
# We are not connected when sending the end of stream
# that means the disconnect has already been handled
pass
def abort(self):
"""
Forcibly close the connection
"""
self.cancel_connection_attempt()
if self.transport:
self.cancel_connection_attempt()
self.transport.close()
self.transport.abort()
self.event("killed")
self.disconnected.set_result(True)
self.disconnected = asyncio.Future()
self.event("disconnected", self.disconnect_reason)
def reconnect(self, wait=2.0, reason="Reconnecting"):
"""Calls disconnect(), and once we are disconnected (after the timeout, or
when the server acknowledgement is received), call connect()
"""
log.debug("reconnecting...")
self.add_event_handler('disconnected', lambda event: self.connect(), disposable=True)
async def handler(event):
# We yield here to allow synchronous handlers to work first
await asyncio.sleep(0, loop=self.loop)
self.connect()
self.add_event_handler('disconnected', handler, disposable=True)
self.disconnect(wait, reason)
def configure_socket(self):
@@ -622,6 +663,10 @@ class XMLStream(asyncio.BaseProtocol):
else:
self.event('ssl_invalid_chain', e)
return False
except OSError as exc:
log.debug("Connection error:", exc_info=True)
self.disconnect()
return False
der_cert = transp.get_extra_info("ssl_object").getpeercert(True)
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
self.event('ssl_cert', pem_cert)
@@ -651,7 +696,6 @@ class XMLStream(asyncio.BaseProtocol):
def _remove_schedules(self, event):
"""Remove some schedules that become pointless when disconnected"""
self.cancel_schedule('Whitespace Keepalive')
self.cancel_schedule('Disconnect wait')
def start_stream_handler(self, xml):
"""Perform any initialization actions, such as handshakes,
@@ -829,7 +873,7 @@ class XMLStream(asyncio.BaseProtocol):
"""
log.debug("Event triggered: %s", name)
handlers = self.__event_handlers.get(name, [])
handlers = self.__event_handlers.get(name, [])[:]
for handler in handlers:
handler_callback, disposable = handler
old_exception = getattr(data, 'exception', None)
@@ -937,6 +981,18 @@ class XMLStream(asyncio.BaseProtocol):
"""
return xml
def _reset_sendq(self):
"""Clear sending tasks on session end"""
# Cancel all pending slow send tasks
log.debug('Cancelling %d slow send tasks', len(self.__slow_tasks))
for slow_task in self.__slow_tasks:
slow_task.cancel()
self.__slow_tasks.clear()
# Purge pending stanzas
while not self.waiting_queue.empty():
discarded = self.waiting_queue.get_nowait()
log.debug('Discarded stanza: %s', discarded)
async def _continue_slow_send(
self,
task: asyncio.Task,
@@ -950,6 +1006,7 @@ class XMLStream(asyncio.BaseProtocol):
:param set already_used: Filters already used on this outgoing stanza
"""
data = await task
self.__slow_tasks.remove(task)
for filter in self.__filters['out']:
if filter in already_used:
continue
@@ -971,7 +1028,6 @@ class XMLStream(asyncio.BaseProtocol):
else:
self.send_raw(data)
async def run_filters(self):
"""
Background loop that processes stanzas to send.
@@ -991,11 +1047,13 @@ class XMLStream(asyncio.BaseProtocol):
timeout=1,
)
if pending:
self.slow_tasks.append(task)
asyncio.ensure_future(
self._continue_slow_send(
task,
already_run_filters
)
),
loop=self.loop,
)
raise Exception("Slow coro, rescheduling")
data = task.result()
@@ -1138,9 +1196,15 @@ class XMLStream(asyncio.BaseProtocol):
:param int timeout: Timeout
"""
fut = asyncio.Future()
def result_handler(event_data):
if not fut.done():
fut.set_result(event_data)
else:
log.debug("Future registered on event '%s' was alredy done", event)
self.add_event_handler(
event,
fut.set_result,
result_handler,
disposable=True,
)
return await asyncio.wait_for(fut, timeout)
return await asyncio.wait_for(fut, timeout, loop=self.loop)

View File

@@ -13,6 +13,8 @@ from slixmpp.plugins.xep_0045.stanza import (
MUCStatus,
MUCInvite,
MUCDecline,
MUCUserItem,
MUCActor,
)
from slixmpp.xmlstream import register_stanza_plugin, ET
@@ -20,6 +22,9 @@ from slixmpp.xmlstream import register_stanza_plugin, ET
class TestMUC(SlixTest):
def setUp(self):
register_stanza_plugin(MUCPresence, MUCUserItem)
register_stanza_plugin(MUCMessage, MUCUserItem)
register_stanza_plugin(MUCUserItem, MUCActor)
register_stanza_plugin(MUCMessage, MUCInvite)
register_stanza_plugin(MUCMessage, MUCDecline)
register_stanza_plugin(MUCMessage, MUCStatus)
@@ -33,7 +38,46 @@ class TestMUC(SlixTest):
register_stanza_plugin(MUCOwnerQuery, MUCOwnerDestroy)
register_stanza_plugin(MUCAdminQuery, MUCAdminItem, iterable=True)
def testPresence(self):
presence = Presence()
presence['from'] = JID('muc@service/nick')
presence['muc']['item']['affiliation'] = 'member'
presence['muc']['item']['role'] = 'participant'
presence['muc']['status_codes'] = (100, 110, 210)
self.check(presence, """
<presence from='muc@service/nick'>
<x xmlns='http://jabber.org/protocol/muc#user'>
<item affiliation='member' role='participant'/>
<status code='100'/>
<status code='110'/>
<status code='210'/>
</x>
</presence>
""", use_values=False)
def testPresenceReason(self):
presence = Presence()
presence['from'] = JID('muc@service/nick')
presence['muc']['item']['affiliation'] = 'member'
presence['muc']['item']['role'] = 'participant'
presence['muc']['item']['reason'] = 'coucou'
presence['muc']['item']['actor']['nick'] = 'JPR'
self.check(presence, """
<presence from='muc@service/nick'>
<x xmlns='http://jabber.org/protocol/muc#user'>
<item affiliation='member' role='participant'>
<actor nick="JPR"/>
<reason>coucou</reason>
</item>
</x>
</presence>
""", use_values=False)
def testPresenceLegacy(self):
presence = Presence()
presence['from'] = JID('muc@service/nick')
presence['muc']['affiliation'] = 'member'
@@ -96,5 +140,20 @@ class TestMUC(SlixTest):
</iq>
""", use_values=False)
def testSetAffiliation(self):
iq = Iq()
iq['type'] = 'set'
iq['id'] = '1'
iq['mucadmin_query']['item']['jid'] = JID('test@example.com')
iq['mucadmin_query']['item']['affiliation'] = 'owner'
self.check(iq, """
<iq type='set' id='1'>
<query xmlns='http://jabber.org/protocol/muc#admin'>
<item jid='test@example.com'
affiliation='owner'/>
</query>
</iq>
""", use_values=False)
suite = unittest.TestLoader().loadTestsFromTestCase(TestMUC)

View File

@@ -13,6 +13,11 @@ from slixmpp.plugins.xep_0444 import XEP_0444
import slixmpp.plugins.xep_0444.stanza as stanza
from slixmpp.xmlstream import register_stanza_plugin
try:
import emoji
except ImportError:
emoji = None
class TestReactions(SlixTest):
@@ -41,13 +46,9 @@ class TestReactions(SlixTest):
self.assertEqual({'😃', '🤗'}, msg['reactions']['values'])
@unittest.skipIf(emoji is None, 'Emoji package not installed')
def testCreateReactionsUnrestricted(self):
"""Testing creating Reactions with the extra all_chars arg."""
try:
import emoji
except ImportError:
# No emoji package: this test does not make sense
return
xmlstring = """
<message>
<reactions xmlns="urn:xmpp:reactions:0" id="abcd">