Compare commits

..

1 Commits

Author SHA1 Message Date
Emmanuel Gil Peyrot
f4683546d9 Manual cleanup of the remaining set([…]) and set((…)). 2016-10-22 13:35:54 +01:00
100 changed files with 383 additions and 2295 deletions

View File

@@ -1,8 +0,0 @@
test:
tags:
- docker
image: ubuntu:latest
script:
- apt update
- apt install -y python3 cython3 gpg
- ./run_tests.py

View File

@@ -1,9 +1,10 @@
language: python language: python
python: python:
- "2.6"
- "2.7"
- "3.2"
- "3.3"
- "3.4" - "3.4"
- "3.5"
- "3.6"
- "3.7-dev"
install: install:
- "pip install ." - "pip install ."
script: testall.py script: testall.py

View File

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

View File

@@ -1,7 +1,7 @@
Slixmpp Slixmpp
######### #########
Slixmpp is an MIT licensed XMPP library for Python 3.5+. It is a fork of Slixmpp is an MIT licensed XMPP library for Python 3.4+. It is a fork of
SleekXMPP. SleekXMPP.
Slixmpp's goals is to only rewrite the core of the library (the low level Slixmpp's goals is to only rewrite the core of the library (the low level
@@ -36,7 +36,7 @@ The Slixmpp Boilerplate
------------------------- -------------------------
Projects using Slixmpp tend to follow a basic pattern for setting up client/component Projects using Slixmpp tend to follow a basic pattern for setting up client/component
connections and configuration. Here is the gist of the boilerplate needed for a Slixmpp connections and configuration. Here is the gist of the boilerplate needed for a Slixmpp
based project. See the documentation or examples directory for more detailed archetypes for based project. See the documetation or examples directory for more detailed archetypes for
Slixmpp projects:: Slixmpp projects::
import logging import logging

View File

@@ -3,8 +3,8 @@
Differences from SleekXMPP Differences from SleekXMPP
========================== ==========================
**Python 3.5+ only** **Python 3.4+ only**
slixmpp will only work on python 3.5 and above. slixmpp will only work on python 3.4 and above.
**Stanza copies** **Stanza copies**
The same stanza object is given through all the handlers; a handler that The same stanza object is given through all the handlers; a handler that

View File

@@ -1,7 +1,7 @@
.. _mucbot: .. _mucbot:
========================= =========================
Multi-User Chat (MUC) Bot Mulit-User Chat (MUC) Bot
========================= =========================
.. note:: .. note::

View File

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

View File

@@ -50,7 +50,7 @@ class ActionBot(slixmpp.ClientXMPP):
register_stanza_plugin(Iq, Action) register_stanza_plugin(Iq, Action)
async def start(self, event): def start(self, event):
""" """
Process the session_start event. Process the session_start event.
@@ -73,7 +73,7 @@ class ActionBot(slixmpp.ClientXMPP):
""" """
self.event('custom_action', iq) self.event('custom_action', iq)
async def _handle_action_event(self, iq): def _handle_action_event(self, iq):
""" """
Respond to the custom action event. Respond to the custom action event.
""" """
@@ -82,20 +82,17 @@ class ActionBot(slixmpp.ClientXMPP):
if method == 'is_prime' and param == '2': if method == 'is_prime' and param == '2':
print("got message: %s" % iq) print("got message: %s" % iq)
rep = iq.reply() iq.reply()
rep['action']['status'] = 'done' iq['action']['status'] = 'done'
await rep.send() iq.send()
elif method == 'bye': elif method == 'bye':
print("got message: %s" % iq) print("got message: %s" % iq)
rep = iq.reply()
rep['action']['status'] = 'done'
await rep.send()
self.disconnect() self.disconnect()
else: else:
print("got message: %s" % iq) print("got message: %s" % iq)
rep = iq.reply() iq.reply()
rep['action']['status'] = 'error' iq['action']['status'] = 'error'
await rep.send() iq.send()
if __name__ == '__main__': if __name__ == '__main__':
# Setup the command line arguments. # Setup the command line arguments.

View File

@@ -43,7 +43,7 @@ class ActionUserBot(slixmpp.ClientXMPP):
register_stanza_plugin(Iq, Action) register_stanza_plugin(Iq, Action)
async def start(self, event): def start(self, event):
""" """
Process the session_start event. Process the session_start event.
@@ -57,11 +57,11 @@ class ActionUserBot(slixmpp.ClientXMPP):
data. data.
""" """
self.send_presence() self.send_presence()
await self.get_roster() self.get_roster()
await self.send_custom_iq() self.send_custom_iq()
async def send_custom_iq(self): def send_custom_iq(self):
"""Create and send two custom actions. """Create and send two custom actions.
If the first action was successful, then send If the first action was successful, then send
@@ -74,14 +74,14 @@ class ActionUserBot(slixmpp.ClientXMPP):
iq['action']['param'] = '2' iq['action']['param'] = '2'
try: try:
resp = await iq.send() resp = iq.send()
if resp['action']['status'] == 'done': if resp['action']['status'] == 'done':
#sending bye #sending bye
iq2 = self.Iq() iq2 = self.Iq()
iq2['to'] = self.action_provider iq2['to'] = self.action_provider
iq2['type'] = 'set' iq2['type'] = 'set'
iq2['action']['method'] = 'bye' iq2['action']['method'] = 'bye'
await iq2.send() iq2.send(block=False)
self.disconnect() self.disconnect()
except XMPPError: except XMPPError:

View File

@@ -55,8 +55,8 @@ class GTalkBot(slixmpp.ClientXMPP):
cert.verify('talk.google.com', der_cert) cert.verify('talk.google.com', der_cert)
logging.debug("CERT: Found GTalk certificate") logging.debug("CERT: Found GTalk certificate")
except cert.CertificateError as err: except cert.CertificateError as err:
logging.error(err.message) log.error(err.message)
self.disconnect() self.disconnect(send_close=False)
def start(self, event): def start(self, event):
""" """

View File

@@ -13,7 +13,7 @@
from slixmpp import ClientXMPP from slixmpp import ClientXMPP
from argparse import ArgumentParser from optparse import OptionParser
import logging import logging
import getpass import getpass
@@ -23,7 +23,7 @@ class HTTPOverXMPPClient(ClientXMPP):
ClientXMPP.__init__(self, jid, password) ClientXMPP.__init__(self, jid, password)
self.register_plugin('xep_0332') # HTTP over XMPP Transport self.register_plugin('xep_0332') # HTTP over XMPP Transport
self.add_event_handler( self.add_event_handler(
'session_start', self.session_start 'session_start', self.session_start, threaded=True
) )
self.add_event_handler('http_request', self.http_request_received) self.add_event_handler('http_request', self.http_request_received)
self.add_event_handler('http_response', self.http_response_received) self.add_event_handler('http_response', self.http_response_received)
@@ -58,40 +58,40 @@ if __name__ == '__main__':
# ./http_over_xmpp.py -J <jid> -P <pwd> -i <ip> -p <port> [-v] # ./http_over_xmpp.py -J <jid> -P <pwd> -i <ip> -p <port> [-v]
# #
parser = ArgumentParser() parser = OptionParser()
# Output verbosity options. # Output verbosity options.
parser.add_argument( parser.add_option(
'-v', '--verbose', help='set logging to DEBUG', action='store_const', '-v', '--verbose', help='set logging to DEBUG', action='store_const',
dest='loglevel', const=logging.DEBUG, default=logging.ERROR dest='loglevel', const=logging.DEBUG, default=logging.ERROR
) )
# JID and password options. # JID and password options.
parser.add_argument('-J', '--jid', dest='jid', help='JID') parser.add_option('-J', '--jid', dest='jid', help='JID')
parser.add_argument('-P', '--password', dest='password', help='Password') parser.add_option('-P', '--password', dest='password', help='Password')
# XMPP server ip and port options. # XMPP server ip and port options.
parser.add_argument( parser.add_option(
'-i', '--ipaddr', dest='ipaddr', '-i', '--ipaddr', dest='ipaddr',
help='IP Address of the XMPP server', default=None help='IP Address of the XMPP server', default=None
) )
parser.add_argument( parser.add_option(
'-p', '--port', dest='port', '-p', '--port', dest='port',
help='Port of the XMPP server', default=None help='Port of the XMPP server', default=None
) )
args = parser.parse_args() opts, args = parser.parse_args()
# Setup logging. # Setup logging.
logging.basicConfig(level=args.loglevel, logging.basicConfig(level=opts.loglevel,
format='%(levelname)-8s %(message)s') format='%(levelname)-8s %(message)s')
if args.jid is None: if opts.jid is None:
args.jid = input('Username: ') opts.jid = input('Username: ')
if args.password is None: if opts.password is None:
args.password = getpass.getpass('Password: ') opts.password = getpass.getpass('Password: ')
xmpp = HTTPOverXMPPClient(args.jid, args.password) xmpp = HTTPOverXMPPClient(opts.jid, opts.password)
xmpp.connect() xmpp.connect()
xmpp.process() xmpp.process()

View File

@@ -1,92 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2018 Emmanuel Gil Peyrot
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from getpass import getpass
from argparse import ArgumentParser
import slixmpp
from slixmpp import asyncio
log = logging.getLogger(__name__)
class HttpUpload(slixmpp.ClientXMPP):
"""
A basic client asking an entity if they confirm the access to an HTTP URL.
"""
def __init__(self, jid, password, recipient, filename):
slixmpp.ClientXMPP.__init__(self, jid, password)
self.recipient = recipient
self.filename = filename
self.add_event_handler("session_start", self.start)
@asyncio.coroutine
def start(self, event):
log.info('Uploading file %s...', self.filename)
url = yield from self['xep_0363'].upload_file(self.filename)
log.info('Upload success!')
log.info('Sending file to %s', self.recipient)
html = '<body xmlns="http://www.w3.org/1999/xhtml"><a href="%s">%s</a></body>' % (url, url)
self.send_message(self.recipient, url, mhtml=html)
self.disconnect()
if __name__ == '__main__':
# Setup the command line arguments.
parser = ArgumentParser()
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)
# 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")
# Other options.
parser.add_argument("-r", "--recipient", required=True,
help="Recipient JID")
parser.add_argument("-f", "--file", required=True,
help="File to send")
args = parser.parse_args()
# Setup logging.
logging.basicConfig(level=args.loglevel,
format='%(levelname)-8s %(message)s')
if args.jid is None:
args.jid = input("Username: ")
if args.password is None:
args.password = getpass("Password: ")
xmpp = HttpUpload(args.jid, args.password, args.recipient, args.file)
xmpp.register_plugin('xep_0071')
xmpp.register_plugin('xep_0128')
xmpp.register_plugin('xep_0363')
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process(forever=False)

View File

@@ -1,98 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2017 Mathieu Pasquet
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from getpass import getpass
from argparse import ArgumentParser
import slixmpp
from slixmpp.exceptions import XMPPError
from slixmpp import asyncio
log = logging.getLogger(__name__)
class MAM(slixmpp.ClientXMPP):
"""
A basic client fetching mam archive messages
"""
def __init__(self, jid, password, remote_jid, start):
slixmpp.ClientXMPP.__init__(self, jid, password)
self.remote_jid = remote_jid
self.start_date = start
self.add_event_handler("session_start", self.start)
async def start(self, *args):
"""
Fetch mam results for the specified JID.
Use RSM to paginate the results.
"""
results = self.plugin['xep_0313'].retrieve(jid=self.remote_jid, iterator=True, rsm={'max': 10}, start=self.start_date)
page = 1
async for rsm in results:
print('Page %d' % page)
for msg in rsm['mam']['results']:
forwarded = msg['mam_result']['forwarded']
timestamp = forwarded['delay']['stamp']
message = forwarded['stanza']
print('[%s] %s: %s' % (timestamp, message['from'], message['body']))
page += 1
self.disconnect()
if __name__ == '__main__':
# Setup the command line arguments.
parser = ArgumentParser()
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)
# 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")
# Other options
parser.add_argument("-r", "--remote-jid", dest="remote_jid",
help="Remote JID")
parser.add_argument("--start", help="Start date", default='2017-09-20T12:00:00Z')
args = parser.parse_args()
# Setup logging.
logging.basicConfig(level=args.loglevel,
format='%(levelname)-8s %(message)s')
if args.jid is None:
args.jid = input("Username: ")
if args.password is None:
args.password = getpass("Password: ")
if args.remote_jid is None:
args.remote_jid = input("Remote JID: ")
if args.start is None:
args.start = input("Start time: ")
xmpp = MAM(args.jid, args.password, args.remote_jid, args.start)
xmpp.register_plugin('xep_0313')
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process(forever=False)

View File

@@ -1,120 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from getpass import getpass
from argparse import ArgumentParser
import slixmpp
from slixmpp.plugins.xep_0394 import stanza as markup_stanza
class EchoBot(slixmpp.ClientXMPP):
"""
A simple Slixmpp bot that will echo messages it
receives, along with a short thank you message.
"""
def __init__(self, jid, password):
slixmpp.ClientXMPP.__init__(self, jid, password)
# The session_start event will be triggered when
# the bot establishes its connection with the server
# and the XML streams are ready for use. We want to
# listen for this event so that we we can initialize
# our roster.
self.add_event_handler("session_start", self.start)
# The message event is triggered whenever a message
# stanza is received. Be aware that that includes
# MUC messages and error messages.
self.add_event_handler("message", self.message)
def start(self, event):
"""
Process the session_start event.
Typical actions for the session_start event are
requesting the roster and broadcasting an initial
presence stanza.
Arguments:
event -- An empty dictionary. The session_start
event does not provide any additional
data.
"""
self.send_presence()
self.get_roster()
def message(self, msg):
"""
Process incoming message stanzas. Be aware that this also
includes MUC messages and error messages. It is usually
a good idea to check the messages's type before processing
or sending replies.
Arguments:
msg -- The received message stanza. See the documentation
for stanza objects and the Message stanza to see
how it may be used.
"""
body = msg['body']
new_body = self['xep_0394'].to_plain_text(body, msg['markup'])
xhtml = self['xep_0394'].to_xhtml_im(body, msg['markup'])
print('Plain text:', new_body)
print('XHTML-IM:', xhtml['body'])
message = msg.reply()
message['body'] = new_body
message['html']['body'] = xhtml['body']
self.send(message)
if __name__ == '__main__':
# Setup the command line arguments.
parser = ArgumentParser(description=EchoBot.__doc__)
# 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)
# 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")
args = parser.parse_args()
# Setup logging.
logging.basicConfig(level=args.loglevel,
format='%(levelname)-8s %(message)s')
if args.jid is None:
args.jid = input("Username: ")
if args.password is None:
args.password = getpass("Password: ")
# Setup the EchoBot and register plugins. Note that while plugins may
# have interdependencies, the order in which you register them does
# not matter.
xmpp = EchoBot(args.jid, args.password)
xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # XMPP Ping
xmpp.register_plugin('xep_0394') # Message Markup
# Connect to the XMPP server and start processing XMPP stanzas.
xmpp.connect()
xmpp.process()

View File

@@ -21,7 +21,7 @@ class PubsubClient(slixmpp.ClientXMPP):
self.register_plugin('xep_0059') self.register_plugin('xep_0059')
self.register_plugin('xep_0060') self.register_plugin('xep_0060')
self.actions = ['nodes', 'create', 'delete', 'get_configure', self.actions = ['nodes', 'create', 'delete',
'publish', 'get', 'retract', 'publish', 'get', 'retract',
'purge', 'subscribe', 'unsubscribe'] 'purge', 'subscribe', 'unsubscribe']
@@ -40,7 +40,7 @@ class PubsubClient(slixmpp.ClientXMPP):
try: try:
yield from getattr(self, self.action)() yield from getattr(self, self.action)()
except: except:
logging.exception('Could not execute %s:', self.action) logging.error('Could not execute: %s', self.action)
self.disconnect() self.disconnect()
def nodes(self): def nodes(self):
@@ -65,13 +65,6 @@ class PubsubClient(slixmpp.ClientXMPP):
except XMPPError as error: except XMPPError as error:
logging.error('Could not delete node %s: %s', self.node, error.format()) logging.error('Could not delete node %s: %s', self.node, error.format())
def get_configure(self):
try:
configuration_form = yield from self['xep_0060'].get_node_config(self.pubsub_server, self.node)
logging.info('Configure form received from node %s: %s', self.node, configuration_form['pubsub_owner']['configure']['form'])
except XMPPError as error:
logging.error('Could not retrieve configure form from node %s: %s', self.node, error.format())
def publish(self): def publish(self):
payload = ET.fromstring("<test xmlns='test'>%s</test>" % self.data) payload = ET.fromstring("<test xmlns='test'>%s</test>" % self.data)
try: try:
@@ -125,7 +118,7 @@ if __name__ == '__main__':
parser = ArgumentParser() parser = ArgumentParser()
parser.version = '%%prog 0.1' parser.version = '%%prog 0.1'
parser.usage = "Usage: %%prog [options] <jid> " + \ parser.usage = "Usage: %%prog [options] <jid> " + \
'nodes|create|delete|get_configure|purge|subscribe|unsubscribe|publish|retract|get' + \ 'nodes|create|delete|purge|subscribe|unsubscribe|publish|retract|get' + \
' [<node> <data>]' ' [<node> <data>]'
parser.add_argument("-q","--quiet", help="set logging to ERROR", parser.add_argument("-q","--quiet", help="set logging to ERROR",
@@ -146,7 +139,7 @@ if __name__ == '__main__':
help="password to use") help="password to use")
parser.add_argument("server") parser.add_argument("server")
parser.add_argument("action", choices=["nodes", "create", "delete", "get_configure", "purge", "subscribe", "unsubscribe", "publish", "retract", "get"]) parser.add_argument("action", choices=["nodes", "create", "delete", "purge", "subscribe", "unsubscribe", "publish", "retract", "get"])
parser.add_argument("node", nargs='?') parser.add_argument("node", nargs='?')
parser.add_argument("data", nargs='?') parser.add_argument("data", nargs='?')

View File

@@ -9,7 +9,7 @@
import os import os
from pathlib import Path from pathlib import Path
from subprocess import call, DEVNULL, check_output, CalledProcessError from subprocess import call, DEVNULL
from tempfile import TemporaryFile from tempfile import TemporaryFile
try: try:
from setuptools import setup from setuptools import setup
@@ -29,40 +29,24 @@ CLASSIFIERS = [
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Topic :: Internet :: XMPP',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
] ]
packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')] packages = [str(mod.parent) for mod in Path('slixmpp').rglob('__init__.py')]
def check_include(library_name, header): def check_include(header):
command = [os.environ.get('PKG_CONFIG', 'pkg-config'), '--cflags', library_name] command = [os.environ.get('CC', 'cc'), '-E', '-']
try:
cflags = check_output(command).decode('utf-8').split()
except FileNotFoundError:
print('pkg-config not found.')
return False
except CalledProcessError:
# pkg-config already prints the missing libraries on stderr.
return False
command = [os.environ.get('CC', 'cc')] + cflags + ['-E', '-']
with TemporaryFile('w+') as c_file: with TemporaryFile('w+') as c_file:
c_file.write('#include <%s>' % header) c_file.write('#include <%s>' % header)
c_file.seek(0) c_file.seek(0)
try: try:
return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0 return call(command, stdin=c_file, stdout=DEVNULL, stderr=DEVNULL) == 0
except FileNotFoundError: except FileNotFoundError:
print('%s headers not found.' % library_name)
return False return False
HAS_PYTHON_HEADERS = check_include('python3', 'Python.h')
HAS_STRINGPREP_HEADERS = check_include('libidn', 'stringprep.h')
ext_modules = None ext_modules = None
if HAS_PYTHON_HEADERS and HAS_STRINGPREP_HEADERS: if check_include('stringprep.h'):
try: try:
from Cython.Build import cythonize from Cython.Build import cythonize
except ImportError: except ImportError:
@@ -70,7 +54,7 @@ if HAS_PYTHON_HEADERS and HAS_STRINGPREP_HEADERS:
else: else:
ext_modules = cythonize('slixmpp/stringprep.pyx') ext_modules = cythonize('slixmpp/stringprep.pyx')
else: else:
print('Falling back to the slow stringprep module.') print('libidn-dev not found, falling back to the slow stringprep module.')
setup( setup(
name="slixmpp", name="slixmpp",

View File

@@ -6,13 +6,12 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
import asyncio
if hasattr(asyncio, 'sslproto'): # no ssl proto: very old asyncio = no need for this
asyncio.sslproto._is_sslproto_available=lambda: False
import logging import logging
logging.getLogger(__name__).addHandler(logging.NullHandler()) logging.getLogger(__name__).addHandler(logging.NullHandler())
import asyncio
# Required for python < 3.7 to use the old ssl implementation
# and manage to do starttls as an unintended side effect
asyncio.sslproto._is_sslproto_available = lambda: False
from slixmpp.stanza import Message, Presence, Iq from slixmpp.stanza import Message, Presence, Iq
from slixmpp.jid import JID, InvalidJID from slixmpp.jid import JID, InvalidJID

View File

@@ -15,7 +15,6 @@
import asyncio import asyncio
import logging import logging
from slixmpp.jid import JID
from slixmpp.stanza import StreamFeatures from slixmpp.stanza import StreamFeatures
from slixmpp.basexmpp import BaseXMPP from slixmpp.basexmpp import BaseXMPP
from slixmpp.exceptions import XMPPError from slixmpp.exceptions import XMPPError
@@ -109,21 +108,10 @@ class ClientXMPP(BaseXMPP):
CoroutineCallback('Stream Features', CoroutineCallback('Stream Features',
MatchXPath('{%s}features' % self.stream_ns), MatchXPath('{%s}features' % self.stream_ns),
self._handle_stream_features)) self._handle_stream_features))
def roster_push_filter(iq):
from_ = iq['from']
if from_ and from_ != JID('') and from_ != self.boundjid.bare:
reply = iq.reply()
reply['type'] = 'error'
reply['error']['type'] = 'cancel'
reply['error']['code'] = 503
reply['error']['condition'] = 'service-unavailable'
reply.send()
return
self.event('roster_update', iq)
self.register_handler( self.register_handler(
Callback('Roster Update', Callback('Roster Update',
StanzaPath('iq@type=set/roster'), StanzaPath('iq@type=set/roster'),
roster_push_filter)) lambda iq: self.event('roster_update', iq)))
# Setup default stream features # Setup default stream features
self.register_plugin('feature_starttls') self.register_plugin('feature_starttls')
@@ -255,7 +243,7 @@ class ClientXMPP(BaseXMPP):
orig_cb(resp) orig_cb(resp)
callback = wrapped callback = wrapped
return iq.send(callback, timeout, timeout_callback) iq.send(callback, timeout, timeout_callback)
def _reset_connection_state(self, event=None): def _reset_connection_state(self, event=None):
#TODO: Use stream state here #TODO: Use stream state here
@@ -265,7 +253,8 @@ class ClientXMPP(BaseXMPP):
self.bindfail = False self.bindfail = False
self.features = set() self.features = set()
async def _handle_stream_features(self, features): @asyncio.coroutine
def _handle_stream_features(self, features):
"""Process the received stream features. """Process the received stream features.
:param features: The features stanza. :param features: The features stanza.
@@ -274,7 +263,7 @@ class ClientXMPP(BaseXMPP):
if name in features['features']: if name in features['features']:
handler, restart = self._stream_feature_handlers[name] handler, restart = self._stream_feature_handlers[name]
if asyncio.iscoroutinefunction(handler): if asyncio.iscoroutinefunction(handler):
result = await handler(features) result = yield from handler(features)
else: else:
result = handler(features) result = handler(features)
if result and restart: if result and restart:

View File

@@ -35,7 +35,8 @@ class FeatureBind(BasePlugin):
register_stanza_plugin(Iq, stanza.Bind) register_stanza_plugin(Iq, stanza.Bind)
register_stanza_plugin(StreamFeatures, stanza.Bind) register_stanza_plugin(StreamFeatures, stanza.Bind)
async def _handle_bind_resource(self, features): @asyncio.coroutine
def _handle_bind_resource(self, features):
""" """
Handle requesting a specific resource. Handle requesting a specific resource.
@@ -50,7 +51,7 @@ class FeatureBind(BasePlugin):
if self.xmpp.requested_jid.resource: if self.xmpp.requested_jid.resource:
iq['bind']['resource'] = self.xmpp.requested_jid.resource iq['bind']['resource'] = self.xmpp.requested_jid.resource
await iq.send(callback=self._on_bind_response) yield from iq.send(callback=self._on_bind_response)
def _on_bind_response(self, response): def _on_bind_response(self, response):
self.xmpp.boundjid = JID(response['bind']['jid']) self.xmpp.boundjid = JID(response['bind']['jid'])

View File

@@ -97,7 +97,13 @@ class FeatureMechanisms(BasePlugin):
jid = self.xmpp.requested_jid.bare jid = self.xmpp.requested_jid.bare
result[value] = creds.get('email', jid) result[value] = creds.get('email', jid)
elif value == 'channel_binding': elif value == 'channel_binding':
result[value] = self.xmpp.socket.get_channel_binding() if hasattr(self.xmpp.socket, 'get_channel_binding'):
result[value] = self.xmpp.socket.get_channel_binding()
else:
log.debug("Channel binding not supported.")
log.debug("Use Python 3.3+ for channel binding and " + \
"SCRAM-SHA-1-PLUS support")
result[value] = None
elif value == 'host': elif value == 'host':
result[value] = creds.get('host', self.xmpp.requested_jid.domain) result[value] = creds.get('host', self.xmpp.requested_jid.domain)
elif value == 'realm': elif value == 'realm':
@@ -116,7 +122,7 @@ class FeatureMechanisms(BasePlugin):
if value == 'encrypted': if value == 'encrypted':
if 'starttls' in self.xmpp.features: if 'starttls' in self.xmpp.features:
result[value] = True result[value] = True
elif isinstance(self.xmpp.socket, (ssl.SSLSocket, ssl.SSLObject)): elif isinstance(self.xmpp.socket, ssl.SSLSocket):
result[value] = True result[value] = True
else: else:
result[value] = False result[value] = False

View File

@@ -35,22 +35,18 @@ class FeatureSession(BasePlugin):
register_stanza_plugin(Iq, stanza.Session) register_stanza_plugin(Iq, stanza.Session)
register_stanza_plugin(StreamFeatures, stanza.Session) register_stanza_plugin(StreamFeatures, stanza.Session)
async def _handle_start_session(self, features): @asyncio.coroutine
def _handle_start_session(self, features):
""" """
Handle the start of the session. Handle the start of the session.
Arguments: Arguments:
feature -- The stream features element. feature -- The stream features element.
""" """
if features['session']['optional']:
self.xmpp.sessionstarted = True
self.xmpp.event('session_start')
return
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
iq['type'] = 'set' iq['type'] = 'set'
iq.enable('session') iq.enable('session')
await iq.send(callback=self._on_start_session_response) yield from iq.send(callback=self._on_start_session_response)
def _on_start_session_response(self, response): def _on_start_session_response(self, response):
self.xmpp.features.add('session') self.xmpp.features.add('session')

View File

@@ -6,7 +6,7 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from slixmpp.xmlstream import ElementBase, ET from slixmpp.xmlstream import ElementBase
class Session(ElementBase): class Session(ElementBase):
@@ -16,19 +16,5 @@ class Session(ElementBase):
name = 'session' name = 'session'
namespace = 'urn:ietf:params:xml:ns:xmpp-session' namespace = 'urn:ietf:params:xml:ns:xmpp-session'
interfaces = {'optional'} interfaces = set()
plugin_attrib = 'session' plugin_attrib = 'session'
def get_optional(self):
return self.xml.find('{%s}optional' % self.namespace) is not None
def set_optional(self, value):
if value:
optional = ET.Element('{%s}optional' % self.namespace)
self.xml.append(optional)
else:
self.del_optional()
def del_optional(self):
optional = self.xml.find('{%s}optional' % self.namespace)
self.xml.remove(optional)

View File

@@ -12,7 +12,7 @@ from slixmpp.stanza import StreamFeatures
from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream.matcher import MatchXPath from slixmpp.xmlstream.matcher import MatchXPath
from slixmpp.xmlstream.handler import CoroutineCallback from slixmpp.xmlstream.handler import Callback
from slixmpp.features.feature_starttls import stanza from slixmpp.features.feature_starttls import stanza
@@ -28,7 +28,7 @@ class FeatureSTARTTLS(BasePlugin):
def plugin_init(self): def plugin_init(self):
self.xmpp.register_handler( self.xmpp.register_handler(
CoroutineCallback('STARTTLS Proceed', Callback('STARTTLS Proceed',
MatchXPath(stanza.Proceed.tag_name()), MatchXPath(stanza.Proceed.tag_name()),
self._handle_starttls_proceed, self._handle_starttls_proceed,
instream=True)) instream=True))
@@ -58,8 +58,8 @@ class FeatureSTARTTLS(BasePlugin):
self.xmpp.send(features['starttls']) self.xmpp.send(features['starttls'])
return True return True
async def _handle_starttls_proceed(self, proceed): def _handle_starttls_proceed(self, proceed):
"""Restart the XML stream when TLS is accepted.""" """Restart the XML stream when TLS is accepted."""
log.debug("Starting TLS") log.debug("Starting TLS")
if await self.xmpp.start_tls(): if self.xmpp.start_tls():
self.xmpp.features.add('starttls') self.xmpp.features.add('starttls')

View File

@@ -1,47 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin, BasePlugin
from slixmpp.plugins.google.gmail import Gmail
from slixmpp.plugins.google.auth import GoogleAuth
from slixmpp.plugins.google.settings import GoogleSettings
from slixmpp.plugins.google.nosave import GoogleNoSave
class Google(BasePlugin):
"""
Google: Custom GTalk Features
Also see: <https://developers.google.com/talk/jep_extensions/extensions>
"""
name = 'google'
description = 'Google: Custom GTalk Features'
dependencies = set([
'gmail',
'google_settings',
'google_nosave',
'google_auth'
])
def __getitem__(self, attr):
if attr in ('settings', 'nosave', 'auth'):
return self.xmpp['google_%s' % attr]
elif attr == 'gmail':
return self.xmpp['gmail']
else:
raise KeyError(attr)
register_plugin(Gmail)
register_plugin(GoogleAuth)
register_plugin(GoogleSettings)
register_plugin(GoogleNoSave)
register_plugin(Google)

View File

@@ -1,10 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.google.auth import stanza
from slixmpp.plugins.google.auth.auth import GoogleAuth

View File

@@ -1,47 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.google.auth import stanza
class GoogleAuth(BasePlugin):
"""
Google: Auth Extensions (JID Domain Discovery, OAuth2)
Also see:
<https://developers.google.com/talk/jep_extensions/jid_domain_change>
<https://developers.google.com/talk/jep_extensions/oauth>
"""
name = 'google_auth'
description = 'Google: Auth Extensions (JID Domain Discovery, OAuth2)'
dependencies = set(['feature_mechanisms'])
stanza = stanza
def plugin_init(self):
self.xmpp.namespace_map['http://www.google.com/talk/protocol/auth'] = 'ga'
register_stanza_plugin(self.xmpp['feature_mechanisms'].stanza.Auth,
stanza.GoogleAuth)
self.xmpp.add_filter('out', self._auth)
def plugin_end(self):
self.xmpp.del_filter('out', self._auth)
def _auth(self, stanza):
if isinstance(stanza, self.xmpp['feature_mechanisms'].stanza.Auth):
stanza.stream = self.xmpp
stanza['google']['client_uses_full_bind_result'] = True
if stanza['mechanism'] == 'X-OAUTH2':
stanza['google']['service'] = 'oauth2'
print(stanza)
return stanza

View File

@@ -1,10 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.google.gmail import stanza
from slixmpp.plugins.google.gmail.notifications import Gmail

View File

@@ -1,101 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
class GmailQuery(ElementBase):
namespace = 'google:mail:notify'
name = 'query'
plugin_attrib = 'gmail'
interfaces = set(['newer_than_time', 'newer_than_tid', 'search'])
def get_search(self):
return self._get_attr('q', '')
def set_search(self, search):
self._set_attr('q', search)
def del_search(self):
self._del_attr('q')
def get_newer_than_time(self):
return self._get_attr('newer-than-time', '')
def set_newer_than_time(self, value):
self._set_attr('newer-than-time', value)
def del_newer_than_time(self):
self._del_attr('newer-than-time')
def get_newer_than_tid(self):
return self._get_attr('newer-than-tid', '')
def set_newer_than_tid(self, value):
self._set_attr('newer-than-tid', value)
def del_newer_than_tid(self):
self._del_attr('newer-than-tid')
class MailBox(ElementBase):
namespace = 'google:mail:notify'
name = 'mailbox'
plugin_attrib = 'gmail_messages'
interfaces = set(['result_time', 'url', 'matched', 'estimate'])
def get_matched(self):
return self._get_attr('total-matched', '')
def get_estimate(self):
return self._get_attr('total-estimate', '') == '1'
def get_result_time(self):
return self._get_attr('result-time', '')
class MailThread(ElementBase):
namespace = 'google:mail:notify'
name = 'mail-thread-info'
plugin_attrib = 'thread'
plugin_multi_attrib = 'threads'
interfaces = set(['tid', 'participation', 'messages', 'date',
'senders', 'url', 'labels', 'subject', 'snippet'])
sub_interfaces = set(['labels', 'subject', 'snippet'])
def get_senders(self):
result = []
senders = self.xml.findall('{%s}senders/{%s}sender' % (
self.namespace, self.namespace))
for sender in senders:
result.append(MailSender(xml=sender))
return result
class MailSender(ElementBase):
namespace = 'google:mail:notify'
name = 'sender'
plugin_attrib = name
interfaces = set(['address', 'name', 'originator', 'unread'])
def get_originator(self):
return self.xml.attrib.get('originator', '0') == '1'
def get_unread(self):
return self.xml.attrib.get('unread', '0') == '1'
class NewMail(ElementBase):
namespace = 'google:mail:notify'
name = 'new-mail'
plugin_attrib = 'gmail_notification'
register_stanza_plugin(MailBox, MailThread, iterable=True)

View File

@@ -1,10 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.google.nosave import stanza
from slixmpp.plugins.google.nosave.nosave import GoogleNoSave

View File

@@ -1,78 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.stanza import Iq, Message
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.google.nosave import stanza
class GoogleNoSave(BasePlugin):
"""
Google: Off the Record Chats
NOTE: This is NOT an encryption method.
Also see <https://developers.google.com/talk/jep_extensions/otr>.
"""
name = 'google_nosave'
description = 'Google: Off the Record Chats'
dependencies = set(['google_settings'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, stanza.NoSave)
register_stanza_plugin(Iq, stanza.NoSaveQuery)
self.xmpp.register_handler(
Callback('Google Nosave',
StanzaPath('iq@type=set/google_nosave'),
self._handle_nosave_change))
def plugin_end(self):
self.xmpp.remove_handler('Google Nosave')
def enable(self, jid=None, timeout=None, callback=None):
if jid is None:
self.xmpp['google_settings'].update({'archiving_enabled': False},
timeout=timeout, callback=callback)
else:
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['google_nosave']['item']['jid'] = jid
iq['google_nosave']['item']['value'] = True
return iq.send(timeout=timeout, callback=callback)
def disable(self, jid=None, timeout=None, callback=None):
if jid is None:
self.xmpp['google_settings'].update({'archiving_enabled': True},
timeout=timeout, callback=callback)
else:
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['google_nosave']['item']['jid'] = jid
iq['google_nosave']['item']['value'] = False
return iq.send(timeout=timeout, callback=callback)
def get(self, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq.enable('google_nosave')
return iq.send(timeout=timeout, callback=callback)
def _handle_nosave_change(self, iq):
reply = self.xmpp.Iq()
reply['type'] = 'result'
reply['id'] = iq['id']
reply['to'] = iq['from']
reply.send()
self.xmpp.event('google_nosave_change', iq)

View File

@@ -1,10 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.google.settings import stanza
from slixmpp.plugins.google.settings.settings import GoogleSettings

View File

@@ -1,110 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ET, ElementBase
class UserSettings(ElementBase):
name = 'usersetting'
namespace = 'google:setting'
plugin_attrib = 'google_settings'
interfaces = set(['auto_accept_suggestions',
'mail_notifications',
'archiving_enabled',
'gmail',
'email_verified',
'domain_privacy_notice',
'display_name'])
def _get_setting(self, setting):
xml = self.xml.find('{%s}%s' % (self.namespace, setting))
if xml is not None:
return xml.attrib.get('value', '') == 'true'
return False
def _set_setting(self, setting, value):
self._del_setting(setting)
if value in (True, False):
xml = ET.Element('{%s}%s' % (self.namespace, setting))
xml.attrib['value'] = 'true' if value else 'false'
self.xml.append(xml)
def _del_setting(self, setting):
xml = self.xml.find('{%s}%s' % (self.namespace, setting))
if xml is not None:
self.xml.remove(xml)
def get_display_name(self):
xml = self.xml.find('{%s}%s' % (self.namespace, 'displayname'))
if xml is not None:
return xml.attrib.get('value', '')
return ''
def set_display_name(self, value):
self._del_setting(setting)
if value:
xml = ET.Element('{%s}%s' % (self.namespace, 'displayname'))
xml.attrib['value'] = value
self.xml.append(xml)
def del_display_name(self):
self._del_setting('displayname')
def get_auto_accept_suggestions(self):
return self._get_setting('autoacceptsuggestions')
def get_mail_notifications(self):
return self._get_setting('mailnotifications')
def get_archiving_enabled(self):
return self._get_setting('archivingenabled')
def get_gmail(self):
return self._get_setting('gmail')
def get_email_verified(self):
return self._get_setting('emailverified')
def get_domain_privacy_notice(self):
return self._get_setting('domainprivacynotice')
def set_auto_accept_suggestions(self, value):
self._set_setting('autoacceptsuggestions', value)
def set_mail_notifications(self, value):
self._set_setting('mailnotifications', value)
def set_archiving_enabled(self, value):
self._set_setting('archivingenabled', value)
def set_gmail(self, value):
self._set_setting('gmail', value)
def set_email_verified(self, value):
self._set_setting('emailverified', value)
def set_domain_privacy_notice(self, value):
self._set_setting('domainprivacynotice', value)
def del_auto_accept_suggestions(self):
self._del_setting('autoacceptsuggestions')
def del_mail_notifications(self):
self._del_setting('mailnotifications')
def del_archiving_enabled(self):
self._del_setting('archivingenabled')
def del_gmail(self):
self._del_setting('gmail')
def del_email_verified(self):
self._del_setting('emailverified')
def del_domain_privacy_notice(self):
self._del_setting('domainprivacynotice')

View File

@@ -23,7 +23,7 @@ class Form(ElementBase):
namespace = 'jabber:x:data' namespace = 'jabber:x:data'
name = 'x' name = 'x'
plugin_attrib = 'form' plugin_attrib = 'form'
interfaces = OrderedSet(('instructions', 'reported', 'title', 'type', 'items', 'values')) interfaces = OrderedSet(('instructions', 'reported', 'title', 'type', 'items', ))
sub_interfaces = {'title'} sub_interfaces = {'title'}
form_types = {'cancel', 'form', 'result', 'submit'} form_types = {'cancel', 'form', 'result', 'submit'}

View File

@@ -61,7 +61,7 @@ def _intercept(method, name, public):
except InvocationException: except InvocationException:
raise raise
except Exception as e: except Exception as e:
raise InvocationException("A problem occurred calling %s.%s!" % (instance.FQN(), method.__name__), e) raise InvocationException("A problem occured calling %s.%s!" % (instance.FQN(), method.__name__), e)
_resolver._rpc = public _resolver._rpc = public
_resolver._rpc_name = method.__name__ if name is None else name _resolver._rpc_name = method.__name__ if name is None else name
return _resolver return _resolver
@@ -405,10 +405,8 @@ class Proxy(Endpoint):
self._callback = callback self._callback = callback
def __getattribute__(self, name, *args): def __getattribute__(self, name, *args):
if name in ('__dict__', '_endpoint', '_callback'): if name in ('__dict__', '_endpoint', 'async', '_callback'):
return object.__getattribute__(self, name) return object.__getattribute__(self, name)
elif name == 'async':
return lambda callback: Proxy(self._endpoint, callback)
else: else:
attribute = self._endpoint.__getattribute__(name) attribute = self._endpoint.__getattribute__(name)
if hasattr(attribute, '__call__'): if hasattr(attribute, '__call__'):
@@ -422,6 +420,9 @@ class Proxy(Endpoint):
pass # If the attribute doesn't exist, don't care! pass # If the attribute doesn't exist, don't care!
return attribute return attribute
def async(self, callback):
return Proxy(self._endpoint, callback)
def get_endpoint(self): def get_endpoint(self):
''' '''
Returns the proxified endpoint. Returns the proxified endpoint.
@@ -695,7 +696,7 @@ class RemoteSession(object):
e = { e = {
'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])), 'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])),
'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])), 'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])),
'undefined-condition': RemoteException("An unexpected problem occurred trying to invoke %s at %s!" % (pmethod, iq['from'])), 'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])),
}[condition] }[condition]
if e is None: if e is None:
RemoteException("An unexpected exception occurred at %s!" % iq['from']) RemoteException("An unexpected exception occurred at %s!" % iq['from'])

View File

@@ -121,7 +121,7 @@ class XEP_0009(BasePlugin):
def _recipient_unvailable(self, iq): def _recipient_unvailable(self, iq):
payload = iq.get_payload() payload = iq.get_payload()
iq = iq.reply() iq = iq.reply()
iq.error().set_payload(payload) error().set_payload(payload)
iq['error']['code'] = '404' iq['error']['code'] = '404'
iq['error']['type'] = 'wait' iq['error']['type'] = 'wait'
iq['error']['condition'] = 'recipient-unavailable' iq['error']['condition'] = 'recipient-unavailable'

View File

@@ -6,7 +6,6 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
import asyncio
import logging import logging
from slixmpp import Iq from slixmpp import Iq
@@ -124,8 +123,6 @@ class XEP_0030(BasePlugin):
for op in self._disco_ops: for op in self._disco_ops:
self.api.register(getattr(self.static, op), op, default=True) self.api.register(getattr(self.static, op), op, default=True)
self.domain_infos = {}
def session_bind(self, jid): def session_bind(self, jid):
self.add_feature('http://jabber.org/protocol/disco#info') self.add_feature('http://jabber.org/protocol/disco#info')
@@ -298,31 +295,6 @@ class XEP_0030(BasePlugin):
'cached': cached} 'cached': cached}
return self.api['has_identity'](jid, node, ifrom, data) return self.api['has_identity'](jid, node, ifrom, data)
async def get_info_from_domain(self, domain=None, timeout=None,
cached=True, callback=None, **kwargs):
if domain is None:
domain = self.xmpp.boundjid.domain
if not cached or domain not in self.domain_infos:
infos = [self.get_info(
domain, timeout=timeout, **kwargs)]
iq_items = await self.get_items(
domain, timeout=timeout, **kwargs)
items = iq_items['disco_items']['items']
infos += [
self.get_info(item[0], timeout=timeout, **kwargs)
for item in items]
info_futures, _ = await asyncio.wait(infos, timeout=timeout)
self.domain_infos[domain] = [
future.result() for future in info_futures]
results = self.domain_infos[domain]
if callback is not None:
callback(results)
return results
@future_wrapper @future_wrapper
def get_info(self, jid=None, node=None, local=None, def get_info(self, jid=None, node=None, local=None,
cached=None, **kwargs): cached=None, **kwargs):
@@ -674,11 +646,9 @@ class XEP_0030(BasePlugin):
info['id'] = iq['id'] info['id'] = iq['id']
info.send() info.send()
else: else:
node = iq['disco_info']['node']
iq = iq.reply() iq = iq.reply()
if info: if info:
info = self._fix_default_info(info) info = self._fix_default_info(info)
info['node'] = node
iq.set_payload(info.xml) iq.set_payload(info.xml)
iq.send() iq.send()
elif iq['type'] == 'result': elif iq['type'] == 'result':

View File

@@ -66,11 +66,10 @@ class StaticDisco(object):
if isinstance(ifrom, JID): if isinstance(ifrom, JID):
ifrom = ifrom.full ifrom = ifrom.full
if (jid, node, ifrom) not in self.nodes: if (jid, node, ifrom) not in self.nodes:
new_node = {'info': DiscoInfo(), 'items': DiscoItems()} self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(),
new_node['info']['node'] = node 'items': DiscoItems()}
new_node['items']['node'] = node self.nodes[(jid, node, ifrom)]['info']['node'] = node
self.nodes[(jid, node, ifrom)] = new_node self.nodes[(jid, node, ifrom)]['items']['node'] = node
return self.nodes[(jid, node, ifrom)]
def get_node(self, jid=None, node=None, ifrom=None): def get_node(self, jid=None, node=None, ifrom=None):
if jid is None: if jid is None:
@@ -209,8 +208,8 @@ class StaticDisco(object):
The data parameter is a disco#info substanza. The data parameter is a disco#info substanza.
""" """
new_node = self.add_node(jid, node) self.add_node(jid, node)
new_node['info'] = data self.get_node(jid, node)['info'] = data
def del_info(self, jid, node, ifrom, data): def del_info(self, jid, node, ifrom, data):
""" """
@@ -243,8 +242,8 @@ class StaticDisco(object):
items -- A set of items in tuple format. items -- A set of items in tuple format.
""" """
items = data.get('items', set()) items = data.get('items', set())
new_node = self.add_node(jid, node) self.add_node(jid, node)
new_node['items']['items'] = items self.get_node(jid, node)['items']['items'] = items
def del_items(self, jid, node, ifrom, data): def del_items(self, jid, node, ifrom, data):
""" """
@@ -257,7 +256,7 @@ class StaticDisco(object):
def add_identity(self, jid, node, ifrom, data): def add_identity(self, jid, node, ifrom, data):
""" """
Add a new identity to the JID/node combination. Add a new identity to te JID/node combination.
The data parameter may provide: The data parameter may provide:
category -- The general category to which the agent belongs. category -- The general category to which the agent belongs.
@@ -265,8 +264,8 @@ class StaticDisco(object):
name -- Optional human readable name for this identity. name -- Optional human readable name for this identity.
lang -- Optional standard xml:lang value. lang -- Optional standard xml:lang value.
""" """
new_node = self.add_node(jid, node) self.add_node(jid, node)
new_node['info'].add_identity( self.get_node(jid, node)['info'].add_identity(
data.get('category', ''), data.get('category', ''),
data.get('itype', ''), data.get('itype', ''),
data.get('name', None), data.get('name', None),
@@ -281,8 +280,8 @@ class StaticDisco(object):
(category, type, name, lang) (category, type, name, lang)
""" """
identities = data.get('identities', set()) identities = data.get('identities', set())
new_node = self.add_node(jid, node) self.add_node(jid, node)
new_node['info']['identities'] = identities self.get_node(jid, node)['info']['identities'] = identities
def del_identity(self, jid, node, ifrom, data): def del_identity(self, jid, node, ifrom, data):
""" """
@@ -317,8 +316,8 @@ class StaticDisco(object):
The data parameter should include: The data parameter should include:
feature -- The namespace of the supported feature. feature -- The namespace of the supported feature.
""" """
new_node = self.add_node(jid, node) self.add_node(jid, node)
new_node['info'].add_feature( self.get_node(jid, node)['info'].add_feature(
data.get('feature', '')) data.get('feature', ''))
def set_features(self, jid, node, ifrom, data): def set_features(self, jid, node, ifrom, data):
@@ -329,8 +328,8 @@ class StaticDisco(object):
features -- The new set of supported features. features -- The new set of supported features.
""" """
features = data.get('features', set()) features = data.get('features', set())
new_node = self.add_node(jid, node) self.add_node(jid, node)
new_node['info']['features'] = features self.get_node(jid, node)['info']['features'] = features
def del_feature(self, jid, node, ifrom, data): def del_feature(self, jid, node, ifrom, data):
""" """
@@ -363,8 +362,8 @@ class StaticDisco(object):
non-addressable items. non-addressable items.
name -- Optional human readable name for the item. name -- Optional human readable name for the item.
""" """
new_node = self.add_node(jid, node) self.add_node(jid, node)
new_node['items'].add_item( self.get_node(jid, node)['items'].add_item(
data.get('ijid', ''), data.get('ijid', ''),
node=data.get('inode', ''), node=data.get('inode', ''),
name=data.get('name', '')) name=data.get('name', ''))
@@ -393,8 +392,8 @@ class StaticDisco(object):
if isinstance(data, Iq): if isinstance(data, Iq):
data = data['disco_info'] data = data['disco_info']
new_node = self.add_node(jid, node, ifrom) self.add_node(jid, node, ifrom)
new_node['info'] = data self.get_node(jid, node, ifrom)['info'] = data
def get_cached_info(self, jid, node, ifrom, data): def get_cached_info(self, jid, node, ifrom, data):
""" """

View File

@@ -31,7 +31,8 @@ class IBBytestream(object):
self.recv_queue = asyncio.Queue() self.recv_queue = asyncio.Queue()
async def send(self, data, timeout=None): @asyncio.coroutine
def send(self, data, timeout=None):
if not self.stream_started or self.stream_out_closed: if not self.stream_started or self.stream_out_closed:
raise socket.error raise socket.error
if len(data) > self.block_size: if len(data) > self.block_size:
@@ -55,20 +56,22 @@ class IBBytestream(object):
iq['ibb_data']['sid'] = self.sid iq['ibb_data']['sid'] = self.sid
iq['ibb_data']['seq'] = seq iq['ibb_data']['seq'] = seq
iq['ibb_data']['data'] = data iq['ibb_data']['data'] = data
await iq.send(timeout=timeout) yield from iq.send(timeout=timeout)
return len(data) return len(data)
async def sendall(self, data, timeout=None): @asyncio.coroutine
def sendall(self, data, timeout=None):
sent_len = 0 sent_len = 0
while sent_len < len(data): while sent_len < len(data):
sent_len += await self.send(data[sent_len:self.block_size], timeout=timeout) sent_len += yield from self.send(data[sent_len:self.block_size], timeout=timeout)
async def sendfile(self, file, timeout=None): @asyncio.coroutine
def sendfile(self, file, timeout=None):
while True: while True:
data = file.read(self.block_size) data = file.read(self.block_size)
if not data: if not data:
break break
await self.send(data, timeout=timeout) yield from self.send(data, timeout=timeout)
def _recv_data(self, stanza): def _recv_data(self, stanza):
new_seq = stanza['ibb_data']['seq'] new_seq = stanza['ibb_data']['seq']

View File

@@ -611,7 +611,7 @@ class XEP_0050(BasePlugin):
def terminate_command(self, session): def terminate_command(self, session):
""" """
Delete a command's session after a command has completed Delete a command's session after a command has completed
or an error has occurred. or an error has occured.
Arguments: Arguments:
session -- All stored data relevant to the current session -- All stored data relevant to the current

View File

@@ -261,7 +261,7 @@ class BinVal(ElementBase):
def get_binval(self): def get_binval(self):
parent = self.parent() parent = self.parent()
xml = parent.xml.find('{%s}BINVAL' % self.namespace) xml = parent.find('{%s}BINVAL' % self.namespace)
if xml is not None: if xml is not None:
return base64.b64decode(bytes(xml.text)) return base64.b64decode(bytes(xml.text))
return b'' return b''

View File

@@ -123,7 +123,7 @@ class XEP_0054(BasePlugin):
if iq['type'] == 'result': if iq['type'] == 'result':
self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp']) self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp'])
return return
elif iq['type'] == 'get' and self.xmpp.is_component: elif iq['type'] == 'get':
vcard = self.api['get_vcard'](iq['from'].bare) vcard = self.api['get_vcard'](iq['from'].bare)
if isinstance(vcard, Iq): if isinstance(vcard, Iq):
vcard.send() vcard.send()

View File

@@ -19,27 +19,23 @@ from slixmpp.exceptions import XMPPError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class ResultIterator: class ResultIterator():
""" """
An iterator for Result Set Management An iterator for Result Set Managment
""" """
def __init__(self, query, interface, results='substanzas', amount=10, def __init__(self, query, interface, results='substanzas', amount=10,
start=None, reverse=False, recv_interface=None, start=None, reverse=False):
pre_cb=None, post_cb=None):
""" """
Arguments: Arguments:
query -- The template query query -- The template query
interface -- The substanza of the query to send, for example disco_items interface -- The substanza of the query, for example disco_items
recv_interface -- The substanza of the query to receive, for example disco_items
results -- The query stanza's interface which provides a results -- The query stanza's interface which provides a
countable list of query results. countable list of query results.
amount -- The max amounts of items to request per iteration amount -- The max amounts of items to request per iteration
start -- From which item id to start start -- From which item id to start
reverse -- If True, page backwards through the results reverse -- If True, page backwards through the results
pre_cb -- Callback to run before sending the stanza
post_cb -- Callback to run after receiving the reply
Example: Example:
q = Iq() q = Iq()
@@ -53,23 +49,17 @@ class ResultIterator:
self.amount = amount self.amount = amount
self.start = start self.start = start
self.interface = interface self.interface = interface
if recv_interface:
self.recv_interface = recv_interface
else:
self.recv_interface = interface
self.pre_cb = pre_cb
self.post_cb = post_cb
self.results = results self.results = results
self.reverse = reverse self.reverse = reverse
self._stop = False self._stop = False
def __aiter__(self): def __iter__(self):
return self return self
async def __anext__(self): def __next__(self):
return await self.next() return self.next()
async def next(self): def next(self):
""" """
Return the next page of results from a query. Return the next page of results from a query.
@@ -78,7 +68,7 @@ class ResultIterator:
of items. of items.
""" """
if self._stop: if self._stop:
raise StopAsyncIteration raise StopIteration
self.query[self.interface]['rsm']['before'] = self.reverse self.query[self.interface]['rsm']['before'] = self.reverse
self.query['id'] = self.query.stream.new_id() self.query['id'] = self.query.stream.new_id()
self.query[self.interface]['rsm']['max'] = str(self.amount) self.query[self.interface]['rsm']['max'] = str(self.amount)
@@ -89,32 +79,28 @@ class ResultIterator:
self.query[self.interface]['rsm']['after'] = self.start self.query[self.interface]['rsm']['after'] = self.start
try: try:
if self.pre_cb: r = self.query.send(block=True)
self.pre_cb(self.query)
r = await self.query.send()
if not r[self.recv_interface]['rsm']['first'] and \ if not r[self.interface]['rsm']['first'] and \
not r[self.recv_interface]['rsm']['last']: not r[self.interface]['rsm']['last']:
raise StopAsyncIteration raise StopIteration
if r[self.recv_interface]['rsm']['count'] and \ if r[self.interface]['rsm']['count'] and \
r[self.recv_interface]['rsm']['first_index']: r[self.interface]['rsm']['first_index']:
count = int(r[self.recv_interface]['rsm']['count']) count = int(r[self.interface]['rsm']['count'])
first = int(r[self.recv_interface]['rsm']['first_index']) first = int(r[self.interface]['rsm']['first_index'])
num_items = len(r[self.recv_interface][self.results]) num_items = len(r[self.interface][self.results])
if first + num_items == count: if first + num_items == count:
self._stop = True self._stop = True
if self.reverse: if self.reverse:
self.start = r[self.recv_interface]['rsm']['first'] self.start = r[self.interface]['rsm']['first']
else: else:
self.start = r[self.recv_interface]['rsm']['last'] self.start = r[self.interface]['rsm']['last']
if self.post_cb:
self.post_cb(r)
return r return r
except XMPPError: except XMPPError:
raise StopAsyncIteration raise StopIteration
class XEP_0059(BasePlugin): class XEP_0059(BasePlugin):
@@ -141,8 +127,7 @@ class XEP_0059(BasePlugin):
def session_bind(self, jid): def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Set.namespace) self.xmpp['xep_0030'].add_feature(Set.namespace)
def iterate(self, stanza, interface, results='substanzas', def iterate(self, stanza, interface, results='substanzas'):
recv_interface=None, pre_cb=None, post_cb=None):
""" """
Create a new result set iterator for a given stanza query. Create a new result set iterator for a given stanza query.
@@ -152,23 +137,9 @@ class XEP_0059(BasePlugin):
basic disco#items query. basic disco#items query.
interface -- The name of the substanza to which the interface -- The name of the substanza to which the
result set management stanza should be result set management stanza should be
appended in the query stanza. For example, appended. For example, for disco#items queries
for disco#items queries the interface the interface 'disco_items' should be used.
'disco_items' should be used.
recv_interface -- The name of the substanza from which the
result set management stanza should be
read in the result stanza. If unspecified,
it will be set to the same value as the
``interface`` parameter.
pre_cb -- Callback to run before sending each stanza e.g.
setting the MAM queryid and starting a stanza
collector.
post_cb -- Callback to run after receiving each stanza e.g.
stopping a MAM stanza collector in order to
gather results.
results -- The name of the interface containing the results -- The name of the interface containing the
query results (typically just 'substanzas'). query results (typically just 'substanzas').
""" """
return ResultIterator(stanza, interface, results, return ResultIterator(stanza, interface, results)
recv_interface=recv_interface, pre_cb=pre_cb,
post_cb=post_cb)

View File

@@ -13,7 +13,7 @@ from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
class Set(ElementBase): class Set(ElementBase):
""" """
XEP-0059 (Result Set Management) can be used to manage the XEP-0059 (Result Set Managment) can be used to manage the
results of queries. For example, limiting the number of items results of queries. For example, limiting the number of items
per response or starting at certain positions. per response or starting at certain positions.

View File

@@ -185,14 +185,14 @@ class XEP_0060(BasePlugin):
if config is not None: if config is not None:
form_type = 'http://jabber.org/protocol/pubsub#node_config' form_type = 'http://jabber.org/protocol/pubsub#node_config'
if 'FORM_TYPE' in config.get_fields(): if 'FORM_TYPE' in config['fields']:
config.field['FORM_TYPE']['value'] = form_type config.field['FORM_TYPE']['value'] = form_type
else: else:
config.add_field(var='FORM_TYPE', config.add_field(var='FORM_TYPE',
ftype='hidden', ftype='hidden',
value=form_type) value=form_type)
if ntype: if ntype:
if 'pubsub#node_type' in config.get_fields(): if 'pubsub#node_type' in config['fields']:
config.field['pubsub#node_type']['value'] = ntype config.field['pubsub#node_type']['value'] = ntype
else: else:
config.add_field(var='pubsub#node_type', value=ntype) config.add_field(var='pubsub#node_type', value=ntype)

View File

@@ -82,9 +82,9 @@ class Item(ElementBase):
self.xml.append(value) self.xml.append(value)
def get_payload(self): def get_payload(self):
children = list(self.xml) childs = list(self.xml)
if len(children) > 0: if len(childs) > 0:
return children[0] return childs[0]
def del_payload(self): def del_payload(self):
for child in self.xml: for child in self.xml:

View File

@@ -31,9 +31,9 @@ class EventItem(ElementBase):
self.xml.append(value) self.xml.append(value)
def get_payload(self): def get_payload(self):
children = list(self.xml) childs = list(self.xml)
if len(children) > 0: if len(childs) > 0:
return children[0] return childs[0]
def del_payload(self): def del_payload(self):
for child in self.xml: for child in self.xml:

View File

@@ -55,17 +55,17 @@ class XEP_0065(BasePlugin):
"""Returns the socket associated to the SID.""" """Returns the socket associated to the SID."""
return self._sessions.get(sid, None) return self._sessions.get(sid, None)
async def handshake(self, to, ifrom=None, sid=None, timeout=None): def handshake(self, to, ifrom=None, sid=None, timeout=None):
""" Starts the handshake to establish the socks5 bytestreams """ Starts the handshake to establish the socks5 bytestreams
connection. connection.
""" """
if not self._proxies: if not self._proxies:
self._proxies = await self.discover_proxies() self._proxies = yield from self.discover_proxies()
if sid is None: if sid is None:
sid = uuid4().hex sid = uuid4().hex
used = await self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout) used = yield from self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout)
proxy = used['socks']['streamhost_used']['jid'] proxy = used['socks']['streamhost_used']['jid']
if proxy not in self._proxies: if proxy not in self._proxies:
@@ -73,16 +73,16 @@ class XEP_0065(BasePlugin):
return return
try: try:
self._sessions[sid] = (await self._connect_proxy( self._sessions[sid] = (yield from self._connect_proxy(
self._get_dest_sha1(sid, self.xmpp.boundjid, to), self._get_dest_sha1(sid, self.xmpp.boundjid, to),
self._proxies[proxy][0], self._proxies[proxy][0],
self._proxies[proxy][1]))[1] self._proxies[proxy][1]))[1]
except socket.error: except socket.error:
return None return None
addr, port = await self._sessions[sid].connected addr, port = yield from self._sessions[sid].connected
# Request that the proxy activate the session with the target. # Request that the proxy activate the session with the target.
await self.activate(proxy, sid, to, timeout=timeout) yield from self.activate(proxy, sid, to, timeout=timeout)
sock = self.get_socket(sid) sock = self.get_socket(sid)
self.xmpp.event('stream:%s:%s' % (sid, to), sock) self.xmpp.event('stream:%s:%s' % (sid, to), sock)
return sock return sock
@@ -104,7 +104,7 @@ class XEP_0065(BasePlugin):
iq['socks'].add_streamhost(proxy, host, port) iq['socks'].add_streamhost(proxy, host, port)
return iq.send(timeout=timeout, callback=callback) return iq.send(timeout=timeout, callback=callback)
async def discover_proxies(self, jid=None, ifrom=None, timeout=None): def discover_proxies(self, jid=None, ifrom=None, timeout=None):
"""Auto-discover the JIDs of SOCKS5 proxies on an XMPP server.""" """Auto-discover the JIDs of SOCKS5 proxies on an XMPP server."""
if jid is None: if jid is None:
if self.xmpp.is_component: if self.xmpp.is_component:
@@ -114,7 +114,7 @@ class XEP_0065(BasePlugin):
discovered = set() discovered = set()
disco_items = await self.xmpp['xep_0030'].get_items(jid, timeout=timeout) disco_items = yield from self.xmpp['xep_0030'].get_items(jid, timeout=timeout)
disco_items = {item[0] for item in disco_items['disco_items']['items']} disco_items = {item[0] for item in disco_items['disco_items']['items']}
disco_info_futures = {} disco_info_futures = {}
@@ -123,7 +123,7 @@ class XEP_0065(BasePlugin):
for item in disco_items: for item in disco_items:
try: try:
disco_info = await disco_info_futures[item] disco_info = yield from disco_info_futures[item]
except XMPPError: except XMPPError:
continue continue
else: else:
@@ -135,7 +135,7 @@ class XEP_0065(BasePlugin):
for jid in discovered: for jid in discovered:
try: try:
addr = await self.get_network_address(jid, ifrom=ifrom, timeout=timeout) addr = yield from self.get_network_address(jid, ifrom=ifrom, timeout=timeout)
self._proxies[jid] = (addr['socks']['streamhost']['host'], self._proxies[jid] = (addr['socks']['streamhost']['host'],
addr['socks']['streamhost']['port']) addr['socks']['streamhost']['port'])
except XMPPError: except XMPPError:
@@ -180,8 +180,9 @@ class XEP_0065(BasePlugin):
streamhost['host'], streamhost['host'],
streamhost['port'])) streamhost['port']))
async def gather(futures, iq, streamhosts): @asyncio.coroutine
proxies = await asyncio.gather(*futures, return_exceptions=True) def gather(futures, iq, streamhosts):
proxies = yield from asyncio.gather(*futures, return_exceptions=True)
for streamhost, proxy in zip(streamhosts, proxies): for streamhost, proxy in zip(streamhosts, proxies):
if isinstance(proxy, ValueError): if isinstance(proxy, ValueError):
continue continue
@@ -191,7 +192,7 @@ class XEP_0065(BasePlugin):
proxy = proxy[1] proxy = proxy[1]
# TODO: what if the future never happens? # TODO: what if the future never happens?
try: try:
addr, port = await proxy.connected addr, port = yield from proxy.connected
except socket.error: except socket.error:
log.exception('Socket error while connecting to the proxy.') log.exception('Socket error while connecting to the proxy.')
continue continue
@@ -212,7 +213,7 @@ class XEP_0065(BasePlugin):
self.xmpp.event('socks5_stream', conn) self.xmpp.event('socks5_stream', conn)
self.xmpp.event('stream:%s:%s' % (sid, requester), conn) self.xmpp.event('stream:%s:%s' % (sid, requester), conn)
asyncio.ensure_future(gather(proxy_futures, iq, streamhosts)) asyncio.async(gather(proxy_futures, iq, streamhosts))
def activate(self, proxy, sid, target, ifrom=None, timeout=None, callback=None): def activate(self, proxy, sid, target, ifrom=None, timeout=None, callback=None):
"""Activate the socks5 session that has been negotiated.""" """Activate the socks5 session that has been negotiated."""
@@ -230,7 +231,7 @@ class XEP_0065(BasePlugin):
sock.close() sock.close()
except socket.error: except socket.error:
pass pass
# Though this should not be necessary remove the closed session anyway # Though this should not be neccessary remove the closed session anyway
if sid in self._sessions: if sid in self._sessions:
log.warn(('SOCKS5 session with sid = "%s" was not ' + log.warn(('SOCKS5 session with sid = "%s" was not ' +
'removed from _sessions by sock.close()') % sid) 'removed from _sessions by sock.close()') % sid)

View File

@@ -137,8 +137,8 @@ class Socks5Protocol(asyncio.Protocol):
def resume_writing(self): def resume_writing(self):
self.paused.set_result(None) self.paused.set_result(None)
async def write(self, data): def write(self, data):
await self.paused yield from self.paused
self.transport.write(data) self.transport.write(data)
def _send_methods(self): def _send_methods(self):

View File

@@ -8,7 +8,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from slixmpp import JID
from slixmpp.xmlstream import ElementBase, register_stanza_plugin from slixmpp.xmlstream import ElementBase, register_stanza_plugin

View File

@@ -65,14 +65,9 @@ class XEP_0092(BasePlugin):
iq -- The Iq stanza containing the software version query. iq -- The Iq stanza containing the software version query.
""" """
iq = iq.reply() iq = iq.reply()
if self.software_name: iq['software_version']['name'] = self.software_name
iq['software_version']['name'] = self.software_name iq['software_version']['version'] = self.version
iq['software_version']['version'] = self.version iq['software_version']['os'] = self.os
iq['software_version']['os'] = self.os
else:
iq.error()
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'service-unavailable'
iq.send() iq.send()
def get_version(self, jid, ifrom=None, timeout=None, callback=None, def get_version(self, jid, ifrom=None, timeout=None, callback=None,

View File

@@ -97,7 +97,7 @@ class XEP_0095(BasePlugin):
extension='bad-profile', extension='bad-profile',
extension_ns=SI.namespace) extension_ns=SI.namespace)
neg = iq['si']['feature_neg']['form'].get_fields() neg = iq['si']['feature_neg']['form']['fields']
options = neg['stream-method']['options'] or [] options = neg['stream-method']['options'] or []
methods = [] methods = []
for opt in options: for opt in options:

View File

@@ -15,7 +15,6 @@ from slixmpp.stanza import StreamFeatures, Presence, Iq
from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.xmlstream import register_stanza_plugin, JID
from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.util import MemoryCache
from slixmpp import asyncio from slixmpp import asyncio
from slixmpp.exceptions import XMPPError, IqError, IqTimeout from slixmpp.exceptions import XMPPError, IqError, IqTimeout
from slixmpp.plugins import BasePlugin from slixmpp.plugins import BasePlugin
@@ -28,7 +27,7 @@ log = logging.getLogger(__name__)
class XEP_0115(BasePlugin): class XEP_0115(BasePlugin):
""" """
XEP-0115: Entity Capabilities XEP-0115: Entity Capabalities
""" """
name = 'xep_0115' name = 'xep_0115'
@@ -38,8 +37,7 @@ class XEP_0115(BasePlugin):
default_config = { default_config = {
'hash': 'sha-1', 'hash': 'sha-1',
'caps_node': None, 'caps_node': None,
'broadcast': True, 'broadcast': True
'cache': None,
} }
def plugin_init(self): def plugin_init(self):
@@ -50,9 +48,6 @@ class XEP_0115(BasePlugin):
if self.caps_node is None: if self.caps_node is None:
self.caps_node = 'http://slixmpp.com/ver/%s' % __version__ self.caps_node = 'http://slixmpp.com/ver/%s' % __version__
if self.cache is None:
self.cache = MemoryCache()
register_stanza_plugin(Presence, stanza.Capabilities) register_stanza_plugin(Presence, stanza.Capabilities)
register_stanza_plugin(StreamFeatures, stanza.Capabilities) register_stanza_plugin(StreamFeatures, stanza.Capabilities)
@@ -137,7 +132,8 @@ class XEP_0115(BasePlugin):
self.xmpp.event('entity_caps', p) self.xmpp.event('entity_caps', p)
async def _process_caps(self, pres): @asyncio.coroutine
def _process_caps(self, pres):
if not pres['caps']['hash']: if not pres['caps']['hash']:
log.debug("Received unsupported legacy caps: %s, %s, %s", log.debug("Received unsupported legacy caps: %s, %s, %s",
pres['caps']['node'], pres['caps']['node'],
@@ -168,7 +164,7 @@ class XEP_0115(BasePlugin):
log.debug("New caps verification string: %s", ver) log.debug("New caps verification string: %s", ver)
try: try:
node = '%s#%s' % (pres['caps']['node'], ver) node = '%s#%s' % (pres['caps']['node'], ver)
caps = await self.xmpp['xep_0030'].get_info(pres['from'], node, caps = yield from self.xmpp['xep_0030'].get_info(pres['from'], node,
coroutine=True) coroutine=True)
if isinstance(caps, Iq): if isinstance(caps, Iq):
@@ -203,8 +199,8 @@ class XEP_0115(BasePlugin):
log.debug("Non form extension found, ignoring for caps") log.debug("Non form extension found, ignoring for caps")
caps.xml.remove(stanza.xml) caps.xml.remove(stanza.xml)
continue continue
if 'FORM_TYPE' in stanza.get_fields(): if 'FORM_TYPE' in stanza['fields']:
f_type = tuple(stanza.get_fields()['FORM_TYPE']['value']) f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
form_types.append(f_type) form_types.append(f_type)
deduped_form_types.add(f_type) deduped_form_types.add(f_type)
if len(form_types) != len(deduped_form_types): if len(form_types) != len(deduped_form_types):
@@ -218,7 +214,7 @@ class XEP_0115(BasePlugin):
log.debug("Extra FORM_TYPE data, invalid for caps") log.debug("Extra FORM_TYPE data, invalid for caps")
return False return False
if stanza.get_fields()['FORM_TYPE']['type'] != 'hidden': if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
log.debug("Field FORM_TYPE type not 'hidden', " + \ log.debug("Field FORM_TYPE type not 'hidden', " + \
"ignoring form for caps") "ignoring form for caps")
caps.xml.remove(stanza.xml) caps.xml.remove(stanza.xml)
@@ -257,7 +253,7 @@ class XEP_0115(BasePlugin):
for stanza in info['substanzas']: for stanza in info['substanzas']:
if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
if 'FORM_TYPE' in stanza.get_fields(): if 'FORM_TYPE' in stanza['fields']:
f_type = stanza['values']['FORM_TYPE'] f_type = stanza['values']['FORM_TYPE']
if len(f_type): if len(f_type):
f_type = f_type[0] f_type = f_type[0]
@@ -269,11 +265,11 @@ class XEP_0115(BasePlugin):
for f_type in sorted_forms: for f_type in sorted_forms:
for form in form_types[f_type]: for form in form_types[f_type]:
S += '%s<' % f_type S += '%s<' % f_type
fields = sorted(form.get_fields().keys()) fields = sorted(form['fields'].keys())
fields.remove('FORM_TYPE') fields.remove('FORM_TYPE')
for field in fields: for field in fields:
S += '%s<' % field S += '%s<' % field
vals = form.get_fields()[field].get_value(convert=False) vals = form['fields'][field].get_value(convert=False)
if vals is None: if vals is None:
S += '<' S += '<'
else: else:
@@ -284,9 +280,10 @@ class XEP_0115(BasePlugin):
binary = hash(S.encode('utf8')).digest() binary = hash(S.encode('utf8')).digest()
return base64.b64encode(binary).decode('utf-8') return base64.b64encode(binary).decode('utf-8')
async def update_caps(self, jid=None, node=None, preserve=False): @asyncio.coroutine
def update_caps(self, jid=None, node=None, preserve=False):
try: try:
info = await self.xmpp['xep_0030'].get_info(jid, node, local=True) info = yield from self.xmpp['xep_0030'].get_info(jid, node, local=True)
if isinstance(info, Iq): if isinstance(info, Iq):
info = info['disco_info'] info = info['disco_info']
ver = self.generate_verstring(info, self.hash) ver = self.generate_verstring(info, self.hash)

View File

@@ -33,6 +33,7 @@ class StaticCaps(object):
self.disco = self.xmpp['xep_0030'] self.disco = self.xmpp['xep_0030']
self.caps = self.xmpp['xep_0115'] self.caps = self.xmpp['xep_0115']
self.static = static self.static = static
self.ver_cache = {}
self.jid_vers = {} self.jid_vers = {}
def supports(self, jid, node, ifrom, data): def supports(self, jid, node, ifrom, data):
@@ -127,7 +128,7 @@ class StaticCaps(object):
info = data.get('info', None) info = data.get('info', None)
if not verstring or not info: if not verstring or not info:
return return
self.caps.cache.store(verstring, info) self.ver_cache[verstring] = info
def assign_verstring(self, jid, node, ifrom, data): def assign_verstring(self, jid, node, ifrom, data):
if isinstance(jid, JID): if isinstance(jid, JID):
@@ -138,7 +139,4 @@ class StaticCaps(object):
return self.jid_vers.get(jid, None) return self.jid_vers.get(jid, None)
def get_caps(self, jid, node, ifrom, data): def get_caps(self, jid, node, ifrom, data):
verstring = data.get('verstring', None) return self.ver_cache.get(data.get('verstring', None), None)
if verstring is None:
return None
return self.caps.cache.retrieve(verstring)

View File

@@ -98,9 +98,10 @@ class XEP_0153(BasePlugin):
first_future.add_done_callback(propagate_timeout_exception) first_future.add_done_callback(propagate_timeout_exception)
return future return future
async def _start(self, event): @asyncio.coroutine
def _start(self, event):
try: try:
vcard = await self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare) vcard = yield from self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare)
data = vcard['vcard_temp']['PHOTO']['BINVAL'] data = vcard['vcard_temp']['PHOTO']['BINVAL']
if not data: if not data:
new_hash = '' new_hash = ''
@@ -137,11 +138,7 @@ class XEP_0153(BasePlugin):
if iq['type'] == 'error': if iq['type'] == 'error':
log.debug('Could not retrieve vCard for %s', jid) log.debug('Could not retrieve vCard for %s', jid)
return return
try: data = iq['vcard_temp']['PHOTO']['BINVAL']
data = iq['vcard_temp']['PHOTO']['BINVAL']
except ValueError:
log.debug('Invalid BINVAL in vCards PHOTO for %s:', jid, exc_info=True)
data = None
if not data: if not data:
new_hash = '' new_hash = ''
else: else:

View File

@@ -62,7 +62,7 @@ class XEP_0163(BasePlugin):
for ns in namespace: for ns in namespace:
self.xmpp['xep_0030'].add_feature('%s+notify' % ns, self.xmpp['xep_0030'].add_feature('%s+notify' % ns,
jid=jid) jid=jid)
asyncio.ensure_future(self.xmpp['xep_0115'].update_caps(jid)) asyncio.async(self.xmpp['xep_0115'].update_caps(jid))
def remove_interest(self, namespace, jid=None): def remove_interest(self, namespace, jid=None):
""" """
@@ -81,7 +81,7 @@ class XEP_0163(BasePlugin):
for ns in namespace: for ns in namespace:
self.xmpp['xep_0030'].del_feature(jid=jid, self.xmpp['xep_0030'].del_feature(jid=jid,
feature='%s+notify' % namespace) feature='%s+notify' % namespace)
asyncio.ensure_future(self.xmpp['xep_0115'].update_caps(jid)) asyncio.async(self.xmpp['xep_0115'].update_caps(jid))
def publish(self, stanza, node=None, id=None, options=None, ifrom=None, def publish(self, stanza, node=None, id=None, options=None, ifrom=None,
timeout_callback=None, callback=None, timeout=None): timeout_callback=None, callback=None, timeout=None):

View File

@@ -174,7 +174,8 @@ class XEP_0198(BasePlugin):
req = stanza.RequestAck(self.xmpp) req = stanza.RequestAck(self.xmpp)
self.xmpp.send_raw(str(req)) self.xmpp.send_raw(str(req))
async def _handle_sm_feature(self, features): @asyncio.coroutine
def _handle_sm_feature(self, features):
""" """
Enable or resume stream management. Enable or resume stream management.
@@ -202,7 +203,7 @@ class XEP_0198(BasePlugin):
MatchXPath(stanza.Enabled.tag_name()), MatchXPath(stanza.Enabled.tag_name()),
MatchXPath(stanza.Failed.tag_name())])) MatchXPath(stanza.Failed.tag_name())]))
self.xmpp.register_handler(waiter) self.xmpp.register_handler(waiter)
result = await waiter.wait() result = yield from waiter.wait()
elif self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features: elif self.sm_id and self.allow_resume and 'bind' not in self.xmpp.features:
self.enabled = True self.enabled = True
resume = stanza.Resume(self.xmpp) resume = stanza.Resume(self.xmpp)
@@ -218,7 +219,7 @@ class XEP_0198(BasePlugin):
MatchXPath(stanza.Resumed.tag_name()), MatchXPath(stanza.Resumed.tag_name()),
MatchXPath(stanza.Failed.tag_name())])) MatchXPath(stanza.Failed.tag_name())]))
self.xmpp.register_handler(waiter) self.xmpp.register_handler(waiter)
result = await waiter.wait() result = yield from waiter.wait()
if result is not None and result.name == 'resumed': if result is not None and result.name == 'resumed':
return True return True
return False return False

View File

@@ -104,12 +104,13 @@ class XEP_0199(BasePlugin):
def disable_keepalive(self, event=None): def disable_keepalive(self, event=None):
self.xmpp.cancel_schedule('Ping keepalive') self.xmpp.cancel_schedule('Ping keepalive')
async def _keepalive(self, event=None): @asyncio.coroutine
def _keepalive(self, event=None):
log.debug("Keepalive ping...") log.debug("Keepalive ping...")
try: try:
rtt = await self.ping(self.xmpp.boundjid.host, timeout=self.timeout) rtt = yield from self.ping(self.xmpp.boundjid.host, timeout=self.timeout)
except IqTimeout: except IqTimeout:
log.debug("Did not receive ping back in time." + \ log.debug("Did not recieve ping back in time." + \
"Requesting Reconnect.") "Requesting Reconnect.")
self.xmpp.reconnect() self.xmpp.reconnect()
else: else:
@@ -144,7 +145,8 @@ class XEP_0199(BasePlugin):
return iq.send(timeout=timeout, callback=callback, return iq.send(timeout=timeout, callback=callback,
timeout_callback=timeout_callback) timeout_callback=timeout_callback)
async def ping(self, jid=None, ifrom=None, timeout=None): @asyncio.coroutine
def ping(self, jid=None, ifrom=None, timeout=None):
"""Send a ping request and calculate RTT. """Send a ping request and calculate RTT.
This is a coroutine. This is a coroutine.
@@ -172,7 +174,7 @@ class XEP_0199(BasePlugin):
log.debug('Pinging %s' % jid) log.debug('Pinging %s' % jid)
try: try:
await self.send_ping(jid, ifrom=ifrom, timeout=timeout) yield from self.send_ping(jid, ifrom=ifrom, timeout=timeout)
except IqError as e: except IqError as e:
if own_host: if own_host:
rtt = time.time() - start rtt = time.time() - start

View File

@@ -36,7 +36,7 @@ class URI(ElementBase):
self.xml.text = value self.xml.text = value
def del_value(self): def del_value(self):
self.xml.text = '' sel.xml.text = ''
register_stanza_plugin(Media, URI, iterable=True) register_stanza_plugin(Media, URI, iterable=True)

View File

@@ -28,7 +28,7 @@ class XEP_0222(BasePlugin):
profile = {'pubsub#persist_items': True, profile = {'pubsub#persist_items': True,
'pubsub#send_last_published_item': 'never'} 'pubsub#send_last_published_item': 'never'}
def configure(self, node, ifrom=None, callback=None, timeout=None): def configure(self, node):
""" """
Update a node's configuration to match the public storage profile. Update a node's configuration to match the public storage profile.
""" """
@@ -73,11 +73,11 @@ class XEP_0222(BasePlugin):
ftype='hidden', ftype='hidden',
value='http://jabber.org/protocol/pubsub#publish-options') value='http://jabber.org/protocol/pubsub#publish-options')
fields = options.get_fields() fields = options['fields']
for field, value in self.profile.items(): for field, value in self.profile.items():
if field not in fields: if field not in fields:
options.add_field(var=field) options.add_field(var=field)
options.get_fields()[field]['value'] = value options['fields'][field]['value'] = value
return self.xmpp['xep_0163'].publish(stanza, node, return self.xmpp['xep_0163'].publish(stanza, node,
options=options, options=options,

View File

@@ -28,7 +28,7 @@ class XEP_0223(BasePlugin):
profile = {'pubsub#persist_items': True, profile = {'pubsub#persist_items': True,
'pubsub#send_last_published_item': 'never'} 'pubsub#send_last_published_item': 'never'}
def configure(self, node, ifrom=None, callback=None, timeout=None): def configure(self, node):
""" """
Update a node's configuration to match the public storage profile. Update a node's configuration to match the public storage profile.
""" """
@@ -78,7 +78,7 @@ class XEP_0223(BasePlugin):
for field, value in self.profile.items(): for field, value in self.profile.items():
if field not in fields: if field not in fields:
options.add_field(var=field) options.add_field(var=field)
options.get_fields()[field]['value'] = value options['fields'][field]['value'] = value
return self.xmpp['xep_0163'].publish(stanza, node, options=options, return self.xmpp['xep_0163'].publish(stanza, node, options=options,
ifrom=ifrom, callback=callback, ifrom=ifrom, callback=callback,

View File

@@ -8,7 +8,6 @@
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from slixmpp import JID
from slixmpp.util import bytes from slixmpp.util import bytes
from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin

View File

@@ -61,12 +61,10 @@ class XEP_0280(BasePlugin):
self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:carbons:2') self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:carbons:2')
def _handle_carbon_received(self, msg): def _handle_carbon_received(self, msg):
if msg['from'].bare == self.xmpp.boundjid.bare: self.xmpp.event('carbon_received', msg)
self.xmpp.event('carbon_received', msg)
def _handle_carbon_sent(self, msg): def _handle_carbon_sent(self, msg):
if msg['from'].bare == self.xmpp.boundjid.bare: self.xmpp.event('carbon_sent', msg)
self.xmpp.event('carbon_sent', msg)
def enable(self, ifrom=None, timeout=None, callback=None, def enable(self, ifrom=None, timeout=None, callback=None,
timeout_callback=None): timeout_callback=None):

View File

@@ -1,16 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2017 Emmanuel Gil Peyrot
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0300 import stanza
from slixmpp.plugins.xep_0300.stanza import Hash
from slixmpp.plugins.xep_0300.hash import XEP_0300
register_plugin(XEP_0300)

View File

@@ -1,87 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2017 Emmanuel Gil Peyrot
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from base64 import b64encode
import hashlib
import logging
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0300 import stanza, Hash
log = logging.getLogger(__name__)
class XEP_0300(BasePlugin):
name = 'xep_0300'
description = 'XEP-0300: Use of Cryptographic Hash Functions in XMPP'
dependencies = {'xep_0030'}
stanza = stanza
default_config = {
'block_size': 1024 * 1024, # One MiB
'preferded': 'sha-256',
'enable_sha-1': False,
'enable_sha-256': True,
'enable_sha-512': True,
'enable_sha3-256': True,
'enable_sha3-512': True,
'enable_BLAKE2b256': True,
'enable_BLAKE2b512': True,
}
_hashlib_function = {
'sha-1': hashlib.sha1,
'sha-256': hashlib.sha256,
'sha-512': hashlib.sha512,
'sha3-256': lambda: hashlib.sha3_256(),
'sha3-512': lambda: hashlib.sha3_512(),
'BLAKE2b256': lambda: hashlib.blake2b(digest_size=32),
'BLAKE2b512': lambda: hashlib.blake2b(digest_size=64),
}
def plugin_init(self):
namespace = 'urn:xmpp:hash-function-text-names:%s'
self.enabled_hashes = []
for algo in self._hashlib_function:
if getattr(self, 'enable_' + algo, False):
# XXX: this is a hack for Python 3.5 or below, which
# dont support sha3 or blake2b…
try:
self._hashlib_function[algo]()
except AttributeError:
log.warn('Algorithm %s unavailable, disabling.', algo)
else:
self.enabled_hashes.append(namespace % algo)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Hash.namespace)
for namespace in self.enabled_hashes:
self.xmpp['xep_0030'].add_feature(namespace)
def plugin_end(self):
for namespace in self.enabled_hashes:
self.xmpp['xep_0030'].del_feature(namespace)
self.xmpp['xep_0030'].del_feature(feature=Hash.namespace)
def compute_hash(self, filename, function=None):
if function is None:
function = self.preferred
h = self._hashlib_function[function]()
with open(filename, 'rb') as f:
while True:
block = f.read(self.block_size)
if not block:
break
h.update(block)
hash_elem = Hash()
hash_elem['algo'] = function
hash_elem['value'] = b64encode(h.digest())
return hash_elem

View File

@@ -1,35 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2017 Emmanuel Gil Peyrot
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class Hash(ElementBase):
name = 'hash'
namespace = 'urn:xmpp:hashes:2'
plugin_attrib = 'hash'
interfaces = {'algo', 'value'}
allowed_algos = ['sha-1', 'sha-256', 'sha-512', 'sha3-256', 'sha3-512', 'BLAKE2b256', 'BLAKE2b512']
def set_algo(self, value):
if value in self.allowed_algos:
self._set_attr('algo', value)
elif value in [None, '']:
self._del_attr('algo')
else:
raise ValueError('Invalid algo: %s' % value)
def get_value(self):
return self.xml.text
def set_value(self, value):
self.xml.text = value
def del_value(self):
self.xml.text = ''

View File

@@ -36,58 +36,35 @@ class XEP_0313(BasePlugin):
register_stanza_plugin(Iq, stanza.MAM) register_stanza_plugin(Iq, stanza.MAM)
register_stanza_plugin(Iq, stanza.Preferences) register_stanza_plugin(Iq, stanza.Preferences)
register_stanza_plugin(Message, stanza.Result) register_stanza_plugin(Message, stanza.Result)
register_stanza_plugin(Iq, stanza.Fin) register_stanza_plugin(Message, stanza.Archived, iterable=True)
register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded) register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded)
register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set) register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set)
register_stanza_plugin(stanza.Fin, self.xmpp['xep_0059'].stanza.Set)
def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None, def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None,
timeout=None, callback=None, iterator=False, rsm=None): timeout=None, callback=None, iterator=False):
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
query_id = iq['id'] query_id = iq['id']
iq['to'] = jid iq['to'] = jid
iq['from'] = ifrom iq['from'] = ifrom
iq['type'] = 'set' iq['type'] = 'get'
iq['mam']['queryid'] = query_id iq['mam']['queryid'] = query_id
iq['mam']['start'] = start iq['mam']['start'] = start
iq['mam']['end'] = end iq['mam']['end'] = end
iq['mam']['with'] = with_jid iq['mam']['with'] = with_jid
if rsm:
for key, value in rsm.items():
iq['mam']['rsm'][key] = str(value)
cb_data = {}
def pre_cb(query):
query['mam']['queryid'] = query['id']
collector = Collector(
'MAM_Results_%s' % query_id,
StanzaPath('message/mam_result@queryid=%s' % query['id']))
self.xmpp.register_handler(collector)
cb_data['collector'] = collector
def post_cb(result):
results = cb_data['collector'].stop()
if result['type'] == 'result':
result['mam']['results'] = results
if iterator:
return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results',
recv_interface='mam_fin',
pre_cb=pre_cb, post_cb=post_cb)
collector = Collector( collector = Collector(
'MAM_Results_%s' % query_id, 'MAM_Results_%s' % query_id,
StanzaPath('message/mam_result@queryid=%s' % query_id)) StanzaPath('message/mam_result@queryid=%s' % query_id))
self.xmpp.register_handler(collector) self.xmpp.register_handler(collector)
if iterator:
return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results')
def wrapped_cb(iq): def wrapped_cb(iq):
results = collector.stop() results = collector.stop()
if iq['type'] == 'result': if iq['type'] == 'result':
iq['mam']['results'] = results iq['mam']['results'] = results
if callback: callback(iq)
callback(iq)
return iq.send(timeout=timeout, callback=wrapped_cb) return iq.send(timeout=timeout, callback=wrapped_cb)
def set_preferences(self, jid=None, default=None, always=None, never=None, def set_preferences(self, jid=None, default=None, always=None, never=None,

View File

@@ -10,76 +10,44 @@ import datetime as dt
from slixmpp.jid import JID from slixmpp.jid import JID
from slixmpp.xmlstream import ElementBase, ET from slixmpp.xmlstream import ElementBase, ET
from slixmpp.plugins import xep_0082, xep_0004 from slixmpp.plugins import xep_0082
class MAM(ElementBase): class MAM(ElementBase):
name = 'query' name = 'query'
namespace = 'urn:xmpp:mam:2' namespace = 'urn:xmpp:mam:tmp'
plugin_attrib = 'mam' plugin_attrib = 'mam'
interfaces = {'queryid', 'start', 'end', 'with', 'results'} interfaces = {'queryid', 'start', 'end', 'with', 'results'}
sub_interfaces = {'start', 'end', 'with'} sub_interfaces = {'start', 'end', 'with'}
def setup(self, xml=None): def setup(self, xml=None):
ElementBase.setup(self, xml) ElementBase.setup(self, xml)
self._form = xep_0004.stanza.Form()
self._form['type'] = 'submit'
field = self._form.add_field(var='FORM_TYPE', ftype='hidden',
value='urn:xmpp:mam:2')
self.append(self._form)
self._results = [] self._results = []
def __get_fields(self):
return self._form.get_fields()
def get_start(self): def get_start(self):
fields = self.__get_fields() timestamp = self._get_sub_text('start')
field = fields.get('start') return xep_0082.parse(timestamp)
if field:
return xep_0082.parse(field['value'])
def set_start(self, value): def set_start(self, value):
if isinstance(value, dt.datetime): if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value) value = xep_0082.format_datetime(value)
fields = self.__get_fields() self._set_sub_text('start', value)
field = fields.get('start')
if field:
field['value'] = value
else:
field = self._form.add_field(var='start')
field['value'] = value
def get_end(self): def get_end(self):
fields = self.__get_fields() timestamp = self._get_sub_text('end')
field = fields.get('end') return xep_0082.parse(timestamp)
if field:
return xep_0082.parse(field['value'])
def set_end(self, value): def set_end(self, value):
if isinstance(value, dt.datetime): if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value) value = xep_0082.format_datetime(value)
fields = self.__get_fields() self._set_sub_text('end', value)
field = fields.get('end')
if field:
field['value'] = value
else:
field = self._form.add_field(var='end')
field['value'] = value
def get_with(self): def get_with(self):
fields = self.__get_fields() return JID(self._get_sub_text('with'))
field = fields.get('with')
if field:
return JID(field['value'])
def set_with(self, value): def set_with(self, value):
fields = self.__get_fields() self._set_sub_text('with', str(value))
field = fields.get('with')
if field:
field['with'] = str(value)
else:
field = self._form.add_field(var='with')
field['value'] = str(value)
# The results interface is meant only as an easy # The results interface is meant only as an easy
# way to access the set of collected message responses # way to access the set of collected message responses
# from the query. # from the query.
@@ -96,7 +64,7 @@ class MAM(ElementBase):
class Preferences(ElementBase): class Preferences(ElementBase):
name = 'prefs' name = 'prefs'
namespace = 'urn:xmpp:mam:2' namespace = 'urn:xmpp:mam:tmp'
plugin_attrib = 'mam_prefs' plugin_attrib = 'mam_prefs'
interfaces = {'default', 'always', 'never'} interfaces = {'default', 'always', 'never'}
sub_interfaces = {'always', 'never'} sub_interfaces = {'always', 'never'}
@@ -150,13 +118,22 @@ class Preferences(ElementBase):
never.append(jid_xml) never.append(jid_xml)
class Fin(ElementBase):
name = 'fin'
namespace = 'urn:xmpp:mam:2'
plugin_attrib = 'mam_fin'
class Result(ElementBase): class Result(ElementBase):
name = 'result' name = 'result'
namespace = 'urn:xmpp:mam:2' namespace = 'urn:xmpp:mam:tmp'
plugin_attrib = 'mam_result' plugin_attrib = 'mam_result'
interfaces = {'queryid', 'id'} interfaces = {'queryid', 'id'}
class Archived(ElementBase):
name = 'archived'
namespace = 'urn:xmpp:mam:tmp'
plugin_attrib = 'mam_archived'
plugin_multi_attrib = 'mam_archives'
interfaces = {'by', 'id'}
def get_by(self):
return JID(self._get_attr('by'))
def set_by(self):
return self._set_attr('by', str(value))

View File

@@ -6,7 +6,7 @@
See the file LICENSE for copying permission. See the file LICENSE for copying permission.
""" """
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from slixmpp.stanza import Presence from slixmpp.stanza import Presence
from slixmpp.plugins import BasePlugin from slixmpp.plugins import BasePlugin
@@ -16,10 +16,6 @@ from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins.xep_0319 import stanza from slixmpp.plugins.xep_0319 import stanza
def get_local_timezone():
return datetime.now(timezone.utc).astimezone().tzinfo
class XEP_0319(BasePlugin): class XEP_0319(BasePlugin):
name = 'xep_0319' name = 'xep_0319'
description = 'XEP-0319: Last User Interaction in Presence' description = 'XEP-0319: Last User Interaction in Presence'
@@ -51,11 +47,10 @@ class XEP_0319(BasePlugin):
def idle(self, jid=None, since=None): def idle(self, jid=None, since=None):
seconds = None seconds = None
timezone = get_local_timezone()
if since is None: if since is None:
since = datetime.now(timezone) since = datetime.now()
else: else:
seconds = datetime.now(timezone) - since seconds = datetime.now() - since
self.api['set_idle'](jid, None, None, since) self.api['set_idle'](jid, None, None, since)
self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds) self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds)

View File

@@ -291,7 +291,7 @@ class XEP_0323(BasePlugin):
request_delay_sec = dtdiff.seconds + dtdiff.days * 24 * 3600 request_delay_sec = dtdiff.seconds + dtdiff.days * 24 * 3600
if request_delay_sec <= 0: if request_delay_sec <= 0:
req_ok = False req_ok = False
error_msg = "Invalid datetime in 'when' flag, cannot set a time in the past (%s). Current time: %s" % (dt.isoformat(), dtnow.isoformat()) error_msg = "Invalid datetime in 'when' flag, cannot set a time in the past. Current time: " + dtnow.isoformat()
if req_ok: if req_ok:
session = self._new_session() session = self._new_session()

View File

@@ -10,7 +10,7 @@
from slixmpp import Iq, Message from slixmpp import Iq, Message
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
import re from re import match
class Sensordata(ElementBase): class Sensordata(ElementBase):
""" Placeholder for the namespace, not used as a stanza """ """ Placeholder for the namespace, not used as a stanza """

View File

@@ -399,7 +399,7 @@ class XEP_0325(BasePlugin):
""" """
if not session in self.sessions: if not session in self.sessions:
# This can happen if a session was deleted, like in a timeout. Just drop the data. # This can happend if a session was deleted, like in a timeout. Just drop the data.
return return
if result == "error": if result == "error":
@@ -457,7 +457,7 @@ class XEP_0325(BasePlugin):
Arguments: Arguments:
from_jid -- The jid of the requester from_jid -- The jid of the requester
to_jid -- The jid of the device(s) to_jid -- The jid of the device(s)
callback -- The callback function to call when data is available. callback -- The callback function to call when data is availble.
The callback function must support the following arguments: The callback function must support the following arguments:

View File

@@ -11,8 +11,6 @@ import logging
from slixmpp import Message from slixmpp import Message
from slixmpp.plugins import BasePlugin from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins.xep_0333 import stanza, Markable, Received, Displayed, Acknowledged from slixmpp.plugins.xep_0333 import stanza, Markable, Received, Displayed, Acknowledged
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View File

@@ -1,14 +0,0 @@
"""
slixmpp: The Slick XMPP Library
Copyright (C) 2018 Emmanuel Gil Peyrot
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0363.stanza import Request, Slot, Put, Get, Header
from slixmpp.plugins.xep_0363.http_upload import XEP_0363
register_plugin(XEP_0363)

View File

@@ -1,146 +0,0 @@
"""
slixmpp: The Slick XMPP Library
Copyright (C) 2018 Emmanuel Gil Peyrot
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
import asyncio
import logging
import os.path
from aiohttp import ClientSession
from mimetypes import guess_type
from slixmpp import Iq, __version__
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins.xep_0363 import stanza, Request, Slot, Put, Get, Header
log = logging.getLogger(__name__)
class FileUploadError(Exception):
pass
class UploadServiceNotFound(FileUploadError):
pass
class FileTooBig(FileUploadError):
pass
class XEP_0363(BasePlugin):
''' This plugin only supports Python 3.5+ '''
name = 'xep_0363'
description = 'XEP-0363: HTTP File Upload'
dependencies = {'xep_0030', 'xep_0128'}
stanza = stanza
default_config = {
'upload_service': None,
'max_file_size': float('+inf'),
'default_content_type': 'application/octet-stream',
}
def plugin_init(self):
register_stanza_plugin(Iq, Request)
register_stanza_plugin(Iq, Slot)
register_stanza_plugin(Slot, Put)
register_stanza_plugin(Slot, Get)
register_stanza_plugin(Put, Header, iterable=True)
self.xmpp.register_handler(
Callback('HTTP Upload Request',
StanzaPath('iq@type=get/http_upload_request'),
self._handle_request))
def plugin_end(self):
self._http_session.close()
self.xmpp.remove_handler('HTTP Upload Request')
self.xmpp.remove_handler('HTTP Upload Slot')
self.xmpp['xep_0030'].del_feature(feature=Request.namespace)
def session_bind(self, jid):
self.xmpp.plugin['xep_0030'].add_feature(Request.namespace)
def _handle_request(self, iq):
self.xmpp.event('http_upload_request', iq)
async def find_upload_service(self, timeout=None):
results = await self.xmpp['xep_0030'].get_info_from_domain()
for info in results:
for identity in info['disco_info']['identities']:
if identity[0] == 'store' and identity[1] == 'file':
return info
def request_slot(self, jid, filename, size, content_type=None, ifrom=None,
timeout=None, callback=None, timeout_callback=None):
iq = self.xmpp.Iq()
iq['to'] = jid
iq['from'] = ifrom
iq['type'] = 'get'
request = iq['http_upload_request']
request['filename'] = filename
request['size'] = str(size)
request['content-type'] = content_type or self.default_content_type
return iq.send(timeout=timeout, callback=callback,
timeout_callback=timeout_callback)
async def upload_file(self, filename, size=None, content_type=None, *,
input_file=None, ifrom=None, timeout=None,
callback=None, timeout_callback=None):
''' Helper function which does all of the uploading process. '''
if self.upload_service is None:
info_iq = await self.find_upload_service(timeout=timeout)
if info_iq is None:
raise UploadServiceNotFound()
self.upload_service = info_iq['from']
for form in info_iq['disco_info'].iterables:
values = form['values']
if values['FORM_TYPE'] == ['urn:xmpp:http:upload:0']:
try:
self.max_file_size = int(values['max-file-size'])
except (TypeError, ValueError):
log.error('Invalid max size received from HTTP File Upload service')
self.max_file_size = float('+inf')
break
if input_file is None:
input_file = open(filename, 'rb')
if size is None:
size = input_file.seek(0, 2)
input_file.seek(0)
if size > self.max_file_size:
raise FileTooBig()
if content_type is None:
content_type = guess_type(filename)[0]
if content_type is None:
content_type = self.default_content_type
basename = os.path.basename(filename)
slot_iq = await self.request_slot(self.upload_service, basename, size,
content_type, ifrom, timeout)
slot = slot_iq['http_upload_slot']
headers = {
'Content-Length': str(size),
'Content-Type': content_type or self.default_content_type,
**{header['name']: header['value'] for header in slot['put']['headers']}
}
# Do the actual upload here.
async with ClientSession(headers={'User-Agent': 'slixmpp ' + __version__}) as session:
response = await session.put(
slot['put']['url'],
data=input_file,
headers=headers,
timeout=timeout)
log.info('Response code: %d (%s)', response.status, await response.text())
response.close()
return slot['get']['url']

View File

@@ -1,48 +0,0 @@
"""
slixmpp: The Slick XMPP Library
Copyright (C) 2018 Emmanuel Gil Peyrot
This file is part of slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class Request(ElementBase):
plugin_attrib = 'http_upload_request'
name = 'request'
namespace = 'urn:xmpp:http:upload:0'
interfaces = {'filename', 'size', 'content-type'}
class Slot(ElementBase):
plugin_attrib = 'http_upload_slot'
name = 'slot'
namespace = 'urn:xmpp:http:upload:0'
class Put(ElementBase):
plugin_attrib = 'put'
name = 'put'
namespace = 'urn:xmpp:http:upload:0'
interfaces = {'url'}
class Get(ElementBase):
plugin_attrib = 'get'
name = 'get'
namespace = 'urn:xmpp:http:upload:0'
interfaces = {'url'}
class Header(ElementBase):
plugin_attrib = 'header'
name = 'header'
namespace = 'urn:xmpp:http:upload:0'
plugin_multi_attrib = 'headers'
interfaces = {'name', 'value'}
def get_value(self):
return self.xml.text
def set_value(self, value):
self.xml.text = value
def del_value(self):
self.xml.text = ''

View File

@@ -1,15 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0380.stanza import Encryption
from slixmpp.plugins.xep_0380.eme import XEP_0380
register_plugin(XEP_0380)

View File

@@ -1,69 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
import slixmpp
from slixmpp.stanza import Message
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0380 import stanza, Encryption
log = logging.getLogger(__name__)
class XEP_0380(BasePlugin):
"""
XEP-0380: Explicit Message Encryption
"""
name = 'xep_0380'
description = 'XEP-0380: Explicit Message Encryption'
dependencies = {'xep_0030'}
default_config = {
'template': 'This message is encrypted with {name} ({namespace})',
}
mechanisms = {
'jabber:x:encrypted': 'Legacy OpenPGP',
'urn:xmpp:ox:0': 'OpenPGP for XMPP',
'urn:xmpp:otr:0': 'OTR',
'eu.siacs.conversations.axolotl': 'Legacy OMEMO',
'urn:xmpp:omemo:0': 'OMEMO',
}
def plugin_init(self):
self.xmpp.register_handler(
Callback('Explicit Message Encryption',
StanzaPath('message/eme'),
self._handle_eme))
register_stanza_plugin(Message, Encryption)
def plugin_end(self):
self.xmpp.remove_handler('Chat State')
def session_bind(self, jid):
self.xmpp.plugin['xep_0030'].add_feature(Encryption.namespace)
def has_eme(self, msg):
return msg.xml.find('{%s}encryption' % Encryption.namespace) is not None
def replace_body_with_eme(self, msg):
eme = msg['eme']
namespace = eme['namespace']
name = self.mechanisms[namespace] if namespace in self.mechanisms else eme['name']
body = self.config['template'].format(name=name, namespace=namespace)
msg['body'] = body
def _handle_eme(self, msg):
self.xmpp.event('message_encryption', msg)

View File

@@ -1,16 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2016 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class Encryption(ElementBase):
name = 'encryption'
namespace = 'urn:xmpp:eme:0'
plugin_attrib = 'eme'
interfaces = {'namespace', 'name'}

View File

@@ -1,15 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0394.stanza import Markup, Span, BlockCode, List, Li, BlockQuote
from slixmpp.plugins.xep_0394.markup import XEP_0394
register_plugin(XEP_0394)

View File

@@ -1,161 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.stanza import Message
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin, ET, tostring
from slixmpp.plugins.xep_0394 import stanza, Markup, Span, BlockCode, List, Li, BlockQuote
from slixmpp.plugins.xep_0071 import XHTML_IM
class Start:
def __init__(self, elem):
self.elem = elem
def __repr__(self):
return 'Start(%s)' % self.elem
class End:
def __init__(self, elem):
self.elem = elem
def __repr__(self):
return 'End(%s)' % self.elem
class XEP_0394(BasePlugin):
name = 'xep_0394'
description = 'XEP-0394: Message Markup'
dependencies = {'xep_0030', 'xep_0071'}
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, Markup)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(feature=Markup.namespace)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Markup.namespace)
@staticmethod
def _split_first_level(body, markup_elem):
split_points = []
elements = {}
for markup in markup_elem['substanzas']:
start = markup['start']
end = markup['end']
split_points.append(start)
split_points.append(end)
elements.setdefault(start, []).append(Start(markup))
elements.setdefault(end, []).append(End(markup))
if isinstance(markup, List):
lis = markup['lis']
for i, li in enumerate(lis):
start = li['start']
split_points.append(start)
li_end = lis[i + 1]['start'] if i < len(lis) - 1 else end
elements.setdefault(li_end, []).append(End(li))
elements.setdefault(start, []).append(Start(li))
split_points = set(split_points)
new_body = [[]]
for i, letter in enumerate(body + '\x00'):
if i in split_points:
body_elements = []
for elem in elements[i]:
body_elements.append(elem)
new_body.append(body_elements)
new_body.append([])
new_body[-1].append(letter)
new_body[-1] = new_body[-1][:-1]
final = []
for chunk in new_body:
if not chunk:
continue
final.append(''.join(chunk) if isinstance(chunk[0], str) else chunk)
return final
def to_plain_text(self, body, markup_elem):
chunks = self._split_first_level(body, markup_elem)
final = []
for chunk in chunks:
if isinstance(chunk, str):
final.append(chunk)
return ''.join(final)
def to_xhtml_im(self, body, markup_elem):
chunks = self._split_first_level(body, markup_elem)
final = []
stack = []
for chunk in chunks:
if isinstance(chunk, str):
chunk = (chunk.replace("&", '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;')
.replace('"', '&quot;')
.replace("'", '&apos;')
.replace('\n', '<br/>'))
final.append(chunk)
continue
num_end = 0
for elem in chunk:
if isinstance(elem, End):
num_end += 1
for i in range(num_end):
stack_top = stack.pop()
for elem in chunk:
if not isinstance(elem, End):
continue
elem = elem.elem
if elem is stack_top:
if isinstance(elem, Span):
final.append('</span>')
elif isinstance(elem, BlockCode):
final.append('</code></pre>')
elif isinstance(elem, List):
final.append('</ul>')
elif isinstance(elem, Li):
final.append('</li>')
elif isinstance(elem, BlockQuote):
final.append('</blockquote>')
break
else:
assert False
for elem in chunk:
if not isinstance(elem, Start):
continue
elem = elem.elem
stack.append(elem)
if isinstance(elem, Span):
style = []
for type_ in elem['types']:
if type_ == 'emphasis':
style.append('font-style: italic;')
if type_ == 'code':
style.append('font-family: monospace;')
if type_ == 'deleted':
style.append('text-decoration: line-through;')
final.append("<span style='%s'>" % ' '.join(style))
elif isinstance(elem, BlockCode):
final.append('<pre><code>')
elif isinstance(elem, List):
final.append('<ul>')
elif isinstance(elem, Li):
final.append('<li>')
elif isinstance(elem, BlockQuote):
final.append('<blockquote>')
p = "<p xmlns='http://www.w3.org/1999/xhtml'>%s</p>" % ''.join(final)
p2 = ET.fromstring(p)
print('coucou', p, tostring(p2))
xhtml_im = XHTML_IM()
xhtml_im['body'] = p2
return xhtml_im

View File

@@ -1,123 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, register_stanza_plugin, ET
class Markup(ElementBase):
namespace = 'urn:xmpp:markup:0'
name = 'markup'
plugin_attrib = 'markup'
class _FirstLevel(ElementBase):
namespace = 'urn:xmpp:markup:0'
interfaces = {'start', 'end'}
def get_start(self):
return int(self._get_attr('start'))
def set_start(self, value):
self._set_attr('start', '%d' % value)
def get_end(self):
return int(self._get_attr('end'))
def set_end(self, value):
self._set_attr('end', '%d' % value)
class Span(_FirstLevel):
name = 'span'
plugin_attrib = 'span'
plugin_multi_attrib = 'spans'
interfaces = {'start', 'end', 'types'}
def get_types(self):
types = []
if self.xml.find('{urn:xmpp:markup:0}emphasis') is not None:
types.append('emphasis')
if self.xml.find('{urn:xmpp:markup:0}code') is not None:
types.append('code')
if self.xml.find('{urn:xmpp:markup:0}deleted') is not None:
types.append('deleted')
return types
def set_types(self, value):
del self['types']
for type_ in value:
if type_ == 'emphasis':
self.xml.append(ET.Element('{urn:xmpp:markup:0}emphasis'))
elif type_ == 'code':
self.xml.append(ET.Element('{urn:xmpp:markup:0}code'))
elif type_ == 'deleted':
self.xml.append(ET.Element('{urn:xmpp:markup:0}deleted'))
def det_types(self):
for child in self.xml:
self.xml.remove(child)
class _SpanType(ElementBase):
namespace = 'urn:xmpp:markup:0'
class EmphasisType(_SpanType):
name = 'emphasis'
plugin_attrib = 'emphasis'
class CodeType(_SpanType):
name = 'code'
plugin_attrib = 'code'
class DeletedType(_SpanType):
name = 'deleted'
plugin_attrib = 'deleted'
class BlockCode(_FirstLevel):
name = 'bcode'
plugin_attrib = 'bcode'
plugin_multi_attrib = 'bcodes'
class List(_FirstLevel):
name = 'list'
plugin_attrib = 'list'
plugin_multi_attrib = 'lists'
interfaces = {'start', 'end', 'li'}
class Li(ElementBase):
namespace = 'urn:xmpp:markup:0'
name = 'li'
plugin_attrib = 'li'
plugin_multi_attrib = 'lis'
interfaces = {'start'}
def get_start(self):
return int(self._get_attr('start'))
def set_start(self, value):
self._set_attr('start', '%d' % value)
class BlockQuote(_FirstLevel):
name = 'bquote'
plugin_attrib = 'bquote'
plugin_multi_attrib = 'bquotes'
register_stanza_plugin(Markup, Span, iterable=True)
register_stanza_plugin(Markup, BlockCode, iterable=True)
register_stanza_plugin(Markup, List, iterable=True)
register_stanza_plugin(Markup, BlockQuote, iterable=True)
register_stanza_plugin(Span, EmphasisType)
register_stanza_plugin(Span, CodeType)
register_stanza_plugin(Span, DeletedType)
register_stanza_plugin(List, Li, iterable=True)

View File

@@ -188,19 +188,10 @@ class Iq(RootStanza):
future = asyncio.Future() future = asyncio.Future()
def callback_success(result): def callback_success(result):
type_ = result['type'] if result['type'] == 'error':
if type_ == 'result':
future.set_result(result)
elif type_ == 'error':
future.set_exception(IqError(result)) future.set_exception(IqError(result))
else: else:
# Most likely an iq addressed to ourself, rearm the callback. future.set_result(result)
handler = constr(handler_name,
matcher,
callback_success,
once=True)
self.stream.register_handler(handler)
return
if timeout is not None: if timeout is not None:
self.stream.cancel_schedule('IqTimeout_%s' % self['id']) self.stream.cancel_schedule('IqTimeout_%s' % self['id'])

View File

@@ -48,7 +48,7 @@ class Message(RootStanza):
namespace = 'jabber:client' namespace = 'jabber:client'
plugin_attrib = name plugin_attrib = name
interfaces = {'type', 'to', 'from', 'id', 'body', 'subject', 'thread', interfaces = {'type', 'to', 'from', 'id', 'body', 'subject', 'thread',
'parent_thread', 'mucroom', 'mucnick'} 'parent_thread', 'mucroom', 'mucnick'])
sub_interfaces = {'body', 'subject', 'thread'} sub_interfaces = {'body', 'subject', 'thread'}
lang_interfaces = sub_interfaces lang_interfaces = sub_interfaces
types = {'normal', 'chat', 'headline', 'error', 'groupchat'} types = {'normal', 'chat', 'headline', 'error', 'groupchat'}

View File

@@ -40,7 +40,7 @@ class Roster(ElementBase):
def get_ver(self): def get_ver(self):
""" """
Ensure handling an empty ver attribute property. Ensure handling an empty ver attribute propery.
The ver attribute is special in that the presence of the The ver attribute is special in that the presence of the
attribute with an empty value is important for boostrapping attribute with an empty value is important for boostrapping
@@ -50,7 +50,7 @@ class Roster(ElementBase):
def set_ver(self, ver): def set_ver(self, ver):
""" """
Ensure handling an empty ver attribute property. Ensure handling an empty ver attribute propery.
The ver attribute is special in that the presence of the The ver attribute is special in that the presence of the
attribute with an empty value is important for boostrapping attribute with an empty value is important for boostrapping

View File

@@ -114,7 +114,7 @@ def punycode(domain):
if char in ILLEGAL_CHARS: if char in ILLEGAL_CHARS:
raise StringprepError raise StringprepError
domain_parts.append(label.encode('ascii')) domain_parts.append(label)
return b'.'.join(domain_parts) return b'.'.join(domain_parts)
logging.getLogger(__name__).warning('Using slower stringprep, consider ' logging.getLogger(__name__).warning('Using slower stringprep, consider '

View File

@@ -13,5 +13,3 @@
from slixmpp.util.misc_ops import bytes, unicode, hashes, hash, \ from slixmpp.util.misc_ops import bytes, unicode, hashes, hash, \
num_to_bytes, bytes_to_num, quote, \ num_to_bytes, bytes_to_num, quote, \
XOR XOR
from slixmpp.util.cache import MemoryCache, MemoryPerJidCache, \
FileSystemCache, FileSystemPerJidCache

View File

@@ -1,105 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2018 Emmanuel Gil Peyrot
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import os
import logging
log = logging.getLogger(__name__)
class Cache:
def retrieve(self, key):
raise NotImplementedError
def store(self, key, value):
raise NotImplementedError
class PerJidCache:
def retrieve_by_jid(self, jid, key):
raise NotImplementedError
def store_by_jid(self, jid, key, value):
raise NotImplementedError
class MemoryCache(Cache):
def __init__(self):
self.cache = {}
def retrieve(self, key):
return self.cache.get(key, None)
def store(self, key, value):
self.cache[key] = value
return True
class MemoryPerJidCache(PerJidCache):
def __init__(self):
self.cache = {}
def retrieve_by_jid(self, jid, key):
cache = self.cache.get(jid, None)
if cache is None:
return None
return cache.get(key, None)
def store_by_jid(self, jid, key, value):
cache = self.cache.setdefault(jid, {})
cache[key] = value
return True
class FileSystemStorage:
def __init__(self, encode, decode, binary):
self.encode = encode if encode is not None else lambda x: x
self.decode = decode if decode is not None else lambda x: x
self.read = 'rb' if binary else 'r'
self.write = 'wb' if binary else 'w'
def _retrieve(self, directory, key):
filename = os.path.join(directory, key.replace('/', '_'))
try:
with open(filename, self.read) as cache_file:
return self.decode(cache_file.read())
except FileNotFoundError:
log.debug('%s not present in cache', key)
except OSError:
log.debug('Failed to read %s from cache:', key, exc_info=True)
return None
def _store(self, directory, key, value):
filename = os.path.join(directory, key.replace('/', '_'))
try:
os.makedirs(directory, exist_ok=True)
with open(filename, self.write) as output:
output.write(self.encode(value))
return True
except OSError:
log.debug('Failed to store %s to cache:', key, exc_info=True)
return False
class FileSystemCache(Cache, FileSystemStorage):
def __init__(self, directory, cache_type, *, encode=None, decode=None, binary=False):
FileSystemStorage.__init__(self, encode, decode, binary)
self.base_dir = os.path.join(directory, cache_type)
def retrieve(self, key):
return self._retrieve(self.base_dir, key)
def store(self, key, value):
return self._store(self.base_dir, key, value)
class FileSystemPerJidCache(PerJidCache, FileSystemStorage):
def __init__(self, directory, cache_type, *, encode=None, decode=None, binary=False):
FileSystemStorage.__init__(self, encode, decode, binary)
self.base_dir = os.path.join(directory, cache_type)
def retrieve_by_jid(self, jid, key):
directory = os.path.join(self.base_dir, jid)
return self._retrieve(directory, key)
def store_by_jid(self, jid, key, value):
directory = os.path.join(self.base_dir, jid)
return self._store(directory, key, value)

View File

@@ -75,7 +75,7 @@ def sasl_mech(score):
MECH_SEC_SCORES[mech.name] = mech.score MECH_SEC_SCORES[mech.name] = mech.score
if mech.channel_binding: if mech.channel_binding:
MECHANISMS[mech.name + '-PLUS'] = mech MECHANISMS[mech.name + '-PLUS'] = mech
MECH_SEC_SCORES[mech.name] = mech.score + 10 MECH_SEC_SCORES[name] = mech.score + 10
return mech return mech
return register return register

View File

@@ -291,7 +291,8 @@ class SCRAM(Mech):
cbind_input = self.gs2_header + cbind_data cbind_input = self.gs2_header + cbind_data
channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'') channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'')
client_final_message_without_proof = channel_binding + b',r=' + nonce client_final_message_without_proof = channel_binding + b',' + \
b'r=' + nonce
salted_password = self.Hi(self.credentials['password'], salted_password = self.Hi(self.credentials['password'],
salt, salt,

View File

@@ -146,7 +146,6 @@ def create(nfkc=True, bidi=True, mappings=None,
if bidi: if bidi:
check_bidi(data) check_bidi(data)
if query and unassigned: if query and unassigned:
#check_unassigned(data, unassigned) check_unassigned(data, unassigned)
raise StringPrepError('Query profile with unassigned data is unimplemented.')
return data return data
return profile return profile

View File

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

View File

@@ -76,7 +76,7 @@ def extract_names(raw_cert):
name_type = name.getName() name_type = name.getName()
if name_type == 'dNSName': if name_type == 'dNSName':
results['DNS'].add(decode_str(name.getComponent())) results['DNS'].add(decode_str(name.getComponent()))
elif name_type == 'uniformResourceIdentifier': if name_type == 'uniformResourceIdentifier':
value = decode_str(name.getComponent()) value = decode_str(name.getComponent())
if value.startswith('xmpp:'): if value.startswith('xmpp:'):
results['URI'].add(value[5:]) results['URI'].add(value[5:])

View File

@@ -45,9 +45,10 @@ class CoroutineCallback(BaseHandler):
if not asyncio.iscoroutinefunction(pointer): if not asyncio.iscoroutinefunction(pointer):
raise ValueError("Given function is not a coroutine") raise ValueError("Given function is not a coroutine")
async def pointer_wrapper(stanza, *args, **kwargs): @asyncio.coroutine
def pointer_wrapper(stanza, *args, **kwargs):
try: try:
await pointer(stanza, *args, **kwargs) yield from pointer(stanza, *args, **kwargs)
except Exception as e: except Exception as e:
stanza.exception(e) stanza.exception(e)
@@ -77,7 +78,7 @@ class CoroutineCallback(BaseHandler):
:meth:`prerun()`. Defaults to ``False``. :meth:`prerun()`. Defaults to ``False``.
""" """
if not self._instream or instream: if not self._instream or instream:
asyncio.ensure_future(self._pointer(payload)) asyncio.async(self._pointer(payload))
if self._once: if self._once:
self._destroy = True self._destroy = True
del self._pointer del self._pointer

View File

@@ -50,7 +50,8 @@ class Waiter(BaseHandler):
"""Do not process this handler during the main event loop.""" """Do not process this handler during the main event loop."""
pass pass
async def wait(self, timeout=None): @asyncio.coroutine
def wait(self, timeout=None):
"""Block an event handler while waiting for a stanza to arrive. """Block an event handler while waiting for a stanza to arrive.
Be aware that this will impact performance if called from a Be aware that this will impact performance if called from a
@@ -69,7 +70,7 @@ class Waiter(BaseHandler):
stanza = None stanza = None
try: try:
stanza = await self._payload.get() stanza = yield from self._payload.get()
except TimeoutError: except TimeoutError:
log.warning("Timed out waiting for %s", self.name) log.warning("Timed out waiting for %s", self.name)
self.stream().remove_handler(self.name) self.stream().remove_handler(self.name)

View File

@@ -75,7 +75,7 @@ class MatchXMLMask(MatcherBase):
Defaults to ``"__no_ns__"``. Defaults to ``"__no_ns__"``.
""" """
if source is None: if source is None:
# If the element was not found. May happen during recursive calls. # If the element was not found. May happend during recursive calls.
return False return False
# Convert the mask to an XML object if it is a string. # Convert the mask to an XML object if it is a string.

View File

@@ -45,7 +45,8 @@ def default_resolver(loop):
return None return None
async def resolve(host, port=None, service=None, proto='tcp', @asyncio.coroutine
def resolve(host, port=None, service=None, proto='tcp',
resolver=None, use_ipv6=True, use_aiodns=True, loop=None): resolver=None, use_ipv6=True, use_aiodns=True, loop=None):
"""Peform DNS resolution for a given hostname. """Peform DNS resolution for a given hostname.
@@ -126,7 +127,7 @@ async def resolve(host, port=None, service=None, proto='tcp',
if not service: if not service:
hosts = [(host, port)] hosts = [(host, port)]
else: else:
hosts = await get_SRV(host, port, service, proto, hosts = yield from get_SRV(host, port, service, proto,
resolver=resolver, resolver=resolver,
use_aiodns=use_aiodns) use_aiodns=use_aiodns)
if not hosts: if not hosts:
@@ -140,19 +141,20 @@ async def resolve(host, port=None, service=None, proto='tcp',
results.append((host, '127.0.0.1', port)) results.append((host, '127.0.0.1', port))
if use_ipv6: if use_ipv6:
aaaa = await get_AAAA(host, resolver=resolver, aaaa = yield from get_AAAA(host, resolver=resolver,
use_aiodns=use_aiodns, loop=loop) use_aiodns=use_aiodns, loop=loop)
for address in aaaa: for address in aaaa:
results.append((host, address, port)) results.append((host, address, port))
a = await get_A(host, resolver=resolver, a = yield from get_A(host, resolver=resolver,
use_aiodns=use_aiodns, loop=loop) use_aiodns=use_aiodns, loop=loop)
for address in a: for address in a:
results.append((host, address, port)) results.append((host, address, port))
return results return results
async def get_A(host, resolver=None, use_aiodns=True, loop=None): @asyncio.coroutine
def get_A(host, resolver=None, use_aiodns=True, loop=None):
"""Lookup DNS A records for a given host. """Lookup DNS A records for a given host.
If ``resolver`` is not provided, or is ``None``, then resolution will If ``resolver`` is not provided, or is ``None``, then resolution will
@@ -176,7 +178,7 @@ async def get_A(host, resolver=None, use_aiodns=True, loop=None):
# getaddrinfo() method. # getaddrinfo() method.
if resolver is None or not use_aiodns: if resolver is None or not use_aiodns:
try: try:
recs = await loop.getaddrinfo(host, None, recs = yield from loop.getaddrinfo(host, None,
family=socket.AF_INET, family=socket.AF_INET,
type=socket.SOCK_STREAM) type=socket.SOCK_STREAM)
return [rec[4][0] for rec in recs] return [rec[4][0] for rec in recs]
@@ -187,14 +189,15 @@ async def get_A(host, resolver=None, use_aiodns=True, loop=None):
# Using aiodns: # Using aiodns:
future = resolver.query(host, 'A') future = resolver.query(host, 'A')
try: try:
recs = await future recs = yield from future
except Exception as e: except Exception as e:
log.debug('DNS: Exception while querying for %s A records: %s', host, e) log.debug('DNS: Exception while querying for %s A records: %s', host, e)
recs = [] recs = []
return [rec.host for rec in recs] return [rec.host for rec in recs]
async def get_AAAA(host, resolver=None, use_aiodns=True, loop=None): @asyncio.coroutine
def get_AAAA(host, resolver=None, use_aiodns=True, loop=None):
"""Lookup DNS AAAA records for a given host. """Lookup DNS AAAA records for a given host.
If ``resolver`` is not provided, or is ``None``, then resolution will If ``resolver`` is not provided, or is ``None``, then resolution will
@@ -221,25 +224,26 @@ async def get_AAAA(host, resolver=None, use_aiodns=True, loop=None):
log.debug("DNS: Unable to query %s for AAAA records: IPv6 is not supported", host) log.debug("DNS: Unable to query %s for AAAA records: IPv6 is not supported", host)
return [] return []
try: try:
recs = await loop.getaddrinfo(host, None, recs = yield from loop.getaddrinfo(host, None,
family=socket.AF_INET6, family=socket.AF_INET6,
type=socket.SOCK_STREAM) type=socket.SOCK_STREAM)
return [rec[4][0] for rec in recs] return [rec[4][0] for rec in recs]
except (OSError, socket.gaierror): except (OSError, socket.gaierror):
log.debug("DNS: Error retrieving AAAA address " + \ log.debug("DNS: Error retreiving AAAA address " + \
"info for %s." % host) "info for %s." % host)
return [] return []
# Using aiodns: # Using aiodns:
future = resolver.query(host, 'AAAA') future = resolver.query(host, 'AAAA')
try: try:
recs = await future recs = yield from future
except Exception as e: except Exception as e:
log.debug('DNS: Exception while querying for %s AAAA records: %s', host, e) log.debug('DNS: Exception while querying for %s AAAA records: %s', host, e)
recs = [] recs = []
return [rec.host for rec in recs] return [rec.host for rec in recs]
async def get_SRV(host, port, service, proto='tcp', resolver=None, use_aiodns=True): @asyncio.coroutine
def get_SRV(host, port, service, proto='tcp', resolver=None, use_aiodns=True):
"""Perform SRV record resolution for a given host. """Perform SRV record resolution for a given host.
.. note:: .. note::
@@ -273,7 +277,7 @@ async def get_SRV(host, port, service, proto='tcp', resolver=None, use_aiodns=Tr
try: try:
future = resolver.query('_%s._%s.%s' % (service, proto, host), future = resolver.query('_%s._%s.%s' % (service, proto, host),
'SRV') 'SRV')
recs = await future recs = yield from future
except Exception as e: except Exception as e:
log.debug('DNS: Exception while querying for %s SRV records: %s', host, e) log.debug('DNS: Exception while querying for %s SRV records: %s', host, e)
return [] return []

View File

@@ -19,10 +19,10 @@ import ssl
import weakref import weakref
import uuid import uuid
import xml.etree.ElementTree as ET import xml.etree.ElementTree
from slixmpp.xmlstream.asyncio import asyncio from slixmpp.xmlstream.asyncio import asyncio
from slixmpp.xmlstream import tostring from slixmpp.xmlstream import tostring, highlight
from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
from slixmpp.xmlstream.resolver import resolve, default_resolver from slixmpp.xmlstream.resolver import resolve, default_resolver
@@ -64,12 +64,12 @@ class XMLStream(asyncio.BaseProtocol):
:param int port: The port to use for the connection. Defaults to 0. :param int port: The port to use for the connection. Defaults to 0.
""" """
def __init__(self, host='', port=0): def __init__(self, socket=None, host='', port=0):
# The asyncio.Transport object provided by the connection_made() # The asyncio.Transport object provided by the connection_made()
# callback when we are connected # callback when we are connected
self.transport = None self.transport = None
# The socket that is used internally by the transport object # The socket the is used internally by the transport object
self.socket = None self.socket = None
self.connect_loop_wait = 0 self.connect_loop_wait = 0
@@ -204,9 +204,6 @@ class XMLStream(asyncio.BaseProtocol):
#: We use an ID prefix to ensure that all ID values are unique. #: We use an ID prefix to ensure that all ID values are unique.
self._id_prefix = '%s-' % uuid.uuid4() self._id_prefix = '%s-' % uuid.uuid4()
# Current connection attempt (Future)
self._current_connection_attempt = None
#: A list of DNS results that have not yet been tried. #: A list of DNS results that have not yet been tried.
self.dns_answers = None self.dns_answers = None
@@ -268,7 +265,6 @@ class XMLStream(asyncio.BaseProtocol):
localhost localhost
""" """
self.cancel_connection_attempt()
if host and port: if host and port:
self.address = (host, int(port)) self.address = (host, int(port))
try: try:
@@ -285,15 +281,15 @@ class XMLStream(asyncio.BaseProtocol):
self.disable_starttls = disable_starttls self.disable_starttls = disable_starttls
self.event("connecting") self.event("connecting")
self._current_connection_attempt = asyncio.ensure_future(self._connect_routine()) asyncio.async(self._connect_routine())
async def _connect_routine(self): @asyncio.coroutine
def _connect_routine(self):
self.event_when_connected = "connected" self.event_when_connected = "connected"
record = await self.pick_dns_answer(self.default_domain) record = yield from self.pick_dns_answer(self.default_domain)
if record is not None: if record is not None:
host, address, dns_port = record host, address, port = record
port = dns_port if dns_port else self.address[1]
self.address = (address, port) self.address = (address, port)
self._service_name = host self._service_name = host
else: else:
@@ -301,19 +297,13 @@ class XMLStream(asyncio.BaseProtocol):
# and try (host, port) as a last resort # and try (host, port) as a last resort
self.dns_answers = None self.dns_answers = None
if self.use_ssl: yield from asyncio.sleep(self.connect_loop_wait)
ssl_context = self.get_ssl_context()
else:
ssl_context = None
await asyncio.sleep(self.connect_loop_wait)
try: try:
await self.loop.create_connection(lambda: self, yield from self.loop.create_connection(lambda: self,
self.address[0], self.address[0],
self.address[1], self.address[1],
ssl=ssl_context, ssl=self.use_ssl,
server_hostname=self.default_domain if self.use_ssl else None) server_hostname=self.default_domain if self.use_ssl else None)
self.connect_loop_wait = 0
except Socket.gaierror as e: except Socket.gaierror as e:
self.event('connection_failed', self.event('connection_failed',
'No DNS record available for %s' % self.default_domain) 'No DNS record available for %s' % self.default_domain)
@@ -321,7 +311,9 @@ class XMLStream(asyncio.BaseProtocol):
log.debug('Connection failed: %s', e) log.debug('Connection failed: %s', e)
self.event("connection_failed", e) self.event("connection_failed", e)
self.connect_loop_wait = self.connect_loop_wait * 2 + 1 self.connect_loop_wait = self.connect_loop_wait * 2 + 1
self._current_connection_attempt = asyncio.ensure_future(self._connect_routine()) asyncio.async(self._connect_routine())
else:
self.connect_loop_wait = 0
def process(self, *, forever=True, timeout=None): def process(self, *, forever=True, timeout=None):
"""Process all the available XMPP events (receiving or sending data on the """Process all the available XMPP events (receiving or sending data on the
@@ -347,17 +339,14 @@ class XMLStream(asyncio.BaseProtocol):
""" """
self.xml_depth = 0 self.xml_depth = 0
self.xml_root = None self.xml_root = None
self.parser = ET.XMLPullParser(("start", "end")) self.parser = xml.etree.ElementTree.XMLPullParser(("start", "end"))
def connection_made(self, transport): def connection_made(self, transport):
"""Called when the TCP connection has been established with the server """Called when the TCP connection has been established with the server
""" """
self.event(self.event_when_connected) self.event(self.event_when_connected)
self.transport = transport self.transport = transport
self.socket = self.transport.get_extra_info( self.socket = self.transport.get_extra_info("socket")
"ssl_object",
default=self.transport.get_extra_info("socket")
)
self.init_parser() self.init_parser()
self.send_raw(self.stream_header) self.send_raw(self.stream_header)
self.dns_answers = None self.dns_answers = None
@@ -369,50 +358,33 @@ class XMLStream(asyncio.BaseProtocol):
event. This could trigger one or more event (a stanza is received, event. This could trigger one or more event (a stanza is received,
the stream is opened, etc). the stream is opened, etc).
""" """
if self.parser is None:
log.warning('Received data before the connection is established: %r',
data)
return
self.parser.feed(data) self.parser.feed(data)
try: for event, xml in self.parser.read_events():
for event, xml in self.parser.read_events(): if event == 'start':
if event == 'start': if self.xml_depth == 0:
if self.xml_depth == 0: # We have received the start of the root element.
# We have received the start of the root element. self.xml_root = xml
self.xml_root = xml log.debug('RECV: %s', highlight(tostring(self.xml_root, xmlns=self.default_ns,
log.debug('RECV: %s', tostring(self.xml_root, stream=self,
xmlns=self.default_ns, top_level=True,
stream=self, open_only=True)))
top_level=True, self.start_stream_handler(self.xml_root)
open_only=True)) self.xml_depth += 1
self.start_stream_handler(self.xml_root) if event == 'end':
self.xml_depth += 1 self.xml_depth -= 1
if event == 'end': if self.xml_depth == 0:
self.xml_depth -= 1 # The stream's root element has closed,
if self.xml_depth == 0: # terminating the stream.
# The stream's root element has closed, log.debug("End of stream received")
# terminating the stream. self.abort()
log.debug("End of stream received") elif self.xml_depth == 1:
self.abort() # A stanza is an XML element that is a direct child of
elif self.xml_depth == 1: # the root element, hence the check of depth == 1
# A stanza is an XML element that is a direct child of self._spawn_event(xml)
# the root element, hence the check of depth == 1 if self.xml_root is not None:
self._spawn_event(xml) # Keep the root element empty of children to
if self.xml_root is not None: # save on memory use.
# Keep the root element empty of children to self.xml_root.clear()
# save on memory use.
self.xml_root.clear()
except ET.ParseError:
log.error('Parse error: %r', data)
# Due to cyclic dependencies, this cant be imported at the module
# level.
from slixmpp.stanza.stream_error import StreamError
error = StreamError()
error['condition'] = 'not-well-formed'
error['text'] = 'Server sent: %r' % data
self.send(error)
self.disconnect()
def is_connected(self): def is_connected(self):
return self.transport is not None return self.transport is not None
@@ -436,22 +408,11 @@ class XMLStream(asyncio.BaseProtocol):
self.transport = None self.transport = None
self.socket = None self.socket = None
def cancel_connection_attempt(self):
"""
Immediately cancel the current create_connection() Future.
This is useful when a client using slixmpp tries to connect
on flaky networks, where sometimes a connection just gets lost
and it needs to reconnect while the attempt is still ongoing.
"""
if self._current_connection_attempt:
self._current_connection_attempt.cancel()
self._current_connection_attempt = None
def disconnect(self, wait=2.0): def disconnect(self, wait=2.0):
"""Close the XML stream and wait for an acknowldgement from the server for """Close the XML stream and wait for an acknowldgement from the server for
at most `wait` seconds. After the given number of seconds has at most `wait` seconds. After the given number of seconds has
passed without a response from the serveur, or when the server passed without a response from the serveur, or when the server
successfully responds with a closure of its own stream, abort() is successfuly responds with a closure of its own stream, abort() is
called. If wait is 0.0, this is almost equivalent to calling abort() called. If wait is 0.0, this is almost equivalent to calling abort()
directly. directly.
@@ -460,7 +421,6 @@ class XMLStream(asyncio.BaseProtocol):
:param wait: Time to wait for a response from the server. :param wait: Time to wait for a response from the server.
""" """
self.cancel_connection_attempt()
if self.transport: if self.transport:
self.send_raw(self.stream_footer) self.send_raw(self.stream_footer)
self.schedule('Disconnect wait', wait, self.schedule('Disconnect wait', wait,
@@ -470,7 +430,6 @@ class XMLStream(asyncio.BaseProtocol):
""" """
Forcibly close the connection Forcibly close the connection
""" """
self.cancel_connection_attempt()
if self.transport: if self.transport:
self.transport.close() self.transport.close()
self.transport.abort() self.transport.abort()
@@ -510,10 +469,14 @@ class XMLStream(asyncio.BaseProtocol):
""" """
pass pass
def get_ssl_context(self): def start_tls(self):
""" """Perform handshakes for TLS.
Get SSL context.
If the handshake is successful, the XML stream will need
to be restarted.
""" """
self.event_when_connected = "tls_success"
if self.ciphers is not None: if self.ciphers is not None:
self.ssl_context.set_ciphers(self.ciphers) self.ssl_context.set_ciphers(self.ciphers)
if self.keyfile and self.certfile: if self.keyfile and self.certfile:
@@ -528,44 +491,26 @@ class XMLStream(asyncio.BaseProtocol):
self.ssl_context.verify_mode = ssl.CERT_REQUIRED self.ssl_context.verify_mode = ssl.CERT_REQUIRED
self.ssl_context.load_verify_locations(cafile=self.ca_certs) self.ssl_context.load_verify_locations(cafile=self.ca_certs)
return self.ssl_context ssl_connect_routine = self.loop.create_connection(lambda: self, ssl=self.ssl_context,
sock=self.socket,
async def start_tls(self): server_hostname=self.default_domain)
"""Perform handshakes for TLS. @asyncio.coroutine
def ssl_coro():
If the handshake is successful, the XML stream will need try:
to be restarted. transp, prot = yield from ssl_connect_routine
""" except ssl.SSLError as e:
self.event_when_connected = "tls_success" log.debug('SSL: Unable to connect', exc_info=True)
ssl_context = self.get_ssl_context() log.error('CERT: Invalid certificate trust chain.')
try: if not self.event_handled('ssl_invalid_chain'):
if hasattr(self.loop, 'start_tls'): self.disconnect()
transp = await self.loop.start_tls(self.transport, else:
self, ssl_context) self.event('ssl_invalid_chain', e)
# Python < 3.7
else: else:
transp, _ = await self.loop.create_connection( der_cert = transp.get_extra_info("socket").getpeercert(True)
lambda: self, pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
ssl=self.ssl_context, self.event('ssl_cert', pem_cert)
sock=self.socket,
server_hostname=self.default_domain asyncio.async(ssl_coro())
)
except ssl.SSLError as e:
log.debug('SSL: Unable to connect', exc_info=True)
log.error('CERT: Invalid certificate trust chain.')
if not self.event_handled('ssl_invalid_chain'):
self.disconnect()
else:
self.event('ssl_invalid_chain', e)
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)
# If we use the builtin start_tls, the connection_made() protocol
# method is not called automatically
if hasattr(self.loop, 'start_tls'):
self.connection_made(transp)
return True
def _start_keepalive(self, event): def _start_keepalive(self, event):
"""Begin sending whitespace periodically to keep the connection alive. """Begin sending whitespace periodically to keep the connection alive.
@@ -678,7 +623,8 @@ class XMLStream(asyncio.BaseProtocol):
idx += 1 idx += 1
return False return False
async def get_dns_records(self, domain, port=None): @asyncio.coroutine
def get_dns_records(self, domain, port=None):
"""Get the DNS records for a domain. """Get the DNS records for a domain.
:param domain: The domain in question. :param domain: The domain in question.
@@ -690,7 +636,7 @@ class XMLStream(asyncio.BaseProtocol):
resolver = default_resolver(loop=self.loop) resolver = default_resolver(loop=self.loop)
self.configure_dns(resolver, domain=domain, port=port) self.configure_dns(resolver, domain=domain, port=port)
result = await resolve(domain, port, result = yield from resolve(domain, port,
service=self.dns_service, service=self.dns_service,
resolver=resolver, resolver=resolver,
use_ipv6=self.use_ipv6, use_ipv6=self.use_ipv6,
@@ -698,7 +644,8 @@ class XMLStream(asyncio.BaseProtocol):
loop=self.loop) loop=self.loop)
return result return result
async def pick_dns_answer(self, domain, port=None): @asyncio.coroutine
def pick_dns_answer(self, domain, port=None):
"""Pick a server and port from DNS answers. """Pick a server and port from DNS answers.
Gets DNS answers if none available. Gets DNS answers if none available.
@@ -708,7 +655,7 @@ class XMLStream(asyncio.BaseProtocol):
:param port: If the results don't include a port, use this one. :param port: If the results don't include a port, use this one.
""" """
if self.dns_answers is None: if self.dns_answers is None:
dns_records = await self.get_dns_records(domain, port) dns_records = yield from self.get_dns_records(domain, port)
self.dns_answers = iter(dns_records) self.dns_answers = iter(dns_records)
try: try:
@@ -773,15 +720,16 @@ class XMLStream(asyncio.BaseProtocol):
# If the callback is a coroutine, schedule it instead of # If the callback is a coroutine, schedule it instead of
# running it directly # running it directly
if asyncio.iscoroutinefunction(handler_callback): if asyncio.iscoroutinefunction(handler_callback):
async def handler_callback_routine(cb): @asyncio.coroutine
def handler_callback_routine(cb):
try: try:
await cb(data) yield from cb(data)
except Exception as e: except Exception as e:
if old_exception: if old_exception:
old_exception(e) old_exception(e)
else: else:
self.exception(e) self.exception(e)
asyncio.ensure_future(handler_callback_routine(handler_callback)) asyncio.async(handler_callback_routine(handler_callback))
else: else:
try: try:
handler_callback(data) handler_callback(data)
@@ -894,7 +842,8 @@ class XMLStream(asyncio.BaseProtocol):
if data is None: if data is None:
return return
str_data = tostring(data.xml, xmlns=self.default_ns, str_data = tostring(data.xml, xmlns=self.default_ns,
stream=self, top_level=True) stream=self,
top_level=True)
self.send_raw(str_data) self.send_raw(str_data)
else: else:
self.send_raw(data) self.send_raw(data)
@@ -912,7 +861,7 @@ class XMLStream(asyncio.BaseProtocol):
:param string data: Any bytes or utf-8 string value. :param string data: Any bytes or utf-8 string value.
""" """
log.debug("SEND: %s", data) log.debug("SEND: %s", highlight(data))
if not self.transport: if not self.transport:
raise NotConnectedError() raise NotConnectedError()
if isinstance(data, str): if isinstance(data, str):
@@ -965,7 +914,7 @@ class XMLStream(asyncio.BaseProtocol):
if stanza is None: if stanza is None:
return return
log.debug("RECV: %s", stanza) log.debug("RECV: %s", highlight(stanza))
# Match the stanza against registered handlers. Handlers marked # Match the stanza against registered handlers. Handlers marked
# to run "in stream" will be executed immediately; the rest will # to run "in stream" will be executed immediately; the rest will

View File

@@ -1,57 +0,0 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2017 Emmanuel Gil Peyrot
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import unittest
from slixmpp import Iq
from slixmpp.test import SlixTest
from slixmpp.plugins.xep_0300 import Hash
from slixmpp.xmlstream import register_stanza_plugin
class TestHash(SlixTest):
def setUp(self):
register_stanza_plugin(Iq, Hash)
def testSimpleElement(self):
"""Test that the element is created correctly."""
iq = Iq()
iq['type'] = 'set'
iq['hash']['algo'] = 'sha-256'
iq['hash']['value'] = 'EQgS9n+h4fARf289cCQcGkKnsHcRqTwkd8xRbZBC+ds='
self.check(iq, """
<iq type="set">
<hash xmlns="urn:xmpp:hashes:2" algo="sha-256">EQgS9n+h4fARf289cCQcGkKnsHcRqTwkd8xRbZBC+ds=</hash>
</iq>
""")
def testInvalidAlgo(self):
"""Test that invalid algos raise an exception."""
iq = Iq()
iq['type'] = 'set'
try:
iq['hash']['algo'] = 'coucou'
except ValueError:
pass
else:
raise self.failureException
#def testDisabledAlgo(self):
# """Test that disabled algos arent used."""
# iq = Iq()
# iq['type'] = 'set'
# try:
# iq['hash']['algo'] = 'sha-1'
# except ValueError:
# pass
# else:
# raise self.failureException
suite = unittest.TestLoader().loadTestsFromTestCase(TestHash)

View File

@@ -1,37 +0,0 @@
import unittest
from slixmpp import Message
from slixmpp.test import SlixTest
import slixmpp.plugins.xep_0380 as xep_0380
from slixmpp.xmlstream import register_stanza_plugin
class TestEME(SlixTest):
def setUp(self):
register_stanza_plugin(Message, xep_0380.stanza.Encryption)
def testCreateEME(self):
"""Testing creating EME."""
xmlstring = """
<message>
<encryption xmlns="urn:xmpp:eme:0" namespace="%s"%s />
</message>
"""
msg = self.Message()
self.check(msg, "<message />")
msg['eme']['namespace'] = 'urn:xmpp:otr:0'
self.check(msg, xmlstring % ('urn:xmpp:otr:0', ''))
msg['eme']['namespace'] = 'urn:xmpp:openpgp:0'
self.check(msg, xmlstring % ('urn:xmpp:openpgp:0', ''))
msg['eme']['name'] = 'OX'
self.check(msg, xmlstring % ('urn:xmpp:openpgp:0', ' name="OX"'))
del msg['eme']
self.check(msg, "<message />")
suite = unittest.TestLoader().loadTestsFromTestCase(TestEME)

View File

@@ -4,7 +4,6 @@ import sys
import datetime import datetime
import time import time
import threading import threading
import re
from slixmpp.test import * from slixmpp.test import *
from slixmpp.xmlstream import ElementBase from slixmpp.xmlstream import ElementBase
@@ -772,7 +771,7 @@ class TestStreamSensorData(SlixTest):
# Remove the returned datetime to allow predictable test # Remove the returned datetime to allow predictable test
xml_stanza = self._filtered_stanza_prepare() xml_stanza = self._filtered_stanza_prepare()
error_text = xml_stanza['rejected']['error'] #['text'] error_text = xml_stanza['rejected']['error'] #['text']
error_text = re.sub(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[\+\-]\d{2}:\d{2})?', '', error_text) error_text = error_text[:error_text.find(':')]
xml_stanza['rejected']['error'] = error_text xml_stanza['rejected']['error'] = error_text
self._filtered_stanza_check(""" self._filtered_stanza_check("""
@@ -781,7 +780,7 @@ class TestStreamSensorData(SlixTest):
to='master@clayster.com/amr' to='master@clayster.com/amr'
id='1'> id='1'>
<rejected xmlns='urn:xmpp:iot:sensordata' seqnr='1'> <rejected xmlns='urn:xmpp:iot:sensordata' seqnr='1'>
<error>Invalid datetime in 'when' flag, cannot set a time in the past (…). Current time: …</error> <error>Invalid datetime in 'when' flag, cannot set a time in the past. Current time</error>
</rejected> </rejected>
</iq> </iq>
""", xml_stanza) """, xml_stanza)