Compare commits
53 Commits
slix-1.6.0
...
slix-1.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee671dfb29 | ||
|
|
d954283fb6 | ||
|
|
ed2c03fade | ||
|
|
a381267d21 | ||
|
|
1e1576473b | ||
|
|
dbcd0c6050 | ||
|
|
f93af07882 | ||
|
|
3f739e513b | ||
|
|
fc7d7b4eb7 | ||
|
|
3642e2c7f4 | ||
|
|
f15311bda8 | ||
|
|
b2dfb4c1f3 | ||
|
|
d227579d56 | ||
|
|
571774edb4 | ||
|
|
456dff0b61 | ||
|
|
a0b6bfcefe | ||
|
|
9fbd40578c | ||
|
|
8700f8d162 | ||
|
|
efdcd396d8 | ||
|
|
0eed84d0b2 | ||
|
|
370abb1d98 | ||
|
|
51866f0d46 | ||
|
|
9390794401 | ||
|
|
70b5081018 | ||
|
|
4cb679ae2a | ||
|
|
ab280b44cc | ||
|
|
0193667ace | ||
|
|
9cb5131f1c | ||
|
|
0bf1b96859 | ||
|
|
c6a0da63ae | ||
|
|
3f10dfe138 | ||
|
|
49577e6c84 | ||
|
|
04dcc8628d | ||
|
|
81ebf4e8ba | ||
|
|
b784b68bcd | ||
|
|
f38c61a6b9 | ||
|
|
2631b25e3e | ||
|
|
2b11d81b86 | ||
|
|
ca465032e7 | ||
|
|
6369ee0e5f | ||
|
|
1e23167ce4 | ||
|
|
cccc1253aa | ||
|
|
fb31e9c1fd | ||
|
|
b4dd1e0132 | ||
|
|
525855c17b | ||
|
|
ce0d615786 | ||
|
|
1e08c90018 | ||
|
|
c05cafc963 | ||
|
|
166b265de0 | ||
|
|
d91eea3a3a | ||
|
|
569b9c5ee2 | ||
|
|
a3ca4c11c3 | ||
|
|
489e419e38 |
20
doap.xml
20
doap.xml
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
25
itests/test_reconnect.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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 room’s 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
13
slixmpp/plugins/xep_0382/__init__.py
Normal file
13
slixmpp/plugins/xep_0382/__init__.py
Normal 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)
|
||||
32
slixmpp/plugins/xep_0382/spoiler.py
Normal file
32
slixmpp/plugins/xep_0382/spoiler.py
Normal 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')
|
||||
26
slixmpp/plugins/xep_0382/stanza.py
Normal file
26
slixmpp/plugins/xep_0382/stanza.py
Normal 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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user