Compare commits

..

43 Commits

Author SHA1 Message Date
mathieui
d73f56a7af Release slixmpp 1.3.0 2017-11-28 20:16:08 +01:00
Emmanuel Gil Peyrot
7c7f4308c5 Add a Markup plugin. 2017-11-23 12:18:01 +00:00
mathieui
eab8c265f4 Record the current connection attempt in a future and allow cancellation
It does not make sense to have competing connection attempts, as the
XMLStream class is not designed for this. On slow and unpredictable
networks, it means we could have two c2s connections opened, leading to
mayhem.
2017-11-23 00:00:37 +01:00
Emmanuel Gil Peyrot
80b9cd43b1 MAM example: Also display the timestamp. 2017-10-24 10:54:53 +01:00
Emmanuel Gil Peyrot
af1f9e08ad Clean up the MAM example a bit. 2017-10-24 10:47:42 +01:00
Emmanuel Gil Peyrot
e3fd0af9c8 xep_0054: Fix parsing BINVAL element. 2017-10-08 15:42:48 +01:00
mathieui
27e23672c1 Update the MAM plugin for asyncio & new namespace
And add an example
2017-09-24 17:43:06 +02:00
mathieui
b38e229359 Update RSM for asyncio
- Use an async iterator
- Add a "recv_interface" parameter in order to differenciate the stanza
   we send from the stanza we receive (required for MAM)
- Add a pre_cb to run before sending the query stanza
- Add a post_cb to run after receiving the result stanza
2017-07-21 15:01:13 +02:00
Emmanuel Gil Peyrot
9a563f1425 XEP-0030: Optimise add_node usage a bit. 2017-07-17 22:46:48 +01:00
Emmanuel Gil Peyrot
8b6f5953a7 XEP-0319: Use the correct timezone.
This fixes a specification violation, XEP-0082 says that a date MUST
have a timezone, but we were sending the *local* time without any
timezone information.
2017-07-17 22:20:30 +01:00
Emmanuel Gil Peyrot
2d2a80c73d xmlstream: Remove pygments dumping.
It’s slow and makes the debug logs difficult to parse.
2017-07-17 21:17:02 +01:00
Mathias Ertl
4dfdd5d8e3 always define ssl_context 2017-05-24 13:18:22 +02:00
Mathias Ertl
1994ed3025 pass SSL context to TLS connections 2017-05-24 11:31:13 +02:00
Mathias Ertl
aaa45846d3 add function to explicitly get the ssl context 2017-05-24 11:31:13 +02:00
louiz’
d7ffcb54eb Merge remote-tracking branch 'samwhited/sslsocket_workaround' 2017-05-16 17:24:46 +02:00
Tom Wambold
c33749e57a Fixes port being set to 0 when connecting via hostname.
This seems to be the same issue as:

  https://dev.louiz.org/issues/3164

Using their suggested fix, if the DNS lookup doesn't return a port, use
the one passed in instead.
2017-05-08 15:58:28 -04:00
Emmanuel Gil Peyrot
e4107d8b4d sasl: Merge two bytes instead of concatenating them at runtime. 2017-04-28 21:26:03 +01:00
mathieui
da5cb72d3a Add XMPP classifier to setup.py 2017-04-10 02:24:14 +02:00
Emmanuel Gil Peyrot
c372bd5168 xmlstream: Warn when the parser is None when data is received. 2017-02-16 11:27:36 +00:00
mathieui
cabf623131 Fix the http over xmpp example 2017-02-14 01:04:38 +01:00
mathieui
ffc240d5b6 Fix the gtalk example 2017-02-14 01:04:27 +01:00
mathieui
cc4522d9cd Fix custom stanza examples 2017-02-14 01:00:41 +01:00
mathieui
5bf69dca76 Return a Future on clientxmpp.get_roster() 2017-02-14 00:46:36 +01:00
Emmanuel Gil Peyrot
59dad12820 XEP-0300: Workaround for Python 3.5 or below. 2017-02-11 23:30:43 +00:00
Emmanuel Gil Peyrot
007c836296 XEP-0300: Add rudimentary tests. 2017-02-11 04:02:44 +00:00
Emmanuel Gil Peyrot
3721bf9f6b Implement XEP-0300 (Use of Cryptographic Hash Functions in XMPP)
This is used to provide hash agility support and let other XEPs select
which hash function they support.
2017-02-11 04:02:20 +00:00
Cédric 'dek' Laudrel
802949eba8 fix small typo in README 2017-02-10 00:08:40 +01:00
mathieui
24f35e433f slixmpp 1.2.4 release 2017-01-30 23:02:45 +01:00
mathieui
22664ee7b8 Fix carbons 2017-01-28 00:02:27 +01:00
Clint Olson
6476cfcde5 Remove unused import caught by Codacy. 2017-01-23 23:58:53 -08:00
Clint Olson
5bb347e884 Fix partially-merged Google plugin from acc52fd935. 2017-01-23 23:51:59 -08:00
Emmanuel Gil Peyrot
eb1251b919 Fix a typo in the title of the MUC documentation. 2017-01-08 17:38:11 +00:00
Emmanuel Gil Peyrot
820144c40c Add missing asyncio.coroutine decorators. 2016-12-30 13:41:15 +01:00
Emmanuel Gil Peyrot
6034df0a78 Check for XML parsing errors and disconnect in that case. 2016-12-29 18:59:09 +01:00
Emmanuel Gil Peyrot
df4012e66d XMLStream: Break a long line to make it more readable. 2016-12-29 18:41:09 +01:00
Emmanuel Gil Peyrot
c372f3071a Examples: Use argparse for http_over_xmpp. 2016-12-29 18:34:37 +01:00
Emmanuel Gil Peyrot
829c8b27b6 Test more things before trying to build our stringprep module. 2016-12-25 13:28:51 +01:00
mathieui
fb3ac78bf9 slixmpp 1.2.3 2016-12-07 21:47:54 +01:00
mathieui
ffd9436e5c Fix roster push origin detection and tests 2016-12-07 19:06:25 +01:00
louiz’
bbb1344d79 Add very basic gitlab-ci.yml file 2016-12-05 00:13:13 +01:00
Emmanuel Gil Peyrot
457785b286 XEP-0380: Add a helper to test for the presence of an EME tag. 2016-11-26 16:41:48 +00:00
Emmanuel Gil Peyrot
4847f834bd Add a plugin for XEP-0380: Explicit Message Encryption. 2016-11-26 16:29:19 +00:00
Sam Whited
8b06aa1146 Fix fetching the SSL socket for Python 3.4 and 3.5 2016-10-06 13:00:17 -05:00
42 changed files with 1612 additions and 175 deletions

8
.gitlab-ci.yml Normal file
View File

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

View File

@@ -36,7 +36,7 @@ The Slixmpp Boilerplate
-------------------------
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
based project. See the documetation or examples directory for more detailed archetypes for
based project. See the documentation or examples directory for more detailed archetypes for
Slixmpp projects::
import logging

View File

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

View File

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

View File

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

View File

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

View File

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

98
examples/mam.py Executable file
View File

@@ -0,0 +1,98 @@
#!/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)

120
examples/markup.py Executable file
View File

@@ -0,0 +1,120 @@
#!/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

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

View File

@@ -15,6 +15,7 @@
import asyncio
import logging
from slixmpp.jid import JID
from slixmpp.stanza import StreamFeatures
from slixmpp.basexmpp import BaseXMPP
from slixmpp.exceptions import XMPPError
@@ -110,7 +111,13 @@ class ClientXMPP(BaseXMPP):
self._handle_stream_features))
def roster_push_filter(iq):
from_ = iq['from']
if from_ and from_ != self.boundjid.bare:
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(
@@ -248,7 +255,7 @@ class ClientXMPP(BaseXMPP):
orig_cb(resp)
callback = wrapped
iq.send(callback, timeout, timeout_callback)
return iq.send(callback, timeout, timeout_callback)
def _reset_connection_state(self, event=None):
#TODO: Use stream state here

View File

@@ -0,0 +1,47 @@
"""
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

@@ -0,0 +1,10 @@
"""
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

@@ -0,0 +1,47 @@
"""
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

@@ -0,0 +1,10 @@
"""
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

@@ -0,0 +1,101 @@
"""
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

@@ -0,0 +1,10 @@
"""
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

@@ -0,0 +1,78 @@
"""
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

@@ -0,0 +1,10 @@
"""
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

@@ -0,0 +1,110 @@
"""
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

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

View File

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

View File

@@ -19,23 +19,27 @@ from slixmpp.exceptions import XMPPError
log = logging.getLogger(__name__)
class ResultIterator():
class ResultIterator:
"""
An iterator for Result Set Managment
"""
def __init__(self, query, interface, results='substanzas', amount=10,
start=None, reverse=False):
start=None, reverse=False, recv_interface=None,
pre_cb=None, post_cb=None):
"""
Arguments:
query -- The template query
interface -- The substanza of the query, for example disco_items
interface -- The substanza of the query to send, 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
countable list of query results.
amount -- The max amounts of items to request per iteration
start -- From which item id to start
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:
q = Iq()
@@ -49,17 +53,23 @@ class ResultIterator():
self.amount = amount
self.start = start
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.reverse = reverse
self._stop = False
def __iter__(self):
def __aiter__(self):
return self
def __next__(self):
return self.next()
async def __anext__(self):
return await self.next()
def next(self):
async def next(self):
"""
Return the next page of results from a query.
@@ -68,7 +78,7 @@ class ResultIterator():
of items.
"""
if self._stop:
raise StopIteration
raise StopAsyncIteration
self.query[self.interface]['rsm']['before'] = self.reverse
self.query['id'] = self.query.stream.new_id()
self.query[self.interface]['rsm']['max'] = str(self.amount)
@@ -79,28 +89,32 @@ class ResultIterator():
self.query[self.interface]['rsm']['after'] = self.start
try:
r = self.query.send(block=True)
if self.pre_cb:
self.pre_cb(self.query)
r = await self.query.send()
if not r[self.interface]['rsm']['first'] and \
not r[self.interface]['rsm']['last']:
raise StopIteration
if not r[self.recv_interface]['rsm']['first'] and \
not r[self.recv_interface]['rsm']['last']:
raise StopAsyncIteration
if r[self.interface]['rsm']['count'] and \
r[self.interface]['rsm']['first_index']:
count = int(r[self.interface]['rsm']['count'])
first = int(r[self.interface]['rsm']['first_index'])
num_items = len(r[self.interface][self.results])
if r[self.recv_interface]['rsm']['count'] and \
r[self.recv_interface]['rsm']['first_index']:
count = int(r[self.recv_interface]['rsm']['count'])
first = int(r[self.recv_interface]['rsm']['first_index'])
num_items = len(r[self.recv_interface][self.results])
if first + num_items == count:
self._stop = True
if self.reverse:
self.start = r[self.interface]['rsm']['first']
self.start = r[self.recv_interface]['rsm']['first']
else:
self.start = r[self.interface]['rsm']['last']
self.start = r[self.recv_interface]['rsm']['last']
if self.post_cb:
self.post_cb(r)
return r
except XMPPError:
raise StopIteration
raise StopAsyncIteration
class XEP_0059(BasePlugin):
@@ -127,7 +141,8 @@ class XEP_0059(BasePlugin):
def session_bind(self, jid):
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.
@@ -137,9 +152,23 @@ class XEP_0059(BasePlugin):
basic disco#items query.
interface -- The name of the substanza to which the
result set management stanza should be
appended. For example, for disco#items queries
the interface 'disco_items' should be used.
appended in the query stanza. For example,
for disco#items queries the interface
'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
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

@@ -55,6 +55,7 @@ class XEP_0065(BasePlugin):
"""Returns the socket associated to the SID."""
return self._sessions.get(sid, None)
@asyncio.coroutine
def handshake(self, to, ifrom=None, sid=None, timeout=None):
""" Starts the handshake to establish the socks5 bytestreams
connection.
@@ -104,6 +105,7 @@ class XEP_0065(BasePlugin):
iq['socks'].add_streamhost(proxy, host, port)
return iq.send(timeout=timeout, callback=callback)
@asyncio.coroutine
def discover_proxies(self, jid=None, ifrom=None, timeout=None):
"""Auto-discover the JIDs of SOCKS5 proxies on an XMPP server."""
if jid is None:

View File

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

View File

@@ -0,0 +1,16 @@
"""
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

@@ -0,0 +1,87 @@
"""
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
'prefered': '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.prefered
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

@@ -0,0 +1,35 @@
"""
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,35 +36,58 @@ class XEP_0313(BasePlugin):
register_stanza_plugin(Iq, stanza.MAM)
register_stanza_plugin(Iq, stanza.Preferences)
register_stanza_plugin(Message, stanza.Result)
register_stanza_plugin(Message, stanza.Archived, iterable=True)
register_stanza_plugin(Iq, stanza.Fin)
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.Fin, self.xmpp['xep_0059'].stanza.Set)
def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None,
timeout=None, callback=None, iterator=False):
timeout=None, callback=None, iterator=False, rsm=None):
iq = self.xmpp.Iq()
query_id = iq['id']
iq['to'] = jid
iq['from'] = ifrom
iq['type'] = 'get'
iq['type'] = 'set'
iq['mam']['queryid'] = query_id
iq['mam']['start'] = start
iq['mam']['end'] = end
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(
'MAM_Results_%s' % query_id,
StanzaPath('message/mam_result@queryid=%s' % query_id))
self.xmpp.register_handler(collector)
if iterator:
return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results')
def wrapped_cb(iq):
results = collector.stop()
if iq['type'] == 'result':
iq['mam']['results'] = results
callback(iq)
if callback:
callback(iq)
return iq.send(timeout=timeout, callback=wrapped_cb)
def set_preferences(self, jid=None, default=None, always=None, never=None,

View File

@@ -10,44 +10,76 @@ import datetime as dt
from slixmpp.jid import JID
from slixmpp.xmlstream import ElementBase, ET
from slixmpp.plugins import xep_0082
from slixmpp.plugins import xep_0082, xep_0004
class MAM(ElementBase):
name = 'query'
namespace = 'urn:xmpp:mam:tmp'
namespace = 'urn:xmpp:mam:2'
plugin_attrib = 'mam'
interfaces = {'queryid', 'start', 'end', 'with', 'results'}
sub_interfaces = {'start', 'end', 'with'}
def setup(self, xml=None):
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 = []
def __get_fields(self):
return self._form.get_fields()
def get_start(self):
timestamp = self._get_sub_text('start')
return xep_0082.parse(timestamp)
fields = self.__get_fields()
field = fields.get('start')
if field:
return xep_0082.parse(field['value'])
def set_start(self, value):
if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value)
self._set_sub_text('start', value)
fields = self.__get_fields()
field = fields.get('start')
if field:
field['value'] = value
else:
field = self._form.add_field(var='start')
field['value'] = value
def get_end(self):
timestamp = self._get_sub_text('end')
return xep_0082.parse(timestamp)
fields = self.__get_fields()
field = fields.get('end')
if field:
return xep_0082.parse(field['value'])
def set_end(self, value):
if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value)
self._set_sub_text('end', value)
fields = self.__get_fields()
field = fields.get('end')
if field:
field['value'] = value
else:
field = self._form.add_field(var='end')
field['value'] = value
def get_with(self):
return JID(self._get_sub_text('with'))
fields = self.__get_fields()
field = fields.get('with')
if field:
return JID(field['value'])
def set_with(self, value):
self._set_sub_text('with', str(value))
fields = self.__get_fields()
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
# way to access the set of collected message responses
# from the query.
@@ -64,7 +96,7 @@ class MAM(ElementBase):
class Preferences(ElementBase):
name = 'prefs'
namespace = 'urn:xmpp:mam:tmp'
namespace = 'urn:xmpp:mam:2'
plugin_attrib = 'mam_prefs'
interfaces = {'default', 'always', 'never'}
sub_interfaces = {'always', 'never'}
@@ -118,22 +150,13 @@ class Preferences(ElementBase):
never.append(jid_xml)
class Fin(ElementBase):
name = 'fin'
namespace = 'urn:xmpp:mam:2'
plugin_attrib = 'mam_fin'
class Result(ElementBase):
name = 'result'
namespace = 'urn:xmpp:mam:tmp'
namespace = 'urn:xmpp:mam:2'
plugin_attrib = 'mam_result'
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, value):
return self._set_attr('by', str(value))

View File

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

View File

@@ -0,0 +1,15 @@
"""
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

@@ -0,0 +1,69 @@
"""
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

@@ -0,0 +1,16 @@
"""
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

@@ -0,0 +1,15 @@
"""
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

@@ -0,0 +1,161 @@
"""
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

@@ -0,0 +1,123 @@
"""
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

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

View File

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

View File

@@ -19,10 +19,10 @@ import ssl
import weakref
import uuid
import xml.etree.ElementTree
import xml.etree.ElementTree as ET
from slixmpp.xmlstream.asyncio import asyncio
from slixmpp.xmlstream import tostring, highlight
from slixmpp.xmlstream import tostring
from slixmpp.xmlstream.stanzabase import StanzaBase, ElementBase
from slixmpp.xmlstream.resolver import resolve, default_resolver
@@ -204,6 +204,9 @@ class XMLStream(asyncio.BaseProtocol):
#: We use an ID prefix to ensure that all ID values are unique.
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.
self.dns_answers = None
@@ -265,6 +268,7 @@ class XMLStream(asyncio.BaseProtocol):
localhost
"""
self.cancel_connection_attempt()
if host and port:
self.address = (host, int(port))
try:
@@ -281,7 +285,7 @@ class XMLStream(asyncio.BaseProtocol):
self.disable_starttls = disable_starttls
self.event("connecting")
asyncio.async(self._connect_routine())
self._current_connection_attempt = asyncio.async(self._connect_routine())
@asyncio.coroutine
def _connect_routine(self):
@@ -289,7 +293,8 @@ class XMLStream(asyncio.BaseProtocol):
record = yield from self.pick_dns_answer(self.default_domain)
if record is not None:
host, address, port = record
host, address, dns_port = record
port = dns_port if dns_port else self.address[1]
self.address = (address, port)
self._service_name = host
else:
@@ -297,13 +302,19 @@ class XMLStream(asyncio.BaseProtocol):
# and try (host, port) as a last resort
self.dns_answers = None
if self.use_ssl:
ssl_context = self.get_ssl_context()
else:
ssl_context = None
yield from asyncio.sleep(self.connect_loop_wait)
try:
yield from self.loop.create_connection(lambda: self,
self.address[0],
self.address[1],
ssl=self.use_ssl,
ssl=ssl_context,
server_hostname=self.default_domain if self.use_ssl else None)
self.connect_loop_wait = 0
except Socket.gaierror as e:
self.event('connection_failed',
'No DNS record available for %s' % self.default_domain)
@@ -311,9 +322,7 @@ class XMLStream(asyncio.BaseProtocol):
log.debug('Connection failed: %s', e)
self.event("connection_failed", e)
self.connect_loop_wait = self.connect_loop_wait * 2 + 1
asyncio.async(self._connect_routine())
else:
self.connect_loop_wait = 0
self._current_connection_attempt = asyncio.async(self._connect_routine())
def process(self, *, forever=True, timeout=None):
"""Process all the available XMPP events (receiving or sending data on the
@@ -339,7 +348,7 @@ class XMLStream(asyncio.BaseProtocol):
"""
self.xml_depth = 0
self.xml_root = None
self.parser = xml.etree.ElementTree.XMLPullParser(("start", "end"))
self.parser = ET.XMLPullParser(("start", "end"))
def connection_made(self, transport):
"""Called when the TCP connection has been established with the server
@@ -358,33 +367,50 @@ class XMLStream(asyncio.BaseProtocol):
event. This could trigger one or more event (a stanza is received,
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)
for event, xml in self.parser.read_events():
if event == 'start':
if self.xml_depth == 0:
# We have received the start of the root element.
self.xml_root = xml
log.debug('RECV: %s', highlight(tostring(self.xml_root, xmlns=self.default_ns,
stream=self,
top_level=True,
open_only=True)))
self.start_stream_handler(self.xml_root)
self.xml_depth += 1
if event == 'end':
self.xml_depth -= 1
if self.xml_depth == 0:
# The stream's root element has closed,
# terminating the stream.
log.debug("End of stream received")
self.abort()
elif self.xml_depth == 1:
# A stanza is an XML element that is a direct child of
# the root element, hence the check of depth == 1
self._spawn_event(xml)
if self.xml_root is not None:
# Keep the root element empty of children to
# save on memory use.
self.xml_root.clear()
try:
for event, xml in self.parser.read_events():
if event == 'start':
if self.xml_depth == 0:
# We have received the start of the root element.
self.xml_root = xml
log.debug('RECV: %s', tostring(self.xml_root,
xmlns=self.default_ns,
stream=self,
top_level=True,
open_only=True))
self.start_stream_handler(self.xml_root)
self.xml_depth += 1
if event == 'end':
self.xml_depth -= 1
if self.xml_depth == 0:
# The stream's root element has closed,
# terminating the stream.
log.debug("End of stream received")
self.abort()
elif self.xml_depth == 1:
# A stanza is an XML element that is a direct child of
# the root element, hence the check of depth == 1
self._spawn_event(xml)
if self.xml_root is not None:
# Keep the root element empty of children to
# 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):
return self.transport is not None
@@ -408,6 +434,17 @@ class XMLStream(asyncio.BaseProtocol):
self.transport = None
self.socket = None
def cancel_connection_attempt(self):
"""
Immediatly 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):
"""Close the XML stream and wait for an acknowldgement from the server for
at most `wait` seconds. After the given number of seconds has
@@ -421,6 +458,7 @@ class XMLStream(asyncio.BaseProtocol):
:param wait: Time to wait for a response from the server.
"""
self.cancel_connection_attempt()
if self.transport:
self.send_raw(self.stream_footer)
self.schedule('Disconnect wait', wait,
@@ -430,6 +468,7 @@ class XMLStream(asyncio.BaseProtocol):
"""
Forcibly close the connection
"""
self.cancel_connection_attempt()
if self.transport:
self.transport.close()
self.transport.abort()
@@ -469,14 +508,10 @@ class XMLStream(asyncio.BaseProtocol):
"""
pass
def start_tls(self):
"""Perform handshakes for TLS.
If the handshake is successful, the XML stream will need
to be restarted.
def get_ssl_context(self):
"""
Get SSL context.
"""
self.event_when_connected = "tls_success"
if self.ciphers is not None:
self.ssl_context.set_ciphers(self.ciphers)
if self.keyfile and self.certfile:
@@ -491,7 +526,18 @@ class XMLStream(asyncio.BaseProtocol):
self.ssl_context.verify_mode = ssl.CERT_REQUIRED
self.ssl_context.load_verify_locations(cafile=self.ca_certs)
ssl_connect_routine = self.loop.create_connection(lambda: self, ssl=self.ssl_context,
return self.ssl_context
def start_tls(self):
"""Perform handshakes for TLS.
If the handshake is successful, the XML stream will need
to be restarted.
"""
self.event_when_connected = "tls_success"
ssl_context = self.get_ssl_context()
ssl_connect_routine = self.loop.create_connection(lambda: self, ssl=ssl_context,
sock=self.socket,
server_hostname=self.default_domain)
@asyncio.coroutine
@@ -506,7 +552,9 @@ class XMLStream(asyncio.BaseProtocol):
else:
self.event('ssl_invalid_chain', e)
else:
der_cert = transp.get_extra_info("socket").getpeercert(True)
# Workaround for a regression in 3.4 where ssl_object was not set.
der_cert = transp.get_extra_info("ssl_object",
default=transp.get_extra_info("socket")).getpeercert(True)
pem_cert = ssl.DER_cert_to_PEM_cert(der_cert)
self.event('ssl_cert', pem_cert)
@@ -842,8 +890,7 @@ class XMLStream(asyncio.BaseProtocol):
if data is None:
return
str_data = tostring(data.xml, xmlns=self.default_ns,
stream=self,
top_level=True)
stream=self, top_level=True)
self.send_raw(str_data)
else:
self.send_raw(data)
@@ -861,7 +908,7 @@ class XMLStream(asyncio.BaseProtocol):
:param string data: Any bytes or utf-8 string value.
"""
log.debug("SEND: %s", highlight(data))
log.debug("SEND: %s", data)
if not self.transport:
raise NotConnectedError()
if isinstance(data, str):
@@ -914,7 +961,7 @@ class XMLStream(asyncio.BaseProtocol):
if stanza is None:
return
log.debug("RECV: %s", highlight(stanza))
log.debug("RECV: %s", stanza)
# Match the stanza against registered handlers. Handlers marked
# to run "in stream" will be executed immediately; the rest will

View File

@@ -0,0 +1,57 @@
"""
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

@@ -0,0 +1,37 @@
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)