Compare commits
43 Commits
slix-1.2.2
...
slix-1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d73f56a7af | ||
|
|
7c7f4308c5 | ||
|
|
eab8c265f4 | ||
|
|
80b9cd43b1 | ||
|
|
af1f9e08ad | ||
|
|
e3fd0af9c8 | ||
|
|
27e23672c1 | ||
|
|
b38e229359 | ||
|
|
9a563f1425 | ||
|
|
8b6f5953a7 | ||
|
|
2d2a80c73d | ||
|
|
4dfdd5d8e3 | ||
|
|
1994ed3025 | ||
|
|
aaa45846d3 | ||
|
|
d7ffcb54eb | ||
|
|
c33749e57a | ||
|
|
e4107d8b4d | ||
|
|
da5cb72d3a | ||
|
|
c372bd5168 | ||
|
|
cabf623131 | ||
|
|
ffc240d5b6 | ||
|
|
cc4522d9cd | ||
|
|
5bf69dca76 | ||
|
|
59dad12820 | ||
|
|
007c836296 | ||
|
|
3721bf9f6b | ||
|
|
802949eba8 | ||
|
|
24f35e433f | ||
|
|
22664ee7b8 | ||
|
|
6476cfcde5 | ||
|
|
5bb347e884 | ||
|
|
eb1251b919 | ||
|
|
820144c40c | ||
|
|
6034df0a78 | ||
|
|
df4012e66d | ||
|
|
c372f3071a | ||
|
|
829c8b27b6 | ||
|
|
fb3ac78bf9 | ||
|
|
ffd9436e5c | ||
|
|
bbb1344d79 | ||
|
|
457785b286 | ||
|
|
4847f834bd | ||
|
|
8b06aa1146 |
8
.gitlab-ci.yml
Normal file
8
.gitlab-ci.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
test:
|
||||
tags:
|
||||
- docker
|
||||
image: ubuntu:latest
|
||||
script:
|
||||
- apt update
|
||||
- apt install -y python3 cython3
|
||||
- ./run_tests.py
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.. _mucbot:
|
||||
|
||||
=========================
|
||||
Mulit-User Chat (MUC) Bot
|
||||
Multi-User Chat (MUC) Bot
|
||||
=========================
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
98
examples/mam.py
Executable 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
120
examples/markup.py
Executable 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()
|
||||
26
setup.py
26
setup.py
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
47
slixmpp/plugins/google/__init__.py
Normal file
47
slixmpp/plugins/google/__init__.py
Normal 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)
|
||||
10
slixmpp/plugins/google/auth/__init__.py
Normal file
10
slixmpp/plugins/google/auth/__init__.py
Normal 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
|
||||
47
slixmpp/plugins/google/auth/auth.py
Normal file
47
slixmpp/plugins/google/auth/auth.py
Normal 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
|
||||
10
slixmpp/plugins/google/gmail/__init__.py
Normal file
10
slixmpp/plugins/google/gmail/__init__.py
Normal 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
|
||||
101
slixmpp/plugins/google/gmail/stanza.py
Normal file
101
slixmpp/plugins/google/gmail/stanza.py
Normal 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)
|
||||
10
slixmpp/plugins/google/nosave/__init__.py
Normal file
10
slixmpp/plugins/google/nosave/__init__.py
Normal 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
|
||||
78
slixmpp/plugins/google/nosave/nosave.py
Normal file
78
slixmpp/plugins/google/nosave/nosave.py
Normal 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)
|
||||
10
slixmpp/plugins/google/settings/__init__.py
Normal file
10
slixmpp/plugins/google/settings/__init__.py
Normal 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
|
||||
110
slixmpp/plugins/google/settings/stanza.py
Normal file
110
slixmpp/plugins/google/settings/stanza.py
Normal 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')
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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''
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
16
slixmpp/plugins/xep_0300/__init__.py
Normal file
16
slixmpp/plugins/xep_0300/__init__.py
Normal 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)
|
||||
87
slixmpp/plugins/xep_0300/hash.py
Normal file
87
slixmpp/plugins/xep_0300/hash.py
Normal 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
|
||||
# don’t 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
|
||||
35
slixmpp/plugins/xep_0300/stanza.py
Normal file
35
slixmpp/plugins/xep_0300/stanza.py
Normal 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 = ''
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
15
slixmpp/plugins/xep_0380/__init__.py
Normal file
15
slixmpp/plugins/xep_0380/__init__.py
Normal 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)
|
||||
69
slixmpp/plugins/xep_0380/eme.py
Normal file
69
slixmpp/plugins/xep_0380/eme.py
Normal 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)
|
||||
16
slixmpp/plugins/xep_0380/stanza.py
Normal file
16
slixmpp/plugins/xep_0380/stanza.py
Normal 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'}
|
||||
15
slixmpp/plugins/xep_0394/__init__.py
Normal file
15
slixmpp/plugins/xep_0394/__init__.py
Normal 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)
|
||||
161
slixmpp/plugins/xep_0394/markup.py
Normal file
161
slixmpp/plugins/xep_0394/markup.py
Normal 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("&", '&')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>')
|
||||
.replace('"', '"')
|
||||
.replace("'", ''')
|
||||
.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
|
||||
123
slixmpp/plugins/xep_0394/stanza.py
Normal file
123
slixmpp/plugins/xep_0394/stanza.py
Normal 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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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('[33;1mRECV[0m: %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 can’t 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("[36;1mSEND[0m: %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("[33;1mRECV[0m: %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
|
||||
|
||||
57
tests/test_stanza_xep_0300.py
Normal file
57
tests/test_stanza_xep_0300.py
Normal 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 aren’t used."""
|
||||
# iq = Iq()
|
||||
# iq['type'] = 'set'
|
||||
# try:
|
||||
# iq['hash']['algo'] = 'sha-1'
|
||||
# except ValueError:
|
||||
# pass
|
||||
# else:
|
||||
# raise self.failureException
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(TestHash)
|
||||
37
tests/test_stanza_xep_0380.py
Normal file
37
tests/test_stanza_xep_0380.py
Normal 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)
|
||||
Reference in New Issue
Block a user