mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-27 11:09:56 +00:00
moved seesmic branch to trunk
This commit is contained in:
commit
96b103b275
44 changed files with 4041 additions and 0 deletions
39
MANIFEST
Normal file
39
MANIFEST
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
setup.py
|
||||||
|
sleekxmpp/__init__.py
|
||||||
|
sleekxmpp/basexmpp.py
|
||||||
|
sleekxmpp/clientxmpp.py
|
||||||
|
sleekxmpp/example.py
|
||||||
|
sleekxmpp/plugins/__init__.py
|
||||||
|
sleekxmpp/plugins/base.py
|
||||||
|
sleekxmpp/plugins/gmail_notify.py
|
||||||
|
sleekxmpp/plugins/xep_0004.py
|
||||||
|
sleekxmpp/plugins/xep_0009.py
|
||||||
|
sleekxmpp/plugins/xep_0030.py
|
||||||
|
sleekxmpp/plugins/xep_0045.py
|
||||||
|
sleekxmpp/plugins/xep_0050.py
|
||||||
|
sleekxmpp/plugins/xep_0060.py
|
||||||
|
sleekxmpp/plugins/xep_0078.py
|
||||||
|
sleekxmpp/plugins/xep_0086.py
|
||||||
|
sleekxmpp/plugins/xep_0092.py
|
||||||
|
sleekxmpp/plugins/xep_0199.py
|
||||||
|
sleekxmpp/stanza/__init__.py
|
||||||
|
sleekxmpp/stanza/iq.py
|
||||||
|
sleekxmpp/stanza/message.py
|
||||||
|
sleekxmpp/stanza/presence.py
|
||||||
|
sleekxmpp/xmlstream/__init__.py
|
||||||
|
sleekxmpp/xmlstream/stanzabase.py
|
||||||
|
sleekxmpp/xmlstream/statemachine.py
|
||||||
|
sleekxmpp/xmlstream/test.py
|
||||||
|
sleekxmpp/xmlstream/testclient.py
|
||||||
|
sleekxmpp/xmlstream/xmlstream.py
|
||||||
|
sleekxmpp/xmlstream/handler/__init__.py
|
||||||
|
sleekxmpp/xmlstream/handler/base.py
|
||||||
|
sleekxmpp/xmlstream/handler/callback.py
|
||||||
|
sleekxmpp/xmlstream/handler/waiter.py
|
||||||
|
sleekxmpp/xmlstream/handler/xmlcallback.py
|
||||||
|
sleekxmpp/xmlstream/handler/xmlwaiter.py
|
||||||
|
sleekxmpp/xmlstream/matcher/__init__.py
|
||||||
|
sleekxmpp/xmlstream/matcher/base.py
|
||||||
|
sleekxmpp/xmlstream/matcher/many.py
|
||||||
|
sleekxmpp/xmlstream/matcher/xmlmask.py
|
||||||
|
sleekxmpp/xmlstream/matcher/xpath.py
|
40
example.py
Normal file
40
example.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import sleekxmpp.clientxmpp
|
||||||
|
import logging
|
||||||
|
from optparse import OptionParser
|
||||||
|
import time
|
||||||
|
|
||||||
|
class Example(sleekxmpp.clientxmpp.ClientXMPP):
|
||||||
|
|
||||||
|
def __init__(self, jid, password):
|
||||||
|
sleekxmpp.clientxmpp.ClientXMPP.__init__(self, jid, password)
|
||||||
|
self.add_event_handler("session_start", self.start)
|
||||||
|
self.add_event_handler("message", self.message)
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
self.getRoster()
|
||||||
|
self.sendPresence()
|
||||||
|
|
||||||
|
def message(self, event):
|
||||||
|
print("******got a message")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
#parse command line arguements
|
||||||
|
optp = OptionParser()
|
||||||
|
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
|
||||||
|
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
|
||||||
|
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
|
||||||
|
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
|
||||||
|
opts,args = optp.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||||
|
xmpp = Example('user@gmail.com/sleekxmpp', 'password')
|
||||||
|
xmpp.registerPlugin('xep_0004')
|
||||||
|
xmpp.registerPlugin('xep_0030')
|
||||||
|
xmpp.registerPlugin('xep_0060')
|
||||||
|
xmpp.registerPlugin('xep_0199')
|
||||||
|
if xmpp.connect(('talk.google.com', 5222)):
|
||||||
|
xmpp.process(threaded=False)
|
||||||
|
print("done")
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
233
ez_setup.py
Normal file
233
ez_setup.py
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
#!python
|
||||||
|
"""Bootstrap setuptools installation
|
||||||
|
|
||||||
|
If you want to use setuptools in your package's setup.py, just include this
|
||||||
|
file in the same directory with it, and add this to the top of your setup.py::
|
||||||
|
|
||||||
|
from ez_setup import use_setuptools
|
||||||
|
use_setuptools()
|
||||||
|
|
||||||
|
If you want to require a specific version of setuptools, set a download
|
||||||
|
mirror, or use an alternate download directory, you can do so by supplying
|
||||||
|
the appropriate options to ``use_setuptools()``.
|
||||||
|
|
||||||
|
This file can also be run as a script to install or upgrade setuptools.
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
DEFAULT_VERSION = "0.6c7"
|
||||||
|
DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
|
||||||
|
|
||||||
|
md5_data = {
|
||||||
|
'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
|
||||||
|
'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
|
||||||
|
'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
|
||||||
|
'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
|
||||||
|
'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
|
||||||
|
'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
|
||||||
|
'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
|
||||||
|
'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
|
||||||
|
'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
|
||||||
|
'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
|
||||||
|
'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
|
||||||
|
'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
|
||||||
|
'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
|
||||||
|
'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
|
||||||
|
'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
|
||||||
|
'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
|
||||||
|
'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
|
||||||
|
'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
|
||||||
|
'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
|
||||||
|
'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
|
||||||
|
'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
|
||||||
|
'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
|
||||||
|
'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
|
||||||
|
'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
|
||||||
|
}
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
def _validate_md5(egg_name, data):
|
||||||
|
if egg_name in md5_data:
|
||||||
|
from md5 import md5
|
||||||
|
digest = md5(data).hexdigest()
|
||||||
|
if digest != md5_data[egg_name]:
|
||||||
|
print >>sys.stderr, (
|
||||||
|
"md5 validation of %s failed! (Possible download problem?)"
|
||||||
|
% egg_name
|
||||||
|
)
|
||||||
|
sys.exit(2)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def use_setuptools(
|
||||||
|
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, min_version=None,
|
||||||
|
download_delay=15
|
||||||
|
):
|
||||||
|
"""Automatically find/download setuptools and make it available on sys.path
|
||||||
|
|
||||||
|
`version` should be a valid setuptools version number that is available
|
||||||
|
as an egg for download under the `download_base` URL (which should end with
|
||||||
|
a '/'). `to_dir` is the directory where setuptools will be downloaded, if
|
||||||
|
it is not already available. If `download_delay` is specified, it should
|
||||||
|
be the number of seconds that will be paused before initiating a download,
|
||||||
|
should one be required. If an older version of setuptools is installed,
|
||||||
|
this routine will print a message to ``sys.stderr`` and raise SystemExit in
|
||||||
|
an attempt to abort the calling script.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import setuptools
|
||||||
|
if setuptools.__version__ == '0.0.1':
|
||||||
|
print >>sys.stderr, (
|
||||||
|
"You have an obsolete version of setuptools installed. Please\n"
|
||||||
|
"remove it from your system entirely before rerunning this script."
|
||||||
|
)
|
||||||
|
sys.exit(2)
|
||||||
|
except ImportError:
|
||||||
|
egg = download_setuptools(version, download_base, to_dir, download_delay)
|
||||||
|
sys.path.insert(0, egg)
|
||||||
|
import setuptools; setuptools.bootstrap_install_from = egg
|
||||||
|
|
||||||
|
import pkg_resources
|
||||||
|
try:
|
||||||
|
if not min_version:
|
||||||
|
min_version = version
|
||||||
|
pkg_resources.require("setuptools>="+min_version)
|
||||||
|
|
||||||
|
except pkg_resources.VersionConflict, e:
|
||||||
|
# XXX could we install in a subprocess here?
|
||||||
|
print >>sys.stderr, (
|
||||||
|
"The required version of setuptools (>=%s) is not available, and\n"
|
||||||
|
"can't be installed while this script is running. Please install\n"
|
||||||
|
" a more recent version first.\n\n(Currently using %r)"
|
||||||
|
) % (min_version, e.args[0])
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
def download_setuptools(
|
||||||
|
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
|
||||||
|
delay = 15
|
||||||
|
):
|
||||||
|
"""Download setuptools from a specified location and return its filename
|
||||||
|
|
||||||
|
`version` should be a valid setuptools version number that is available
|
||||||
|
as an egg for download under the `download_base` URL (which should end
|
||||||
|
with a '/'). `to_dir` is the directory where the egg will be downloaded.
|
||||||
|
`delay` is the number of seconds to pause before an actual download attempt.
|
||||||
|
"""
|
||||||
|
import urllib2, shutil
|
||||||
|
egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
|
||||||
|
url = download_base + egg_name
|
||||||
|
saveto = os.path.join(to_dir, egg_name)
|
||||||
|
src = dst = None
|
||||||
|
if not os.path.exists(saveto): # Avoid repeated downloads
|
||||||
|
try:
|
||||||
|
from distutils import log
|
||||||
|
if delay:
|
||||||
|
log.warn("""
|
||||||
|
---------------------------------------------------------------------------
|
||||||
|
This script requires setuptools version %s to run (even to display
|
||||||
|
help). I will attempt to download it for you (from
|
||||||
|
%s), but
|
||||||
|
you may need to enable firewall access for this script first.
|
||||||
|
I will start the download in %d seconds.
|
||||||
|
|
||||||
|
(Note: if this machine does not have network access, please obtain the file
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
and place it in this directory before rerunning this script.)
|
||||||
|
---------------------------------------------------------------------------""",
|
||||||
|
version, download_base, delay, url
|
||||||
|
); from time import sleep; sleep(delay)
|
||||||
|
log.warn("Downloading %s", url)
|
||||||
|
src = urllib2.urlopen(url)
|
||||||
|
# Read/write all in one block, so we don't create a corrupt file
|
||||||
|
# if the download is interrupted.
|
||||||
|
data = _validate_md5(egg_name, src.read())
|
||||||
|
dst = open(saveto,"wb"); dst.write(data)
|
||||||
|
finally:
|
||||||
|
if src: src.close()
|
||||||
|
if dst: dst.close()
|
||||||
|
return os.path.realpath(saveto)
|
||||||
|
|
||||||
|
def main(argv, version=DEFAULT_VERSION):
|
||||||
|
"""Install or upgrade setuptools and EasyInstall"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import setuptools
|
||||||
|
except ImportError:
|
||||||
|
egg = None
|
||||||
|
try:
|
||||||
|
egg = download_setuptools(version, delay=0)
|
||||||
|
sys.path.insert(0,egg)
|
||||||
|
from setuptools.command.easy_install import main
|
||||||
|
return main(list(argv)+[egg]) # we're done here
|
||||||
|
finally:
|
||||||
|
if egg and os.path.exists(egg):
|
||||||
|
os.unlink(egg)
|
||||||
|
else:
|
||||||
|
if setuptools.__version__ == '0.0.1':
|
||||||
|
# tell the user to uninstall obsolete version
|
||||||
|
use_setuptools(version)
|
||||||
|
|
||||||
|
req = "setuptools>="+version
|
||||||
|
import pkg_resources
|
||||||
|
try:
|
||||||
|
pkg_resources.require(req)
|
||||||
|
except pkg_resources.VersionConflict:
|
||||||
|
try:
|
||||||
|
from setuptools.command.easy_install import main
|
||||||
|
except ImportError:
|
||||||
|
from easy_install import main
|
||||||
|
main(list(argv)+[download_setuptools(delay=0)])
|
||||||
|
sys.exit(0) # try to force an exit
|
||||||
|
else:
|
||||||
|
if argv:
|
||||||
|
from setuptools.command.easy_install import main
|
||||||
|
main(argv)
|
||||||
|
else:
|
||||||
|
print "Setuptools version",version,"or greater has been installed."
|
||||||
|
print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def update_md5(filenames):
|
||||||
|
"""Update our built-in md5 registry"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from md5 import md5
|
||||||
|
|
||||||
|
for name in filenames:
|
||||||
|
base = os.path.basename(name)
|
||||||
|
f = open(name,'rb')
|
||||||
|
md5_data[base] = md5(f.read()).hexdigest()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
data = [" %r: %r,\n" % it for it in md5_data.items()]
|
||||||
|
data.sort()
|
||||||
|
repl = "".join(data)
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
srcfile = inspect.getsourcefile(sys.modules[__name__])
|
||||||
|
f = open(srcfile, 'rb'); src = f.read(); f.close()
|
||||||
|
|
||||||
|
match = re.search("\nmd5_data = {\n([^}]+)}", src)
|
||||||
|
if not match:
|
||||||
|
print >>sys.stderr, "Internal error!"
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
src = src[:match.start(1)] + repl + src[match.end(1):]
|
||||||
|
f = open(srcfile,'w')
|
||||||
|
f.write(src)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
if len(sys.argv)>2 and sys.argv[1]=='--md5update':
|
||||||
|
update_md5(sys.argv[2:])
|
||||||
|
else:
|
||||||
|
main(sys.argv[1:])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
58
setup.py
Normal file
58
setup.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Copyright (C) 2007-2008 Nathanael C. Fritz
|
||||||
|
# All Rights Reserved
|
||||||
|
#
|
||||||
|
# This software is licensed as described in the README file,
|
||||||
|
# which you should have received as part of this distribution.
|
||||||
|
#
|
||||||
|
|
||||||
|
# from ez_setup import use_setuptools
|
||||||
|
from distutils.core import setup
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# if 'cygwin' in sys.platform.lower():
|
||||||
|
# min_version = '0.6c6'
|
||||||
|
# else:
|
||||||
|
# min_version = '0.6a9'
|
||||||
|
#
|
||||||
|
# try:
|
||||||
|
# use_setuptools(min_version=min_version)
|
||||||
|
# except TypeError:
|
||||||
|
# # locally installed ez_setup won't have min_version
|
||||||
|
# use_setuptools()
|
||||||
|
#
|
||||||
|
# from setuptools import setup, find_packages, Extension, Feature
|
||||||
|
|
||||||
|
VERSION = '0.2.3.1'
|
||||||
|
DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).'
|
||||||
|
LONG_DESCRIPTION = """
|
||||||
|
SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).
|
||||||
|
"""
|
||||||
|
|
||||||
|
CLASSIFIERS = [ 'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: GPL v2.0',
|
||||||
|
'Programming Language :: Python',
|
||||||
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name = "sleekxmpp",
|
||||||
|
version = VERSION,
|
||||||
|
description = DESCRIPTION,
|
||||||
|
long_description = LONG_DESCRIPTION,
|
||||||
|
author = 'Nathanael Fritz',
|
||||||
|
author_email = 'fritzy [at] netflint.net',
|
||||||
|
url = 'http://code.google.com/p/sleekxmpp',
|
||||||
|
license = 'GPLv2',
|
||||||
|
platforms = [ 'any' ],
|
||||||
|
packages = [ 'sleekxmpp',
|
||||||
|
'sleekxmpp/plugins',
|
||||||
|
'sleekxmpp/stanza',
|
||||||
|
'sleekxmpp/xmlstream',
|
||||||
|
'sleekxmpp/xmlstream/matcher',
|
||||||
|
'sleekxmpp/xmlstream/handler' ],
|
||||||
|
requires = [ 'tlslite', 'pythondns' ],
|
||||||
|
)
|
||||||
|
|
270
sleekxmpp/__init__.py
Normal file
270
sleekxmpp/__init__.py
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
#!/usr/bin/python2.5
|
||||||
|
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from . basexmpp import basexmpp
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
from . xmlstream.xmlstream import XMLStream
|
||||||
|
from . xmlstream.xmlstream import RestartStream
|
||||||
|
from . xmlstream.matcher.xmlmask import MatchXMLMask
|
||||||
|
from . xmlstream.matcher.xpath import MatchXPath
|
||||||
|
from . xmlstream.matcher.many import MatchMany
|
||||||
|
from . xmlstream.handler.callback import Callback
|
||||||
|
from . xmlstream.stanzabase import StanzaBase
|
||||||
|
from . xmlstream import xmlstream as xmlstreammod
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import base64
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
import copy
|
||||||
|
from . import plugins
|
||||||
|
from . import stanza
|
||||||
|
srvsupport = True
|
||||||
|
try:
|
||||||
|
import dns.resolver
|
||||||
|
except ImportError:
|
||||||
|
srvsupport = False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#class PresenceStanzaType(object):
|
||||||
|
#
|
||||||
|
# def fromXML(self, xml):
|
||||||
|
# self.ptype = xml.get('type')
|
||||||
|
|
||||||
|
|
||||||
|
class ClientXMPP(basexmpp, XMLStream):
|
||||||
|
"""SleekXMPP's client class. Use only for good, not evil."""
|
||||||
|
|
||||||
|
def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True):
|
||||||
|
global srvsupport
|
||||||
|
XMLStream.__init__(self)
|
||||||
|
self.default_ns = 'jabber:client'
|
||||||
|
basexmpp.__init__(self)
|
||||||
|
self.plugin_config = plugin_config
|
||||||
|
self.escape_quotes = escape_quotes
|
||||||
|
self.set_jid(jid)
|
||||||
|
self.plugin_whitelist = plugin_whitelist
|
||||||
|
self.auto_reconnect = True
|
||||||
|
self.srvsupport = srvsupport
|
||||||
|
self.password = password
|
||||||
|
self.registered_features = []
|
||||||
|
self.stream_header = """<stream:stream to='%s' xmlns:stream='http://etherx.jabber.org/streams' xmlns='%s' version='1.0'>""" % (self.server,self.default_ns)
|
||||||
|
self.stream_footer = "</stream:stream>"
|
||||||
|
#self.map_namespace('http://etherx.jabber.org/streams', 'stream')
|
||||||
|
#self.map_namespace('jabber:client', '')
|
||||||
|
self.features = []
|
||||||
|
self.authenticated = False
|
||||||
|
self.sessionstarted = False
|
||||||
|
self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True))
|
||||||
|
self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, thread=True))
|
||||||
|
self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True))
|
||||||
|
self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True)
|
||||||
|
self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True)
|
||||||
|
self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource)
|
||||||
|
self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session)
|
||||||
|
|
||||||
|
#self.registerStanzaExtension('PresenceStanza', PresenceStanzaType)
|
||||||
|
#self.register_plugins()
|
||||||
|
|
||||||
|
def importStanzas(self):
|
||||||
|
for modname in stanza.__all__:
|
||||||
|
__import__("%s.%s" % (globals()['stanza'].__name__, modname))
|
||||||
|
for register in getattr(stanza, modname).stanzas:
|
||||||
|
self.registerStanza(**register)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if self.plugin.has_key(key):
|
||||||
|
return self.plugin[key]
|
||||||
|
else:
|
||||||
|
logging.warning("""Plugin "%s" is not loaded.""" % key)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get(self, key, default):
|
||||||
|
return self.plugin.get(key, default)
|
||||||
|
|
||||||
|
def connect(self, address=tuple()):
|
||||||
|
"""Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses
|
||||||
|
the JID server."""
|
||||||
|
self.importStanzas()
|
||||||
|
if not address or len(address) < 2:
|
||||||
|
if not self.srvsupport:
|
||||||
|
logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using server hostname from JID.")
|
||||||
|
else:
|
||||||
|
logging.debug("Since no address is supplied, attempting SRV lookup.")
|
||||||
|
try:
|
||||||
|
answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.server, "SRV")
|
||||||
|
except dns.resolver.NXDOMAIN:
|
||||||
|
logging.debug("No appropriate SRV record found. Using JID server name.")
|
||||||
|
else:
|
||||||
|
# pick a random answer, weighted by priority
|
||||||
|
# there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway
|
||||||
|
# suggestions are welcome
|
||||||
|
addresses = {}
|
||||||
|
intmax = 0
|
||||||
|
priorities = []
|
||||||
|
for answer in answers:
|
||||||
|
intmax += answer.priority
|
||||||
|
addresses[intmax] = (answer.target.to_text()[:-1], answer.port)
|
||||||
|
priorities.append(intmax) # sure, I could just do priorities = addresses.keys()\n priorities.sort()
|
||||||
|
picked = random.randint(0, intmax)
|
||||||
|
for priority in priorities:
|
||||||
|
if picked <= priority:
|
||||||
|
address = addresses[priority]
|
||||||
|
break
|
||||||
|
if not address:
|
||||||
|
# if all else fails take server from JID.
|
||||||
|
address = (self.server, 5222)
|
||||||
|
result = XMLStream.connect(self, address[0], address[1], use_tls=True)
|
||||||
|
if result:
|
||||||
|
self.event("connected")
|
||||||
|
else:
|
||||||
|
print "** Failed to connect -- disconnected"
|
||||||
|
self.event("disconnected")
|
||||||
|
return result
|
||||||
|
|
||||||
|
# overriding reconnect and disconnect so that we can get some events
|
||||||
|
# should events be part of or required by xmlstream? Maybe that would be cleaner
|
||||||
|
def reconnect(self):
|
||||||
|
print "** Reconnect -- disconnected"
|
||||||
|
self.event("disconnected")
|
||||||
|
XMLStream.reconnect(self)
|
||||||
|
|
||||||
|
def disconnect(self, init=True, close=False, reconnect=False):
|
||||||
|
print "** Called -- disconnected"
|
||||||
|
# raise TypeError
|
||||||
|
self.event("disconnected")
|
||||||
|
XMLStream.disconnect(self, reconnect)
|
||||||
|
|
||||||
|
def registerFeature(self, mask, pointer, breaker = False):
|
||||||
|
"""Register a stream feature."""
|
||||||
|
self.registered_features.append((MatchXMLMask(mask), pointer, breaker))
|
||||||
|
|
||||||
|
def updateRoster(self, jid, name=None, subscription=None, groups=[]):
|
||||||
|
"""Add or change a roster item."""
|
||||||
|
iq = self.makeIqSet()
|
||||||
|
iq.attrib['from'] = self.fulljid
|
||||||
|
query = self.makeQueryRoster(iq)
|
||||||
|
item = ET.Element('item')
|
||||||
|
item.attrib['jid'] = jid
|
||||||
|
if name:
|
||||||
|
item.attrib['name'] = name
|
||||||
|
if subscription in ['to', 'from', 'both']:
|
||||||
|
item.attrib['subscription'] = subscription
|
||||||
|
else:
|
||||||
|
item.attrib['subscription'] = 'none'
|
||||||
|
for group in groups:
|
||||||
|
groupxml = ET.Element('group')
|
||||||
|
groupxml.text = group
|
||||||
|
item.append.groupxml
|
||||||
|
return self.send(iq, self.makeIq(self.getId()))
|
||||||
|
|
||||||
|
def getRoster(self):
|
||||||
|
"""Request the roster be sent."""
|
||||||
|
self.send(self.makeIqGet('jabber:iq:roster'))
|
||||||
|
|
||||||
|
def _handleStreamFeatures(self, features):
|
||||||
|
for subelement in features.xml:
|
||||||
|
for feature in self.registered_features:
|
||||||
|
if feature[0].match(subelement):
|
||||||
|
#if self.maskcmp(subelement, feature[0], True):
|
||||||
|
if feature[1](subelement) and feature[2]: #if breaker, don't continue
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handler_starttls(self, xml):
|
||||||
|
if self.ssl_support:
|
||||||
|
self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start)
|
||||||
|
self.send(xml)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.warning("The module tlslite is required in to some servers, and has not been found.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def handler_tls_start(self, xml):
|
||||||
|
logging.debug("Starting TLS")
|
||||||
|
if self.startTLS():
|
||||||
|
raise RestartStream()
|
||||||
|
|
||||||
|
def handler_sasl_auth(self, xml):
|
||||||
|
logging.debug("Starting SASL Auth")
|
||||||
|
self.add_handler("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_success)
|
||||||
|
self.add_handler("<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_fail)
|
||||||
|
sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism')
|
||||||
|
if len(sasl_mechs):
|
||||||
|
for sasl_mech in sasl_mechs:
|
||||||
|
self.features.append("sasl:%s" % sasl_mech.text)
|
||||||
|
if 'sasl:PLAIN' in self.features:
|
||||||
|
self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % str(base64.b64encode('\x00' + self.username + '\x00' + self.password)))
|
||||||
|
else:
|
||||||
|
logging.error("No appropriate login method.")
|
||||||
|
self.disconnect()
|
||||||
|
#if 'sasl:DIGEST-MD5' in self.features:
|
||||||
|
# self._auth_digestmd5()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handler_auth_success(self, xml):
|
||||||
|
self.authenticated = True
|
||||||
|
self.features = []
|
||||||
|
raise RestartStream()
|
||||||
|
|
||||||
|
def handler_auth_fail(self, xml):
|
||||||
|
logging.info("Authentication failed.")
|
||||||
|
self.disconnect()
|
||||||
|
self.event("failed_auth")
|
||||||
|
|
||||||
|
def handler_bind_resource(self, xml):
|
||||||
|
logging.debug("Requesting resource: %s" % self.resource)
|
||||||
|
out = self.makeIqSet()
|
||||||
|
res = ET.Element('resource')
|
||||||
|
res.text = self.resource
|
||||||
|
xml.append(res)
|
||||||
|
out.append(xml)
|
||||||
|
id = out.get('id')
|
||||||
|
response = self.send(out, self.makeIqResult(id))
|
||||||
|
self.set_jid(response.find('{urn:ietf:params:xml:ns:xmpp-bind}bind/{urn:ietf:params:xml:ns:xmpp-bind}jid').text)
|
||||||
|
logging.info("Node set to: %s" % self.fulljid)
|
||||||
|
|
||||||
|
def handler_start_session(self, xml):
|
||||||
|
if self.authenticated:
|
||||||
|
response = self.send(self.makeIqSet(xml), self.makeIq(self.getId()))
|
||||||
|
logging.debug("Established Session")
|
||||||
|
self.sessionstarted = True
|
||||||
|
self.event("session_start")
|
||||||
|
|
||||||
|
def _handleRoster(self, roster):
|
||||||
|
xml = roster.xml
|
||||||
|
xml = roster.xml
|
||||||
|
roster_update = {}
|
||||||
|
for item in xml.findall('{jabber:iq:roster}query/{jabber:iq:roster}item'):
|
||||||
|
if not item.attrib['jid'] in self.roster:
|
||||||
|
self.roster[item.attrib['jid']] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': False}
|
||||||
|
self.roster[item.attrib['jid']]['name'] = item.get('name', '')
|
||||||
|
self.roster[item.attrib['jid']]['subscription'] = item.get('subscription', 'none')
|
||||||
|
self.roster[item.attrib['jid']]['in_roster'] = 'True'
|
||||||
|
for group in item.findall('{jabber:iq:roster}group'):
|
||||||
|
self.roster[item.attrib['jid']]['groups'].append(group.text)
|
||||||
|
if self.roster[item.attrib['jid']]['groups'] == []:
|
||||||
|
self.roster[item.attrib['jid']]['groups'].append('Default')
|
||||||
|
roster_update[item.attrib['jid']] = self.roster[item.attrib['jid']]
|
||||||
|
if xml.get('type', 'result') == 'set':
|
||||||
|
self.send(self.makeIqResult(xml.get('id', '0')))
|
||||||
|
self.event("roster_update", roster_update)
|
422
sleekxmpp/basexmpp.py
Normal file
422
sleekxmpp/basexmpp.py
Normal file
|
@ -0,0 +1,422 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from __future__ import with_statement
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
from . xmlstream.xmlstream import XMLStream
|
||||||
|
from . xmlstream.matcher.xmlmask import MatchXMLMask
|
||||||
|
from . xmlstream.matcher.many import MatchMany
|
||||||
|
from . xmlstream.handler.xmlcallback import XMLCallback
|
||||||
|
from . xmlstream.handler.xmlwaiter import XMLWaiter
|
||||||
|
from . xmlstream.handler.callback import Callback
|
||||||
|
from . import plugins
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class basexmpp(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.id = 0
|
||||||
|
self.id_lock = threading.Lock()
|
||||||
|
self.stanza_errors = {
|
||||||
|
'bad-request':False,
|
||||||
|
'conflict':False,
|
||||||
|
'feature-not-implemented':False,
|
||||||
|
'forbidden':False,
|
||||||
|
'gone':True,
|
||||||
|
'internal-server-error':False,
|
||||||
|
'item-not-found':False,
|
||||||
|
'jid-malformed':False,
|
||||||
|
'not-acceptable':False,
|
||||||
|
'not-allowed':False,
|
||||||
|
'payment-required':False,
|
||||||
|
'recipient-unavailable':False,
|
||||||
|
'redirect':True,
|
||||||
|
'registration-required':False,
|
||||||
|
'remote-server-not-found':False,
|
||||||
|
'remote-server-timeout':False,
|
||||||
|
'resource-constraint':False,
|
||||||
|
'service-unavailable':False,
|
||||||
|
'subscription-required':False,
|
||||||
|
'undefined-condition':False,
|
||||||
|
'unexpected-request':False}
|
||||||
|
self.stream_errors = {
|
||||||
|
'bad-format':False,
|
||||||
|
'bad-namespace-prefix':False,
|
||||||
|
'conflict':False,
|
||||||
|
'connection-timeout':False,
|
||||||
|
'host-gone':False,
|
||||||
|
'host-unknown':False,
|
||||||
|
'improper-addressing':False,
|
||||||
|
'internal-server-error':False,
|
||||||
|
'invalid-from':False,
|
||||||
|
'invalid-id':False,
|
||||||
|
'invalid-namespace':False,
|
||||||
|
'invalid-xml':False,
|
||||||
|
'not-authorized':False,
|
||||||
|
'policy-violation':False,
|
||||||
|
'remote-connection-failed':False,
|
||||||
|
'resource-constraint':False,
|
||||||
|
'restricted-xml':False,
|
||||||
|
'see-other-host':True,
|
||||||
|
'system-shutdown':False,
|
||||||
|
'undefined-condition':False,
|
||||||
|
'unsupported-encoding':False,
|
||||||
|
'unsupported-stanza-type':False,
|
||||||
|
'unsupported-version':False,
|
||||||
|
'xml-not-well-formed':False}
|
||||||
|
self.sentpresence = False
|
||||||
|
self.fulljid = ''
|
||||||
|
self.resource = ''
|
||||||
|
self.jid = ''
|
||||||
|
self.username = ''
|
||||||
|
self.server = ''
|
||||||
|
self.plugin = {}
|
||||||
|
self.auto_authorize = True
|
||||||
|
self.auto_subscribe = True
|
||||||
|
self.event_handlers = {}
|
||||||
|
self.roster = {}
|
||||||
|
self.registerHandler(Callback('IM', MatchMany((MatchXMLMask("<message xmlns='%s' type='chat'><body /></message>" % self.default_ns),MatchXMLMask("<message xmlns='%s' type='normal'><body /></message>" % self.default_ns),MatchXMLMask("<message xmlns='%s' type='__None__'><body /></message>" % self.default_ns))), self._handleMessage))
|
||||||
|
self.registerHandler(Callback('Presence', MatchMany((MatchXMLMask("<presence xmlns='%s' type='available'/>" % self.default_ns),MatchXMLMask("<presence xmlns='%s' type='__None__'/>" % self.default_ns),MatchXMLMask("<presence xmlns='%s' type='unavailable'/>" % self.default_ns))), self._handlePresence))
|
||||||
|
self.registerHandler(Callback('PresenceSubscribe', MatchMany((MatchXMLMask("<presence xmlns='%s' type='subscribe'/>" % self.default_ns),MatchXMLMask("<presence xmlns='%s' type='unsubscribed'/>" % self.default_ns))), self._handlePresenceSubscribe))
|
||||||
|
|
||||||
|
def set_jid(self, jid):
|
||||||
|
"""Rip a JID apart and claim it as our own."""
|
||||||
|
self.fulljid = jid
|
||||||
|
self.resource = self.getjidresource(jid)
|
||||||
|
self.jid = self.getjidbare(jid)
|
||||||
|
self.username = jid.split('@', 1)[0]
|
||||||
|
self.server = jid.split('@',1)[-1].split('/', 1)[0]
|
||||||
|
|
||||||
|
def registerPlugin(self, plugin, pconfig = {}):
|
||||||
|
"""Register a plugin not in plugins.__init__.__all__ but in the plugins
|
||||||
|
directory."""
|
||||||
|
# discover relative "path" to the plugins module from the main app, and import it.
|
||||||
|
__import__("%s.%s" % (globals()['plugins'].__name__, plugin))
|
||||||
|
# init the plugin class
|
||||||
|
self.plugin[plugin] = getattr(getattr(plugins, plugin), plugin)(self, pconfig) # eek
|
||||||
|
# all of this for a nice debug? sure.
|
||||||
|
xep = ''
|
||||||
|
if hasattr(self.plugin[plugin], 'xep'):
|
||||||
|
xep = "(XEP-%s) " % self.plugin[plugin].xep
|
||||||
|
logging.debug("Loaded Plugin %s%s" % (xep, self.plugin[plugin].description))
|
||||||
|
|
||||||
|
def register_plugins(self):
|
||||||
|
"""Initiates all plugins in the plugins/__init__.__all__"""
|
||||||
|
if self.plugin_whitelist:
|
||||||
|
plugin_list = self.plugin_whitelist
|
||||||
|
else:
|
||||||
|
plugin_list = plugins.__all__
|
||||||
|
for plugin in plugin_list:
|
||||||
|
if plugin in plugins.__all__:
|
||||||
|
self.registerPlugin(plugin, self.plugin_config.get(plugin, {}))
|
||||||
|
else:
|
||||||
|
raise NameError("No plugin by the name of %s listed in plugins.__all__." % plugin)
|
||||||
|
# run post_init() for cross-plugin interaction
|
||||||
|
for plugin in self.plugin:
|
||||||
|
self.plugin[plugin].post_init()
|
||||||
|
|
||||||
|
def getNewId(self):
|
||||||
|
with self.id_lock:
|
||||||
|
self.id += 1
|
||||||
|
return self.getId()
|
||||||
|
|
||||||
|
def add_handler(self, mask, pointer, disposable=False, threaded=False, filter=False):
|
||||||
|
logging.warning("Deprecated add_handler used for %s: %s." % (mask, pointer))
|
||||||
|
self.registerHandler(XMLCallback('add_handler_%s' % self.getNewId(), MatchXMLMask(mask), pointer, threaded, disposable))
|
||||||
|
|
||||||
|
def getId(self):
|
||||||
|
return "%x".upper() % self.id
|
||||||
|
|
||||||
|
def send(self, data, mask=None, timeout=60):
|
||||||
|
#logging.warning("Deprecated send used for \"%s\"" % (data,))
|
||||||
|
if not type(data) == type(''):
|
||||||
|
data = self.tostring(data)
|
||||||
|
if mask is not None:
|
||||||
|
waitfor = XMLWaiter('SendWait_%s' % self.getNewId(), MatchXMLMask(mask))
|
||||||
|
self.registerHandler(waitfor)
|
||||||
|
self.sendRaw(data)
|
||||||
|
if mask is not None:
|
||||||
|
return waitfor.wait(timeout)
|
||||||
|
|
||||||
|
def makeIq(self, id=0, ifrom=None):
|
||||||
|
iq = ET.Element('{%s}iq' % self.default_ns)
|
||||||
|
if id == 0:
|
||||||
|
id = self.getNewId()
|
||||||
|
iq.set('id', str(id))
|
||||||
|
if ifrom is not None:
|
||||||
|
iq.attrib['from'] = ifrom
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def makeIqGet(self, queryxmlns = None):
|
||||||
|
iq = self.makeIq()
|
||||||
|
iq.set('type', 'get')
|
||||||
|
if queryxmlns:
|
||||||
|
query = ET.Element("{%s}query" % queryxmlns)
|
||||||
|
iq.append(query)
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def makeIqResult(self, id):
|
||||||
|
iq = self.makeIq(id)
|
||||||
|
iq.set('type', 'result')
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def makeIqSet(self, sub=None):
|
||||||
|
iq = self.makeIq()
|
||||||
|
iq.set('type', 'set')
|
||||||
|
if sub != None:
|
||||||
|
iq.append(sub)
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def makeIqError(self, id):
|
||||||
|
iq = self.makeIq(id)
|
||||||
|
iq.set('type', 'error')
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def makeStanzaErrorCondition(self, condition, cdata=None):
|
||||||
|
if condition not in self.stanza_errors:
|
||||||
|
raise ValueError()
|
||||||
|
stanzaError = ET.Element('{urn:ietf:params:xml:ns:xmpp-stanzas}'+condition)
|
||||||
|
if cdata is not None:
|
||||||
|
if not self.stanza_errors[condition]:
|
||||||
|
raise ValueError()
|
||||||
|
stanzaError.text = cdata
|
||||||
|
return stanzaError
|
||||||
|
|
||||||
|
|
||||||
|
def makeStanzaError(self, condition, errorType, code=None, text=None, customElem=None):
|
||||||
|
if errorType not in ['auth', 'cancel', 'continue', 'modify', 'wait']:
|
||||||
|
raise ValueError()
|
||||||
|
error = ET.Element('error')
|
||||||
|
error.append(self.makeStanzaErrorCondition(condition))
|
||||||
|
error.set('type',errorType)
|
||||||
|
if code is not None:
|
||||||
|
error.set('code', code)
|
||||||
|
if text is not None:
|
||||||
|
textElem = ET.Element('text')
|
||||||
|
textElem.text = text
|
||||||
|
error.append(textElem)
|
||||||
|
if customElem is not None:
|
||||||
|
error.append(customElem)
|
||||||
|
return error
|
||||||
|
|
||||||
|
def makeStreamErrorCondition(self, condition, cdata=None):
|
||||||
|
if condition not in self.stream_errors:
|
||||||
|
raise ValueError()
|
||||||
|
streamError = ET.Element('{urn:ietf:params:xml:ns:xmpp-streams}'+condition)
|
||||||
|
if cdata is not None:
|
||||||
|
if not self.stream_errors[condition]:
|
||||||
|
raise ValueError()
|
||||||
|
textElem = ET.Element('text')
|
||||||
|
textElem.text = text
|
||||||
|
streamError.append(textElem)
|
||||||
|
|
||||||
|
def makeStreamError(self, errorElem, text=None):
|
||||||
|
error = ET.Element('error')
|
||||||
|
error.append(errorElem)
|
||||||
|
if text is not None:
|
||||||
|
textElem = ET.Element('text')
|
||||||
|
textElem.text = text
|
||||||
|
error.append(text)
|
||||||
|
return error
|
||||||
|
|
||||||
|
def makeIqQuery(self, iq, xmlns):
|
||||||
|
query = ET.Element("{%s}query" % xmlns)
|
||||||
|
iq.append(query)
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def makeQueryRoster(self, iq=None):
|
||||||
|
query = ET.Element("{jabber:iq:roster}query")
|
||||||
|
if iq:
|
||||||
|
iq.append(query)
|
||||||
|
return query
|
||||||
|
|
||||||
|
def add_event_handler(self, name, pointer, threaded=False, disposable=False):
|
||||||
|
if not name in self.event_handlers:
|
||||||
|
self.event_handlers[name] = []
|
||||||
|
self.event_handlers[name].append((pointer, threaded, disposable))
|
||||||
|
|
||||||
|
def event(self, name, eventdata = {}): # called on an event
|
||||||
|
for handler in self.event_handlers.get(name, []):
|
||||||
|
if handler[1]: #if threaded
|
||||||
|
#thread.start_new(handler[0], (eventdata,))
|
||||||
|
x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(eventdata,))
|
||||||
|
x.start()
|
||||||
|
else:
|
||||||
|
handler[0](eventdata)
|
||||||
|
if handler[2]: #disposable
|
||||||
|
with self.lock:
|
||||||
|
self.event_handlers[name].pop(self.event_handlers[name].index(handler))
|
||||||
|
|
||||||
|
def makeMessage(self, mto, mbody='', msubject=None, mtype=None, mhtml=None, mfrom=None):
|
||||||
|
message = ET.Element('{%s}message' % self.default_ns)
|
||||||
|
if mfrom is None:
|
||||||
|
message.attrib['from'] = self.fulljid
|
||||||
|
else:
|
||||||
|
message.attrib['from'] = mfrom
|
||||||
|
message.attrib['to'] = mto
|
||||||
|
if not mtype:
|
||||||
|
mtype='chat'
|
||||||
|
message.attrib['type'] = mtype
|
||||||
|
if mtype == 'none':
|
||||||
|
del message.attrib['type']
|
||||||
|
if mbody:
|
||||||
|
body = ET.Element('body')
|
||||||
|
body.text = mbody
|
||||||
|
message.append(body)
|
||||||
|
if mhtml :
|
||||||
|
html = ET.Element('{http://jabber.org/protocol/xhtml-im}html')
|
||||||
|
html_body = ET.XML('<body xmlns="http://www.w3.org/1999/xhtml">' + mhtml + '</body>')
|
||||||
|
html.append(html_body)
|
||||||
|
message.append(html)
|
||||||
|
if msubject:
|
||||||
|
subject = ET.Element('subject')
|
||||||
|
subject.text = msubject
|
||||||
|
message.append(subject)
|
||||||
|
return message
|
||||||
|
|
||||||
|
def makePresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None):
|
||||||
|
presence = ET.Element('{%s}presence' % self.default_ns)
|
||||||
|
if ptype:
|
||||||
|
presence.attrib['type'] = ptype
|
||||||
|
if pshow:
|
||||||
|
show = ET.Element('show')
|
||||||
|
show.text = pshow
|
||||||
|
presence.append(show)
|
||||||
|
if pstatus:
|
||||||
|
status = ET.Element('status')
|
||||||
|
status.text = pstatus
|
||||||
|
presence.append(status)
|
||||||
|
if ppriority:
|
||||||
|
priority = ET.Element('priority')
|
||||||
|
priority.text = str(ppriority)
|
||||||
|
presence.append(priority)
|
||||||
|
if pto:
|
||||||
|
presence.attrib['to'] = pto
|
||||||
|
if pfrom is None:
|
||||||
|
presence.attrib['from'] = self.fulljid
|
||||||
|
else:
|
||||||
|
presence.attrib['from'] = pfrom
|
||||||
|
return presence
|
||||||
|
|
||||||
|
def sendMessage(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None):
|
||||||
|
self.send(self.makeMessage(mto,mbody,msubject,mtype,mhtml,mfrom))
|
||||||
|
|
||||||
|
def sendPresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None):
|
||||||
|
self.send(self.makePresence(pshow,pstatus,ppriority,pto, pfrom=pfrom))
|
||||||
|
if not self.sentpresence:
|
||||||
|
self.event('sent_presence')
|
||||||
|
self.sentpresence = True
|
||||||
|
|
||||||
|
def sendPresenceSubscription(self, pto, pfrom=None, ptype='subscribe', pnick=None) :
|
||||||
|
presence = self.makePresence(ptype=ptype, pfrom=pfrom, pto=self.getjidbare(pto))
|
||||||
|
if pnick :
|
||||||
|
nick = ET.Element('{http://jabber.org/protocol/nick}nick')
|
||||||
|
nick.text = pnick
|
||||||
|
presence.append(nick)
|
||||||
|
self.send(presence)
|
||||||
|
|
||||||
|
def getjidresource(self, fulljid):
|
||||||
|
if '/' in fulljid:
|
||||||
|
return fulljid.split('/', 1)[-1]
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def getjidbare(self, fulljid):
|
||||||
|
return fulljid.split('/', 1)[0]
|
||||||
|
|
||||||
|
def _handleMessage(self, msg):
|
||||||
|
xml = msg.xml
|
||||||
|
mfrom = xml.attrib['from']
|
||||||
|
message = xml.find('{%s}body' % self.default_ns).text
|
||||||
|
subject = xml.find('{%s}subject' % self.default_ns)
|
||||||
|
if subject is not None:
|
||||||
|
subject = subject.text
|
||||||
|
else:
|
||||||
|
subject = ''
|
||||||
|
resource = self.getjidresource(mfrom)
|
||||||
|
mfrom = self.getjidbare(mfrom)
|
||||||
|
mtype = xml.attrib.get('type', 'normal')
|
||||||
|
name = self.roster.get('name', '')
|
||||||
|
self.event("message", {'jid': mfrom, 'resource': resource, 'name': name, 'type': mtype, 'subject': subject, 'message': message, 'to': xml.attrib.get('to', '')})
|
||||||
|
|
||||||
|
def _handlePresence(self, presence):
|
||||||
|
xml = presence.xml
|
||||||
|
"""Update roster items based on presence"""
|
||||||
|
show = xml.find('{%s}show' % self.default_ns)
|
||||||
|
status = xml.find('{%s}status' % self.default_ns)
|
||||||
|
priority = xml.find('{%s}priority' % self.default_ns)
|
||||||
|
fulljid = xml.attrib['from']
|
||||||
|
to = xml.attrib['to']
|
||||||
|
resource = self.getjidresource(fulljid)
|
||||||
|
if not resource:
|
||||||
|
resouce = None
|
||||||
|
jid = self.getjidbare(fulljid)
|
||||||
|
if type(status) == type(None) or status.text is None:
|
||||||
|
status = ''
|
||||||
|
else:
|
||||||
|
status = status.text
|
||||||
|
if type(show) == type(None):
|
||||||
|
show = 'available'
|
||||||
|
else:
|
||||||
|
show = show.text
|
||||||
|
if xml.get('type', None) == 'unavailable':
|
||||||
|
show = 'unavailable'
|
||||||
|
if type(priority) == type(None):
|
||||||
|
priority = 0
|
||||||
|
else:
|
||||||
|
priority = int(priority.text)
|
||||||
|
wasoffline = False
|
||||||
|
oldroster = self.roster.get(jid, {}).get(resource, {})
|
||||||
|
if not jid in self.roster:
|
||||||
|
self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': False}
|
||||||
|
if not resource in self.roster[jid]['presence']:
|
||||||
|
wasoffline = True
|
||||||
|
self.roster[jid]['presence'][resource] = {'show': show, 'status': status, 'priority': priority}
|
||||||
|
else:
|
||||||
|
if self.roster[jid]['presence'][resource].get('show', None) == 'unavailable':
|
||||||
|
wasoffline = True
|
||||||
|
self.roster[jid]['presence'][resource] = {'show': show, 'status': status}
|
||||||
|
if priority:
|
||||||
|
self.roster[jid]['presence'][resource]['priority'] = priority
|
||||||
|
name = self.roster[jid].get('name', '')
|
||||||
|
eventdata = {'jid': jid, 'to': to, 'resource': resource, 'name': name, 'type': show, 'priority': priority, 'message': status}
|
||||||
|
if wasoffline and show in ('available', 'away', 'xa', 'na'):
|
||||||
|
self.event("got_online", eventdata)
|
||||||
|
elif not wasoffline and show == 'unavailable':
|
||||||
|
self.event("got_offline", eventdata)
|
||||||
|
elif oldroster != self.roster.get(jid, {'presence': {}})['presence'].get(resource, {}) and show != 'unavailable':
|
||||||
|
self.event("changed_status", eventdata)
|
||||||
|
name = ''
|
||||||
|
if name:
|
||||||
|
name = "(%s) " % name
|
||||||
|
logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, show,status))
|
||||||
|
|
||||||
|
def _handlePresenceSubscribe(self, presence):
|
||||||
|
"""Handling subscriptions automatically."""
|
||||||
|
xml = presence.xml
|
||||||
|
if self.auto_authorize == True:
|
||||||
|
#self.updateRoster(self.getjidbare(xml.attrib['from']))
|
||||||
|
self.send(self.makePresence(ptype='subscribed', pto=self.getjidbare(xml.attrib['from'])))
|
||||||
|
if self.auto_subscribe:
|
||||||
|
self.send(self.makePresence(ptype='subscribe', pto=self.getjidbare(xml.attrib['from'])))
|
||||||
|
elif self.auto_authorize == False:
|
||||||
|
self.send(self.makePresence(ptype='unsubscribed', pto=self.getjidbare(xml.attrib['from'])))
|
||||||
|
elif self.auto_authorize == None:
|
||||||
|
pass
|
42
sleekxmpp/component_example.py
Normal file
42
sleekxmpp/component_example.py
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import sleekxmpp.componentxmpp
|
||||||
|
import logging
|
||||||
|
from optparse import OptionParser
|
||||||
|
import time
|
||||||
|
|
||||||
|
class Example(sleekxmpp.componentxmpp.ComponentXMPP):
|
||||||
|
|
||||||
|
def __init__(self, jid, password):
|
||||||
|
sleekxmpp.componentxmpp.ComponentXMPP.__init__(self, jid, password, 'localhost', 5060)
|
||||||
|
self.add_event_handler("session_start", self.start)
|
||||||
|
self.add_event_handler("message", self.message)
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
#self.getRoster()
|
||||||
|
#self.sendPresence(pto='admin@tigase.netflint.net/sarkozy')
|
||||||
|
self.sendPresence(pto='tigase.netflint.net')
|
||||||
|
pass
|
||||||
|
|
||||||
|
def message(self, event):
|
||||||
|
print event
|
||||||
|
self.sendMessage("%s/%s" % (event['jid'], event['resource']), "Thanks for sending me, \"%s\"." % event['message'], mtype=event['type'])
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
#parse command line arguements
|
||||||
|
optp = OptionParser()
|
||||||
|
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
|
||||||
|
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
|
||||||
|
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
|
||||||
|
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
|
||||||
|
opts,args = optp.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||||
|
xmpp = Example('component.server.tld', 'asdfasdf')
|
||||||
|
xmpp.registerPlugin('xep_0004')
|
||||||
|
xmpp.registerPlugin('xep_0030')
|
||||||
|
xmpp.registerPlugin('xep_0060')
|
||||||
|
xmpp.registerPlugin('xep_0199')
|
||||||
|
if xmpp.connect():
|
||||||
|
xmpp.process(threaded=False)
|
||||||
|
print("done")
|
||||||
|
else:
|
||||||
|
print("Unable to connect.")
|
98
sleekxmpp/componentxmpp.py
Executable file
98
sleekxmpp/componentxmpp.py
Executable file
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/python2.5
|
||||||
|
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from . basexmpp import basexmpp
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
from . xmlstream.xmlstream import XMLStream
|
||||||
|
from . xmlstream.xmlstream import RestartStream
|
||||||
|
from . xmlstream.matcher.xmlmask import MatchXMLMask
|
||||||
|
from . xmlstream.matcher.xpath import MatchXPath
|
||||||
|
from . xmlstream.matcher.many import MatchMany
|
||||||
|
from . xmlstream.handler.callback import Callback
|
||||||
|
from . xmlstream.stanzabase import StanzaBase
|
||||||
|
from . xmlstream import xmlstream as xmlstreammod
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
import base64
|
||||||
|
import sys
|
||||||
|
import random
|
||||||
|
import copy
|
||||||
|
from . import plugins
|
||||||
|
from . import stanza
|
||||||
|
import sha
|
||||||
|
srvsupport = True
|
||||||
|
try:
|
||||||
|
import dns.resolver
|
||||||
|
except ImportError:
|
||||||
|
srvsupport = False
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentXMPP(basexmpp, XMLStream):
|
||||||
|
"""SleekXMPP's client class. Use only for good, not evil."""
|
||||||
|
|
||||||
|
def __init__(self, jid, secret, host, port, plugin_config = {}, plugin_whitelist=[]):
|
||||||
|
XMLStream.__init__(self)
|
||||||
|
self.default_ns = 'jabber:component:accept'
|
||||||
|
basexmpp.__init__(self)
|
||||||
|
self.auto_authorize = None
|
||||||
|
self.stream_header = "<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='%s'>" % jid
|
||||||
|
self.stream_footer = "</stream:stream>"
|
||||||
|
self.server_host = host
|
||||||
|
self.server_port = port
|
||||||
|
self.set_jid(jid)
|
||||||
|
self.secret = secret
|
||||||
|
self.registerHandler(Callback('PresenceProbe', MatchXMLMask("<presence xmlns='%s' type='probe'/>" % self.default_ns), self._handlePresenceProbe))
|
||||||
|
self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake))
|
||||||
|
self.registerHandler(Callback('PresenceSubscription', MatchMany(\
|
||||||
|
(MatchXMLMask("<presence xmlns='%s' type='subscribe'/>" % self.default_ns), \
|
||||||
|
MatchXMLMask("<presence xmlns='%s' type='subscribed'/>" % self.default_ns), \
|
||||||
|
MatchXMLMask("<presence xmlns='%s' type='unsubscribe'/>" % self.default_ns), \
|
||||||
|
MatchXMLMask("<presence xmlns='%s' type='unsubscribed'/>" % self.default_ns) \
|
||||||
|
)), self._handlePresenceSubscription))
|
||||||
|
|
||||||
|
def _handlePresenceProbe(self, stanza):
|
||||||
|
xml = stanza.xml
|
||||||
|
self.event("got_presence_probe", ({
|
||||||
|
'from': xml.attrib['from'],
|
||||||
|
'to': xml.attrib['to']
|
||||||
|
}))
|
||||||
|
|
||||||
|
def _handlePresenceSubscription(self, presence):
|
||||||
|
xml = presence.xml
|
||||||
|
self.event("changed_subscription", {
|
||||||
|
'type' : xml.attrib['type'],
|
||||||
|
'from': xml.attrib['from'],
|
||||||
|
'to': xml.attrib['to']
|
||||||
|
})
|
||||||
|
|
||||||
|
def start_stream_handler(self, xml):
|
||||||
|
sid = xml.get('id', '')
|
||||||
|
handshake = ET.Element('{jabber:component:accept}handshake')
|
||||||
|
handshake.text = sha.new(u"%s%s" % (sid, self.secret)).hexdigest().lower()
|
||||||
|
self.send(handshake)
|
||||||
|
|
||||||
|
def _handleHandshake(self, xml):
|
||||||
|
self.event("session_start")
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
logging.debug("Connecting to %s:%s" % (self.server_host, self.server_port))
|
||||||
|
return xmlstreammod.XMLStream.connect(self, self.server_host, self.server_port)
|
20
sleekxmpp/plugins/__init__.py
Normal file
20
sleekxmpp/plugins/__init__.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
__all__ = ['xep_0004', 'xep_0030', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060']
|
35
sleekxmpp/plugins/base.py
Normal file
35
sleekxmpp/plugins/base.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
class base_plugin(object):
|
||||||
|
|
||||||
|
def __init__(self, xmpp, config):
|
||||||
|
self.xep = 'base'
|
||||||
|
self.description = 'Base Plugin'
|
||||||
|
self.xmpp = xmpp
|
||||||
|
self.config = config
|
||||||
|
self.enable = config.get('enable', True)
|
||||||
|
if self.enable:
|
||||||
|
self.plugin_init()
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
pass
|
57
sleekxmpp/plugins/gmail_notify.py
Normal file
57
sleekxmpp/plugins/gmail_notify.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from __future__ import with_statement
|
||||||
|
import base
|
||||||
|
import logging
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
|
||||||
|
class gmail_notify(base.base_plugin):
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.description = 'Google Talk Gmail Notification'
|
||||||
|
self.xmpp.add_event_handler('sent_presence', self.handler_gmailcheck, threaded=True)
|
||||||
|
self.emails = []
|
||||||
|
|
||||||
|
def handler_gmailcheck(self, payload):
|
||||||
|
#TODO XEP 30 should cache results and have getFeature
|
||||||
|
result = self.xmpp['xep_0030'].getInfo(self.xmpp.server)
|
||||||
|
features = []
|
||||||
|
for feature in result.findall('{http://jabber.org/protocol/disco#info}query/{http://jabber.org/protocol/disco#info}feature'):
|
||||||
|
features.append(feature.get('var'))
|
||||||
|
if 'google:mail:notify' in features:
|
||||||
|
logging.debug("Server supports Gmail Notify")
|
||||||
|
self.xmpp.add_handler("<iq type='set' xmlns='%s'><new-mail xmlns='google:mail:notify' /></iq>" % self.xmpp.default_ns, self.handler_notify)
|
||||||
|
self.getEmail()
|
||||||
|
|
||||||
|
def handler_notify(self, xml):
|
||||||
|
logging.info("New Gmail recieved!")
|
||||||
|
self.xmpp.event('gmail_notify')
|
||||||
|
|
||||||
|
def getEmail(self):
|
||||||
|
iq = self.xmpp.makeIqGet()
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
iq.attrib['to'] = self.xmpp.jid
|
||||||
|
self.xmpp.makeIqQuery(iq, 'google:mail:notify')
|
||||||
|
emails = self.xmpp.send(iq, self.xmpp.makeIq(self.xmpp.id))
|
||||||
|
mailbox = emails.find('{google:mail:notify}mailbox')
|
||||||
|
total = int(mailbox.get('total-matched', 0))
|
||||||
|
logging.info("%s New Gmail Messages" % total)
|
389
sleekxmpp/plugins/xep_0004.py
Normal file
389
sleekxmpp/plugins/xep_0004.py
Normal file
|
@ -0,0 +1,389 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from . import base
|
||||||
|
import logging
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import copy
|
||||||
|
#TODO support item groups and results
|
||||||
|
|
||||||
|
class xep_0004(base.base_plugin):
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0004'
|
||||||
|
self.description = 'Data Forms'
|
||||||
|
self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
self.xmpp['xep_0030'].add_feature('jabber:x:data')
|
||||||
|
|
||||||
|
def handler_message_xform(self, xml):
|
||||||
|
object = self.handle_form(xml)
|
||||||
|
self.xmpp.event("message_form", object)
|
||||||
|
|
||||||
|
def handler_presence_xform(self, xml):
|
||||||
|
object = self.handle_form(xml)
|
||||||
|
self.xmpp.event("presence_form", object)
|
||||||
|
|
||||||
|
def handle_form(self, xml):
|
||||||
|
xmlform = xml.find('{jabber:x:data}x')
|
||||||
|
object = self.buildForm(xmlform)
|
||||||
|
self.xmpp.event("message_xform", object)
|
||||||
|
return object
|
||||||
|
|
||||||
|
def buildForm(self, xml):
|
||||||
|
form = Form(xml.attrib['type'])
|
||||||
|
form.fromXML(xml)
|
||||||
|
return form
|
||||||
|
|
||||||
|
def makeForm(self, ftype='form', title='', instructions=''):
|
||||||
|
return Form(self.xmpp, ftype, title, instructions)
|
||||||
|
|
||||||
|
class FieldContainer(object):
|
||||||
|
def __init__(self, stanza = 'form'):
|
||||||
|
self.fields = []
|
||||||
|
self.field = {}
|
||||||
|
self.stanza = stanza
|
||||||
|
|
||||||
|
def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None):
|
||||||
|
self.field[var] = FormField(var, ftype, label, desc, required, value)
|
||||||
|
self.fields.append(self.field[var])
|
||||||
|
return self.field[var]
|
||||||
|
|
||||||
|
def buildField(self, xml):
|
||||||
|
self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single'))
|
||||||
|
self.fields.append(self.field[xml.get('var', '__unnamed__')])
|
||||||
|
self.field[xml.get('var', '__unnamed__')].buildField(xml)
|
||||||
|
|
||||||
|
def buildContainer(self, xml):
|
||||||
|
self.stanza = xml.tag
|
||||||
|
for field in xml.findall('{jabber:x:data}field'):
|
||||||
|
self.buildField(field)
|
||||||
|
|
||||||
|
def getXML(self, ftype):
|
||||||
|
container = ET.Element(self.stanza)
|
||||||
|
for field in self.fields:
|
||||||
|
container.append(field.getXML(ftype))
|
||||||
|
return container
|
||||||
|
|
||||||
|
class Form(FieldContainer):
|
||||||
|
types = ('form', 'submit', 'cancel', 'result')
|
||||||
|
def __init__(self, xmpp=None, ftype='form', title='', instructions=''):
|
||||||
|
if not ftype in self.types:
|
||||||
|
raise ValueError("Invalid Form Type")
|
||||||
|
FieldContainer.__init__(self)
|
||||||
|
self.xmpp = xmpp
|
||||||
|
self.type = ftype
|
||||||
|
self.title = title
|
||||||
|
self.instructions = instructions
|
||||||
|
self.reported = []
|
||||||
|
self.items = []
|
||||||
|
|
||||||
|
def getValues(self):
|
||||||
|
result = {}
|
||||||
|
for field in self.fields:
|
||||||
|
value = field.value
|
||||||
|
if len(value) == 1:
|
||||||
|
value = value[0]
|
||||||
|
result[field.var] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
def fromXML(self, xml):
|
||||||
|
self.buildForm(xml)
|
||||||
|
|
||||||
|
def addItem(self):
|
||||||
|
newitem = FieldContainer('item')
|
||||||
|
self.items.append(newitem)
|
||||||
|
return newitem
|
||||||
|
|
||||||
|
def buildItem(self, xml):
|
||||||
|
newitem = self.addItem()
|
||||||
|
newitem.buildContainer(xml)
|
||||||
|
|
||||||
|
def addReported(self):
|
||||||
|
reported = FieldContainer('reported')
|
||||||
|
self.reported.append(reported)
|
||||||
|
return reported
|
||||||
|
|
||||||
|
def buildReported(self, xml):
|
||||||
|
reported = self.addReported()
|
||||||
|
reported.buildContainer(xml)
|
||||||
|
|
||||||
|
def setTitle(self, title):
|
||||||
|
self.title = title
|
||||||
|
|
||||||
|
def setInstructions(self, instructions):
|
||||||
|
self.instructions = instructions
|
||||||
|
|
||||||
|
def setType(self, ftype):
|
||||||
|
self.type = ftype
|
||||||
|
|
||||||
|
def getXMLMessage(self, to):
|
||||||
|
msg = self.xmpp.makeMessage(to)
|
||||||
|
msg.append(self.getXML())
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def buildForm(self, xml):
|
||||||
|
self.type = xml.get('type', 'form')
|
||||||
|
if xml.find('{jabber:x:data}title') is not None:
|
||||||
|
self.setTitle(xml.find('{jabber:x:data}title').text)
|
||||||
|
if xml.find('{jabber:x:data}instructions') is not None:
|
||||||
|
self.setInstructions(xml.find('{jabber:x:data}instructions').text)
|
||||||
|
for field in xml.findall('{jabber:x:data}field'):
|
||||||
|
self.buildField(field)
|
||||||
|
for reported in xml.findall('{jabber:x:data}reported'):
|
||||||
|
self.buildReported(reported)
|
||||||
|
for item in xml.findall('{jabber:x:data}item'):
|
||||||
|
self.buildItem(item)
|
||||||
|
|
||||||
|
#def getXML(self, tostring = False):
|
||||||
|
def getXML(self, ftype=None):
|
||||||
|
logging.debug("creating form as %s" % ftype)
|
||||||
|
if ftype:
|
||||||
|
self.type = ftype
|
||||||
|
form = ET.Element('{jabber:x:data}x')
|
||||||
|
form.attrib['type'] = self.type
|
||||||
|
if self.title and self.type in ('form', 'result'):
|
||||||
|
title = ET.Element('title')
|
||||||
|
title.text = self.title
|
||||||
|
form.append(title)
|
||||||
|
if self.instructions and self.type == 'form':
|
||||||
|
instructions = ET.Element('instructions')
|
||||||
|
instructions.text = self.instructions
|
||||||
|
form.append(instructions)
|
||||||
|
for field in self.fields:
|
||||||
|
form.append(field.getXML(self.type))
|
||||||
|
for reported in self.reported:
|
||||||
|
form.append(reported.getXML('reported'))
|
||||||
|
for item in self.items:
|
||||||
|
form.append(item.getXML(self.type))
|
||||||
|
#if tostring:
|
||||||
|
# form = self.xmpp.tostring(form)
|
||||||
|
return form
|
||||||
|
|
||||||
|
def getXHTML(self):
|
||||||
|
form = ET.Element('{http://www.w3.org/1999/xhtml}form')
|
||||||
|
if self.title:
|
||||||
|
title = ET.Element('h2')
|
||||||
|
title.text = self.title
|
||||||
|
form.append(title)
|
||||||
|
if self.instructions:
|
||||||
|
instructions = ET.Element('p')
|
||||||
|
instructions.text = self.instructions
|
||||||
|
form.append(instructions)
|
||||||
|
for field in self.fields:
|
||||||
|
form.append(field.getXHTML())
|
||||||
|
for field in self.reported:
|
||||||
|
form.append(field.getXHTML())
|
||||||
|
for field in self.items:
|
||||||
|
form.append(field.getXHTML())
|
||||||
|
return form
|
||||||
|
|
||||||
|
|
||||||
|
def makeSubmit(self):
|
||||||
|
self.setType('submit')
|
||||||
|
|
||||||
|
class FormField(object):
|
||||||
|
types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single')
|
||||||
|
listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single')
|
||||||
|
lbtypes = ('fixed', 'text-multi')
|
||||||
|
def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None):
|
||||||
|
if not ftype in self.types:
|
||||||
|
raise ValueError("Invalid Field Type")
|
||||||
|
self.type = ftype
|
||||||
|
self.var = var
|
||||||
|
self.label = label
|
||||||
|
self.desc = desc
|
||||||
|
self.options = []
|
||||||
|
self.required = False
|
||||||
|
self.value = []
|
||||||
|
if self.type in self.listtypes:
|
||||||
|
self.islist = True
|
||||||
|
else:
|
||||||
|
self.islist = False
|
||||||
|
if self.type in self.lbtypes:
|
||||||
|
self.islinebreak = True
|
||||||
|
else:
|
||||||
|
self.islinebreak = False
|
||||||
|
if value:
|
||||||
|
self.setValue(value)
|
||||||
|
|
||||||
|
def addOption(self, value, label):
|
||||||
|
if self.islist:
|
||||||
|
self.options.append((value, label))
|
||||||
|
else:
|
||||||
|
raise ValueError("Cannot add options to non-list type field.")
|
||||||
|
|
||||||
|
def setTrue(self):
|
||||||
|
if self.type == 'boolean':
|
||||||
|
self.value = [True]
|
||||||
|
|
||||||
|
def setFalse(self):
|
||||||
|
if self.type == 'boolean':
|
||||||
|
self.value = [False]
|
||||||
|
|
||||||
|
def require(self):
|
||||||
|
self.required = True
|
||||||
|
|
||||||
|
def setDescription(self, desc):
|
||||||
|
self.desc = desc
|
||||||
|
|
||||||
|
def setValue(self, value):
|
||||||
|
if self.islinebreak and value is not None:
|
||||||
|
self.value += value.split('\n')
|
||||||
|
else:
|
||||||
|
if len(self.value) and (not self.islist or self.type == 'list-single'):
|
||||||
|
self.value = [value]
|
||||||
|
else:
|
||||||
|
self.value.append(value)
|
||||||
|
|
||||||
|
def delValue(self, value):
|
||||||
|
if type(self.value) == type([]):
|
||||||
|
try:
|
||||||
|
idx = self.value.index(value)
|
||||||
|
if idx != -1:
|
||||||
|
self.value.pop(idx)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.value = ''
|
||||||
|
|
||||||
|
def setAnswer(self, value):
|
||||||
|
self.setValue(value)
|
||||||
|
|
||||||
|
def buildField(self, xml):
|
||||||
|
self.type = xml.get('type', 'text-single')
|
||||||
|
self.label = xml.get('label', '')
|
||||||
|
for option in xml.findall('{jabber:x:data}option'):
|
||||||
|
self.addOption(option.find('{jabber:x:data}value').text, option.get('label', ''))
|
||||||
|
for value in xml.findall('{jabber:x:data}value'):
|
||||||
|
self.setValue(value.text)
|
||||||
|
if xml.find('{jabber:x:data}required') is not None:
|
||||||
|
self.require()
|
||||||
|
if xml.find('{jabber:x:data}desc') is not None:
|
||||||
|
self.setDescription(xml.find('{jabber:x:data}desc').text)
|
||||||
|
|
||||||
|
def getXML(self, ftype):
|
||||||
|
field = ET.Element('field')
|
||||||
|
if ftype != 'result':
|
||||||
|
field.attrib['type'] = self.type
|
||||||
|
if self.type != 'fixed':
|
||||||
|
if self.var:
|
||||||
|
field.attrib['var'] = self.var
|
||||||
|
if self.label:
|
||||||
|
field.attrib['label'] = self.label
|
||||||
|
if ftype == 'form':
|
||||||
|
for option in self.options:
|
||||||
|
optionxml = ET.Element('option')
|
||||||
|
optionxml.attrib['label'] = option[1]
|
||||||
|
optionval = ET.Element('value')
|
||||||
|
optionval.text = option[0]
|
||||||
|
optionxml.append(optionval)
|
||||||
|
field.append(optionxml)
|
||||||
|
if self.required:
|
||||||
|
required = ET.Element('required')
|
||||||
|
field.append(required)
|
||||||
|
if self.desc:
|
||||||
|
desc = ET.Element('desc')
|
||||||
|
desc.text = self.desc
|
||||||
|
field.append(desc)
|
||||||
|
for value in self.value:
|
||||||
|
valuexml = ET.Element('value')
|
||||||
|
if value is True or value is False:
|
||||||
|
if value:
|
||||||
|
valuexml.text = '1'
|
||||||
|
else:
|
||||||
|
valuexml.text = '0'
|
||||||
|
else:
|
||||||
|
valuexml.text = value
|
||||||
|
field.append(valuexml)
|
||||||
|
return field
|
||||||
|
|
||||||
|
def getXHTML(self):
|
||||||
|
field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type})
|
||||||
|
if self.label:
|
||||||
|
label = ET.Element('p')
|
||||||
|
label.text = "%s: " % self.label
|
||||||
|
else:
|
||||||
|
label = ET.Element('p')
|
||||||
|
label.text = "%s: " % self.var
|
||||||
|
field.append(label)
|
||||||
|
if self.type == 'boolean':
|
||||||
|
formf = ET.Element('input', {'type': 'checkbox', 'name': self.var})
|
||||||
|
if len(self.value) and self.value[0] in (True, 'true', '1'):
|
||||||
|
formf.attrib['checked'] = 'checked'
|
||||||
|
elif self.type == 'fixed':
|
||||||
|
formf = ET.Element('p')
|
||||||
|
try:
|
||||||
|
formf.text = ', '.join(self.value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
field.append(formf)
|
||||||
|
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
|
||||||
|
try:
|
||||||
|
formf.text = ', '.join(self.value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif self.type == 'hidden':
|
||||||
|
formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
|
||||||
|
try:
|
||||||
|
formf.text = ', '.join(self.value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif self.type in ('jid-multi', 'list-multi'):
|
||||||
|
formf = ET.Element('select', {'name': self.var})
|
||||||
|
for option in self.options:
|
||||||
|
optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'})
|
||||||
|
optf.text = option[1]
|
||||||
|
if option[1] in self.value:
|
||||||
|
optf.attrib['selected'] = 'selected'
|
||||||
|
formf.append(option)
|
||||||
|
elif self.type in ('jid-single', 'text-single'):
|
||||||
|
formf = ET.Element('input', {'type': 'text', 'name': self.var})
|
||||||
|
try:
|
||||||
|
formf.attrib['value'] = ', '.join(self.value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif self.type == 'list-single':
|
||||||
|
formf = ET.Element('select', {'name': self.var})
|
||||||
|
for option in self.options:
|
||||||
|
optf = ET.Element('option', {'value': option[0]})
|
||||||
|
optf.text = option[1]
|
||||||
|
if not optf.text:
|
||||||
|
optf.text = option[0]
|
||||||
|
if option[1] in self.value:
|
||||||
|
optf.attrib['selected'] = 'selected'
|
||||||
|
formf.append(optf)
|
||||||
|
elif self.type == 'text-multi':
|
||||||
|
formf = ET.Element('textarea', {'name': self.var})
|
||||||
|
try:
|
||||||
|
formf.text = ', '.join(self.value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not formf.text:
|
||||||
|
formf.text = ' '
|
||||||
|
elif self.type == 'text-private':
|
||||||
|
formf = ET.Element('input', {'type': 'password', 'name': self.var})
|
||||||
|
try:
|
||||||
|
formf.attrib['value'] = ', '.join(self.value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
label.append(formf)
|
||||||
|
return field
|
||||||
|
|
273
sleekxmpp/plugins/xep_0009.py
Normal file
273
sleekxmpp/plugins/xep_0009.py
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
"""
|
||||||
|
XEP-0009 XMPP Remote Procedure Calls
|
||||||
|
"""
|
||||||
|
from __future__ import with_statement
|
||||||
|
import base
|
||||||
|
import logging
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import copy
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
|
||||||
|
def py2xml(*args):
|
||||||
|
params = ET.Element("params")
|
||||||
|
for x in args:
|
||||||
|
param = ET.Element("param")
|
||||||
|
param.append(_py2xml(x))
|
||||||
|
params.append(param) #<params><param>...
|
||||||
|
return params
|
||||||
|
|
||||||
|
def _py2xml(*args):
|
||||||
|
for x in args:
|
||||||
|
val = ET.Element("value")
|
||||||
|
if type(x) is int:
|
||||||
|
i4 = ET.Element("i4")
|
||||||
|
i4.text = str(x)
|
||||||
|
val.append(i4)
|
||||||
|
if type(x) is bool:
|
||||||
|
boolean = ET.Element("boolean")
|
||||||
|
boolean.text = str(int(x))
|
||||||
|
val.append(boolean)
|
||||||
|
elif type(x) is str:
|
||||||
|
string = ET.Element("string")
|
||||||
|
string.text = x
|
||||||
|
val.append(string)
|
||||||
|
elif type(x) is float:
|
||||||
|
double = ET.Element("double")
|
||||||
|
double.text = str(x)
|
||||||
|
val.append(double)
|
||||||
|
elif type(x) is rpcbase64:
|
||||||
|
b64 = ET.Element("Base64")
|
||||||
|
b64.text = x.encoded()
|
||||||
|
val.append(b64)
|
||||||
|
elif type(x) is rpctime:
|
||||||
|
iso = ET.Element("dateTime.iso8601")
|
||||||
|
iso.text = str(x)
|
||||||
|
val.append(iso)
|
||||||
|
elif type(x) is list:
|
||||||
|
array = ET.Element("array")
|
||||||
|
data = ET.Element("data")
|
||||||
|
for y in x:
|
||||||
|
data.append(_py2xml(y))
|
||||||
|
array.append(data)
|
||||||
|
val.append(array)
|
||||||
|
elif type(x) is dict:
|
||||||
|
struct = ET.Element("struct")
|
||||||
|
for y in x.keys():
|
||||||
|
member = ET.Element("member")
|
||||||
|
name = ET.Element("name")
|
||||||
|
name.text = y
|
||||||
|
member.append(name)
|
||||||
|
member.append(_py2xml(x[y]))
|
||||||
|
struct.append(member)
|
||||||
|
val.append(struct)
|
||||||
|
return val
|
||||||
|
|
||||||
|
def xml2py(params):
|
||||||
|
vals = []
|
||||||
|
for param in params.findall('param'):
|
||||||
|
vals.append(_xml2py(param.find('value')))
|
||||||
|
return vals
|
||||||
|
|
||||||
|
def _xml2py(value):
|
||||||
|
if value.find('i4') is not None:
|
||||||
|
return int(value.find('i4').text)
|
||||||
|
if value.find('int') is not None:
|
||||||
|
return int(value.find('int').text)
|
||||||
|
if value.find('boolean') is not None:
|
||||||
|
return bool(value.find('boolean').text)
|
||||||
|
if value.find('string') is not None:
|
||||||
|
return value.find('string').text
|
||||||
|
if value.find('double') is not None:
|
||||||
|
return float(value.find('double').text)
|
||||||
|
if value.find('Base64') is not None:
|
||||||
|
return rpcbase64(value.find('Base64').text)
|
||||||
|
if value.find('dateTime.iso8601') is not None:
|
||||||
|
return rpctime(value.find('dateTime.iso8601'))
|
||||||
|
if value.find('struct') is not None:
|
||||||
|
struct = {}
|
||||||
|
for member in value.find('struct').findall('member'):
|
||||||
|
struct[member.find('name').text] = _xml2py(member.find('value'))
|
||||||
|
return struct
|
||||||
|
if value.find('array') is not None:
|
||||||
|
array = []
|
||||||
|
for val in value.find('array').find('data').findall('value'):
|
||||||
|
array.append(_xml2py(val))
|
||||||
|
return array
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
class rpcbase64(object):
|
||||||
|
def __init__(self, data):
|
||||||
|
#base 64 encoded string
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
def decode(self):
|
||||||
|
return base64.decodestring(data)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.decode()
|
||||||
|
|
||||||
|
def encoded(self):
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
class rpctime(object):
|
||||||
|
def __init__(self,data=None):
|
||||||
|
#assume string data is in iso format YYYYMMDDTHH:MM:SS
|
||||||
|
if type(data) is str:
|
||||||
|
self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
|
||||||
|
elif type(data) is time.struct_time:
|
||||||
|
self.timestamp = data
|
||||||
|
elif data is None:
|
||||||
|
self.timestamp = time.gmtime()
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
def iso8601(self):
|
||||||
|
#return a iso8601 string
|
||||||
|
return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.iso8601()
|
||||||
|
|
||||||
|
class JabberRPCEntry(object):
|
||||||
|
def __init__(self,call):
|
||||||
|
self.call = call
|
||||||
|
self.result = None
|
||||||
|
self.error = None
|
||||||
|
self.allow = {} #{'<jid>':['<resource1>',...],...}
|
||||||
|
self.deny = {}
|
||||||
|
|
||||||
|
def check_acl(self, jid, resource):
|
||||||
|
#Check for deny
|
||||||
|
if jid in self.deny.keys():
|
||||||
|
if self.deny[jid] == None or resource in self.deny[jid]:
|
||||||
|
return False
|
||||||
|
#Check for allow
|
||||||
|
if allow == None:
|
||||||
|
return True
|
||||||
|
if jid in self.allow.keys():
|
||||||
|
if self.allow[jid] == None or resource in self.allow[jid]:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def acl_allow(self, jid, resource):
|
||||||
|
if jid == None:
|
||||||
|
self.allow = None
|
||||||
|
elif resource == None:
|
||||||
|
self.allow[jid] = None
|
||||||
|
elif jid in self.allow.keys():
|
||||||
|
self.allow[jid].append(resource)
|
||||||
|
else:
|
||||||
|
self.allow[jid] = [resource]
|
||||||
|
|
||||||
|
def acl_deny(self, jid, resource):
|
||||||
|
if jid == None:
|
||||||
|
self.deny = None
|
||||||
|
elif resource == None:
|
||||||
|
self.deny[jid] = None
|
||||||
|
elif jid in self.deny.keys():
|
||||||
|
self.deny[jid].append(resource)
|
||||||
|
else:
|
||||||
|
self.deny[jid] = [resource]
|
||||||
|
|
||||||
|
def call_method(self, args):
|
||||||
|
ret = self.call(*args)
|
||||||
|
|
||||||
|
class xep_0009(base.base_plugin):
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0009'
|
||||||
|
self.description = 'Jabber-RPC'
|
||||||
|
self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>", self._callMethod)
|
||||||
|
self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>", self._callResult)
|
||||||
|
self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>", self._callError)
|
||||||
|
self.entries = {}
|
||||||
|
self.activeCalls = []
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
|
||||||
|
self.xmpp['xep_0030'].add_identity('automatition','rpc')
|
||||||
|
|
||||||
|
def register_call(self, method, name=None):
|
||||||
|
#@returns an string that can be used in acl commands.
|
||||||
|
with self.lock:
|
||||||
|
if name is None:
|
||||||
|
self.entries[method.__name__] = JabberRPCEntry(method)
|
||||||
|
return method.__name__
|
||||||
|
else:
|
||||||
|
self.entries[name] = JabberRPCEntry(method)
|
||||||
|
return name
|
||||||
|
|
||||||
|
def acl_allow(self, entry, jid=None, resource=None):
|
||||||
|
#allow the method entry to be called by the given jid and resource.
|
||||||
|
#if jid is None it will allow any jid/resource.
|
||||||
|
#if resource is None it will allow any resource belonging to the jid.
|
||||||
|
with self.lock:
|
||||||
|
if self.entries[entry]:
|
||||||
|
self.entries[entry].acl_allow(jid,resource)
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
def acl_deny(self, entry, jid=None, resource=None):
|
||||||
|
#Note: by default all requests are denied unless allowed with acl_allow.
|
||||||
|
#If you deny an entry it will not be allowed regardless of acl_allow
|
||||||
|
with self.lock:
|
||||||
|
if self.entries[entry]:
|
||||||
|
self.entries[entry].acl_deny(jid,resource)
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
def unregister_call(self, entry):
|
||||||
|
#removes the registered call
|
||||||
|
with self.lock:
|
||||||
|
if self.entries[entry]:
|
||||||
|
del self.entries[entry]
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
def makeMethodCallQuery(self,pmethod,params):
|
||||||
|
query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
|
||||||
|
methodCall = ET.Element('methodCall')
|
||||||
|
methodName = ET.Element('methodName')
|
||||||
|
methodName.text = pmethod
|
||||||
|
methodCall.append(methodName)
|
||||||
|
methodCall.append(params)
|
||||||
|
query.append(methodCall)
|
||||||
|
return query
|
||||||
|
|
||||||
|
def makeIqMethodCall(self,pto,pmethod,params):
|
||||||
|
iq = self.xmpp.makeIqSet()
|
||||||
|
iq.set('to',pto)
|
||||||
|
iq.append(self.makeMethodCallQuery(pmethod,params))
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def makeIqMethodResponse(self,pto,pid,params):
|
||||||
|
iq = self.xmpp.makeIqResult(pid)
|
||||||
|
iq.set('to',pto)
|
||||||
|
query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
|
||||||
|
methodResponse = ET.Element('methodResponse')
|
||||||
|
methodResponse.append(params)
|
||||||
|
query.append(methodResponse)
|
||||||
|
return iq
|
||||||
|
|
||||||
|
def makeIqMethodError(self,pto,id,pmethod,params,condition):
|
||||||
|
iq = self.xmpp.makeIqError(id)
|
||||||
|
iq.set('to',pto)
|
||||||
|
iq.append(self.makeMethodCallQuery(pmethod,params))
|
||||||
|
iq.append(self.xmpp['xep_0086'].makeError(condition))
|
||||||
|
return iq
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def call_remote(self, pto, pmethod, *args):
|
||||||
|
#calls a remote method. Returns the id of the Iq.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _callMethod(self,xml):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _callResult(self,xml):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _callError(self,xml):
|
||||||
|
pass
|
117
sleekxmpp/plugins/xep_0030.py
Normal file
117
sleekxmpp/plugins/xep_0030.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from __future__ import absolute_import, with_statement
|
||||||
|
from . import base
|
||||||
|
import logging
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import thread
|
||||||
|
|
||||||
|
class xep_0030(base.base_plugin):
|
||||||
|
"""
|
||||||
|
XEP-0030 Service Discovery
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0030'
|
||||||
|
self.description = 'Service Discovery'
|
||||||
|
self.features = {'main': ['http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#items']}
|
||||||
|
self.identities = {'main': [{'category': 'client', 'type': 'pc', 'name': 'SleekXMPP'}]}
|
||||||
|
self.items = {'main': []}
|
||||||
|
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>" % self.xmpp.default_ns, self.info_handler)
|
||||||
|
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='http://jabber.org/protocol/disco#items' /></iq>" % self.xmpp.default_ns, self.item_handler)
|
||||||
|
self.lock = thread.allocate_lock()
|
||||||
|
|
||||||
|
def add_feature(self, feature, node='main'):
|
||||||
|
with self.lock:
|
||||||
|
if not self.features.has_key(node):
|
||||||
|
self.features[node] = []
|
||||||
|
self.features[node].append(feature)
|
||||||
|
|
||||||
|
def add_identity(self, category=None, itype=None, name=None, node='main'):
|
||||||
|
if not self.identities.has_key(node):
|
||||||
|
self.identities[node] = []
|
||||||
|
self.identities[node].append({'category': category, 'type': itype, 'name': name})
|
||||||
|
|
||||||
|
def add_item(self, jid=None, name=None, node='main', subnode=''):
|
||||||
|
if not self.items.has_key(node):
|
||||||
|
self.items[node] = []
|
||||||
|
self.items[node].append({'jid': jid, 'name': name, 'node': subnode})
|
||||||
|
|
||||||
|
def info_handler(self, xml):
|
||||||
|
logging.debug("Info request from %s" % xml.get('from', ''))
|
||||||
|
iq = self.xmpp.makeIqResult(xml.get('id', self.xmpp.getNewId()))
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||||
|
query = xml.find('{http://jabber.org/protocol/disco#info}query')
|
||||||
|
node = query.get('node', 'main')
|
||||||
|
for identity in self.identities.get(node, []):
|
||||||
|
idxml = ET.Element('identity')
|
||||||
|
for attrib in identity:
|
||||||
|
if identity[attrib]:
|
||||||
|
idxml.attrib[attrib] = identity[attrib]
|
||||||
|
query.append(idxml)
|
||||||
|
for feature in self.features.get(node, []):
|
||||||
|
featxml = ET.Element('feature')
|
||||||
|
featxml.attrib['var'] = feature
|
||||||
|
query.append(featxml)
|
||||||
|
iq.append(query)
|
||||||
|
#print ET.tostring(iq)
|
||||||
|
self.xmpp.send(iq)
|
||||||
|
|
||||||
|
def item_handler(self, xml):
|
||||||
|
logging.debug("Item request from %s" % xml.get('from', ''))
|
||||||
|
iq = self.xmpp.makeIqResult(xml.get('id', self.xmpp.getNewId()))
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||||
|
query = self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#items').find('{http://jabber.org/protocol/disco#items}query')
|
||||||
|
node = xml.find('{http://jabber.org/protocol/disco#items}query').get('node', 'main')
|
||||||
|
for item in self.items.get(node, []):
|
||||||
|
itemxml = ET.Element('item')
|
||||||
|
itemxml.attrib = item
|
||||||
|
if itemxml.attrib['jid'] is None:
|
||||||
|
itemxml.attrib['jid'] = self.xmpp.fulljid
|
||||||
|
query.append(itemxml)
|
||||||
|
self.xmpp.send(iq)
|
||||||
|
|
||||||
|
def getItems(self, jid, node=None):
|
||||||
|
iq = self.xmpp.makeIqGet()
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#items')
|
||||||
|
if node:
|
||||||
|
iq.find('{http://jabber.org/protocol/disco#items}query').attrib['node'] = node
|
||||||
|
return self.xmpp.send(iq, "<iq id='%s' />" % iq.get('id'))
|
||||||
|
|
||||||
|
def getInfo(self, jid, node=None):
|
||||||
|
iq = self.xmpp.makeIqGet()
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#info')
|
||||||
|
if node:
|
||||||
|
iq.find('{http://jabber.org/protocol/disco#info}query').attrib['node'] = node
|
||||||
|
return self.xmpp.send(iq, self.xmpp.makeIq(iq.get('id')))
|
||||||
|
|
||||||
|
def parseInfo(self, xml):
|
||||||
|
result = {'identity': {}, 'feature': []}
|
||||||
|
for identity in xml.findall('{http://jabber.org/protocol/disco#info}query/{{http://jabber.org/protocol/disco#info}identity'):
|
||||||
|
result['identity'][identity['name']] = identity.attrib
|
||||||
|
for feature in xml.findall('{http://jabber.org/protocol/disco#info}query/{{http://jabber.org/protocol/disco#info}feature'):
|
||||||
|
result['feature'].append(feature.get('var', '__unknown__'))
|
||||||
|
return result
|
193
sleekxmpp/plugins/xep_0045.py
Normal file
193
sleekxmpp/plugins/xep_0045.py
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from __future__ import with_statement
|
||||||
|
import base
|
||||||
|
import logging
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
|
||||||
|
class xep_0045(base.base_plugin):
|
||||||
|
"""
|
||||||
|
Impliments XEP-0045 Multi User Chat
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.rooms = {}
|
||||||
|
self.ourNicks = {}
|
||||||
|
self.xep = '0045'
|
||||||
|
self.description = 'Multi User Chat (Very Basic Still)'
|
||||||
|
self.xmpp.add_handler("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns, self.handle_groupchat_message)
|
||||||
|
self.xmpp.add_handler("<presence />", self.handle_groupchat_presence)
|
||||||
|
|
||||||
|
def handle_groupchat_presence(self, xml):
|
||||||
|
""" Handle a presence in a muc.
|
||||||
|
"""
|
||||||
|
source = xml.attrib['from']
|
||||||
|
room = self.xmpp.getjidbare(source)
|
||||||
|
if room not in self.rooms.keys():
|
||||||
|
return
|
||||||
|
nick = self.xmpp.getjidresource(source)
|
||||||
|
entry = {
|
||||||
|
'nick': nick,
|
||||||
|
'room': room,
|
||||||
|
}
|
||||||
|
if 'type' in xml.attrib.keys():
|
||||||
|
entry['type'] = xml.attrib['type']
|
||||||
|
for tag in ['status','show','priority']:
|
||||||
|
if xml.find(('{%s}' % self.xmpp.default_ns) + tag) != None:
|
||||||
|
entry[tag] = xml.find(('{%s}' % self.xmpp.default_ns) + tag).text
|
||||||
|
else:
|
||||||
|
entry[tag] = None
|
||||||
|
|
||||||
|
for tag in ['affiliation','role','jid']:
|
||||||
|
item = xml.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}item')
|
||||||
|
if item != None:
|
||||||
|
if tag in item.attrib:
|
||||||
|
entry[tag] = item.attrib[tag]
|
||||||
|
else:
|
||||||
|
entry[tag] = None
|
||||||
|
else:
|
||||||
|
entry[tag] = None
|
||||||
|
|
||||||
|
if entry['status'] == 'unavailable':
|
||||||
|
self.rooms[room][nick] = None
|
||||||
|
else:
|
||||||
|
self.rooms[room][nick] = entry
|
||||||
|
logging.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
|
||||||
|
self.xmpp.event("groupchat_presence", entry)
|
||||||
|
|
||||||
|
def handle_groupchat_message(self, xml):
|
||||||
|
""" Handle a message event in a muc.
|
||||||
|
"""
|
||||||
|
mfrom = xml.attrib['from']
|
||||||
|
message = xml.find('{%s}body' % self.xmpp.default_ns).text
|
||||||
|
subject = xml.find('{%s}subject' % self.xmpp.default_ns)
|
||||||
|
if subject:
|
||||||
|
subject = subject.text
|
||||||
|
else:
|
||||||
|
subject = ''
|
||||||
|
resource = self.xmpp.getjidresource(mfrom)
|
||||||
|
mfrom = self.xmpp.getjidbare(mfrom)
|
||||||
|
mtype = xml.attrib.get('type', 'normal')
|
||||||
|
self.xmpp.event("groupchat_message", {'room': mfrom, 'name': resource, 'type': mtype, 'subject': subject, 'message': message})
|
||||||
|
|
||||||
|
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False):
|
||||||
|
""" Join the specified room, requesting 'maxhistory' lines of history.
|
||||||
|
"""
|
||||||
|
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick))
|
||||||
|
x = ET.Element('{http://jabber.org/protocol/muc}x')
|
||||||
|
if password:
|
||||||
|
passelement = ET.Element('password')
|
||||||
|
passelement.text = password
|
||||||
|
x.append(passelement)
|
||||||
|
history = ET.Element('history')
|
||||||
|
history.attrib['maxstanzas'] = maxhistory
|
||||||
|
x.append(history)
|
||||||
|
stanza.append(x)
|
||||||
|
if not wait:
|
||||||
|
self.xmpp.send(stanza)
|
||||||
|
else:
|
||||||
|
#wait for our own room presence back
|
||||||
|
expect = ET.Element('{jabber:client}presence', {'from':"%s/%s" % (room, nick)})
|
||||||
|
self.xmpp.send(stanza, expect)
|
||||||
|
self.rooms[room] = {}
|
||||||
|
self.ourNicks[room] = nick
|
||||||
|
|
||||||
|
def setAffiliation(self, room, jid, affiliation='member'):
|
||||||
|
""" Change room affiliation."""
|
||||||
|
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
|
||||||
|
raise TypeError
|
||||||
|
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
|
||||||
|
item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
|
||||||
|
query.append(item)
|
||||||
|
iq = self.xmpp.makeIqSet(query)
|
||||||
|
iq.attrib['to'] = room
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s' />" % iq.get('id'))
|
||||||
|
if result is None or result.get('type') != 'result':
|
||||||
|
raise ValueError
|
||||||
|
return True
|
||||||
|
|
||||||
|
def invite(self, room, jid, reason=''):
|
||||||
|
""" Invite a jid to a room."""
|
||||||
|
msg = self.xmpp.makeMessage(room, mtype='none')
|
||||||
|
x = ET.Element('{http://jabber.org/protocol/muc#user}x')
|
||||||
|
invite = ET.Element('invite', {'to': jid})
|
||||||
|
if reason:
|
||||||
|
rxml = ET.Element('reason')
|
||||||
|
rxml.text = reason
|
||||||
|
invite.append(rxml)
|
||||||
|
x.append(invite)
|
||||||
|
msg.append(x)
|
||||||
|
self.xmpp.send(msg)
|
||||||
|
|
||||||
|
def leaveMUC(self, room, nick):
|
||||||
|
""" Leave the specified room.
|
||||||
|
"""
|
||||||
|
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick))
|
||||||
|
del self.rooms[room]
|
||||||
|
|
||||||
|
def getRoomConfig(self, room):
|
||||||
|
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
|
||||||
|
iq.attrib['to'] = room
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s' />" % iq.get('id'))
|
||||||
|
if result is None or result.get('type') != 'result':
|
||||||
|
raise ValueError
|
||||||
|
form = result.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
|
||||||
|
if form is None:
|
||||||
|
raise ValueError
|
||||||
|
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
||||||
|
|
||||||
|
def cancelConfig(self, room):
|
||||||
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
|
x = ET.Element('{jabber:x:data}x', type='cancel')
|
||||||
|
query.append(x)
|
||||||
|
iq = self.xmpp.makeIqSet(query)
|
||||||
|
self.xmpp.send(iq, "<iq id='%s' />" % iq.get('id'))
|
||||||
|
|
||||||
|
def setRoomConfig(self, room, config):
|
||||||
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
||||||
|
x = config.getXML('submit')
|
||||||
|
query.append(x)
|
||||||
|
iq = self.xmpp.makeIqSet(query)
|
||||||
|
iq.attrib['to'] = room
|
||||||
|
self.xmpp.send(iq, "<iq id='%s' />" % iq.get('id'))
|
||||||
|
|
||||||
|
def getJoinedRooms(self):
|
||||||
|
return self.rooms.keys()
|
||||||
|
|
||||||
|
def getOurJidInRoom(self, roomJid):
|
||||||
|
""" Return the jid we're using in a room.
|
||||||
|
"""
|
||||||
|
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
|
||||||
|
|
||||||
|
def getJidProperty(self, room, nick, jidProperty):
|
||||||
|
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
|
||||||
|
If not found, return None.
|
||||||
|
"""
|
||||||
|
if self.rooms.has_key(room) and self.rooms[room].has_key(nick) and self.rooms[room][nick].has_key(jidProperty):
|
||||||
|
return self.rooms[room][nick][jidProperty]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getRoster(self, room):
|
||||||
|
""" Get the list of nicks in a room.
|
||||||
|
"""
|
||||||
|
if room not in self.rooms.keys():
|
||||||
|
return None
|
||||||
|
return self.rooms[room].keys()
|
142
sleekxmpp/plugins/xep_0050.py
Normal file
142
sleekxmpp/plugins/xep_0050.py
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from __future__ import with_statement
|
||||||
|
import base
|
||||||
|
import logging
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import traceback
|
||||||
|
import time
|
||||||
|
import thread
|
||||||
|
|
||||||
|
class xep_0050(base.base_plugin):
|
||||||
|
"""
|
||||||
|
XEP-0050 Ad-Hoc Commands
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0050'
|
||||||
|
self.description = 'Ad-Hoc Commands'
|
||||||
|
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command)
|
||||||
|
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command)
|
||||||
|
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, threaded=True)
|
||||||
|
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel)
|
||||||
|
self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete)
|
||||||
|
self.commands = {}
|
||||||
|
self.sessions = {}
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/commands')
|
||||||
|
|
||||||
|
def addCommand(self, node, name, form, pointer=None, multi=False):
|
||||||
|
self.xmpp['xep_0030'].add_item(None, name, 'http://jabber.org/protocol/commands', node)
|
||||||
|
self.xmpp['xep_0030'].add_identity('automation', 'command-node', name, node)
|
||||||
|
self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/commands', node)
|
||||||
|
self.xmpp['xep_0030'].add_feature('jabber:x:data', node)
|
||||||
|
self.commands[node] = (name, form, pointer, multi)
|
||||||
|
|
||||||
|
def getNewSession(self):
|
||||||
|
return str(time.time()) + '-' + self.xmpp.getNewId()
|
||||||
|
|
||||||
|
def handler_command(self, xml):
|
||||||
|
in_command = xml.find('{http://jabber.org/protocol/commands}command')
|
||||||
|
sessionid = in_command.get('sessionid', None)
|
||||||
|
node = in_command.get('node')
|
||||||
|
sessionid = self.getNewSession()
|
||||||
|
name, form, pointer, multi = self.commands[node]
|
||||||
|
self.sessions[sessionid] = {}
|
||||||
|
self.sessions[sessionid]['jid'] = xml.get('from')
|
||||||
|
self.sessions[sessionid]['past'] = [(form, None)]
|
||||||
|
self.sessions[sessionid]['next'] = pointer
|
||||||
|
npointer = pointer
|
||||||
|
if multi:
|
||||||
|
actions = ['next']
|
||||||
|
status = 'executing'
|
||||||
|
else:
|
||||||
|
if pointer is None:
|
||||||
|
status = 'completed'
|
||||||
|
actions = []
|
||||||
|
else:
|
||||||
|
status = 'executing'
|
||||||
|
actions = ['complete']
|
||||||
|
self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions))
|
||||||
|
|
||||||
|
def handler_command_complete(self, xml):
|
||||||
|
in_command = xml.find('{http://jabber.org/protocol/commands}command')
|
||||||
|
sessionid = in_command.get('sessionid', None)
|
||||||
|
pointer = self.sessions[sessionid]['next']
|
||||||
|
results = self.xmpp['xep_0004'].makeForm('result')
|
||||||
|
results.fromXML(in_command.find('{jabber:x:data}x'))
|
||||||
|
apply(pointer, (results,sessionid))
|
||||||
|
self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[]))
|
||||||
|
del self.sessions[command.get('sessionid')]
|
||||||
|
|
||||||
|
|
||||||
|
def handler_command_next(self, xml):
|
||||||
|
in_command = xml.find('{http://jabber.org/protocol/commands}command')
|
||||||
|
sessionid = in_command.get('sessionid', None)
|
||||||
|
pointer = self.sessions[sessionid]['next']
|
||||||
|
results = self.xmpp['xep_0004'].makeForm('result')
|
||||||
|
results.fromXML(in_command.find('{jabber:x:data}x'))
|
||||||
|
form, npointer, next = apply(pointer, (results,sessionid))
|
||||||
|
self.sessions[sessionid]['next'] = npointer
|
||||||
|
self.sessions[sessionid]['past'].append((form, pointer))
|
||||||
|
actions = []
|
||||||
|
actions.append('prev')
|
||||||
|
if npointer is None:
|
||||||
|
status = 'completed'
|
||||||
|
else:
|
||||||
|
status = 'executing'
|
||||||
|
if next:
|
||||||
|
actions.append('next')
|
||||||
|
else:
|
||||||
|
actions.append('complete')
|
||||||
|
self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions))
|
||||||
|
|
||||||
|
def handler_command_cancel(self, xml):
|
||||||
|
command = xml.find('{http://jabber.org/protocol/commands}command')
|
||||||
|
try:
|
||||||
|
del self.sessions[command.get('sessionid')]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.xmpp.send(self.makeCommand(xml.attrib['from'], command.attrib['node'], id=xml.attrib['id'], sessionid=command.attrib['sessionid'], status='canceled'))
|
||||||
|
|
||||||
|
def makeCommand(self, to, node, id=None, form=None, sessionid=None, status='executing', actions=[]):
|
||||||
|
if not id:
|
||||||
|
id = self.xmpp.getNewId()
|
||||||
|
iq = self.xmpp.makeIqResult(id)
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
iq.attrib['to'] = to
|
||||||
|
command = ET.Element('{http://jabber.org/protocol/commands}command')
|
||||||
|
command.attrib['node'] = node
|
||||||
|
command.attrib['status'] = status
|
||||||
|
xmlactions = ET.Element('actions')
|
||||||
|
for action in actions:
|
||||||
|
xmlactions.append(ET.Element(action))
|
||||||
|
if xmlactions:
|
||||||
|
command.append(xmlactions)
|
||||||
|
if not sessionid:
|
||||||
|
sessionid = self.getNewSession()
|
||||||
|
command.attrib['sessionid'] = sessionid
|
||||||
|
if form is not None:
|
||||||
|
if hasattr(form,'getXML'):
|
||||||
|
form = form.getXML()
|
||||||
|
command.append(form)
|
||||||
|
iq.append(command)
|
||||||
|
return iq
|
306
sleekxmpp/plugins/xep_0060.py
Normal file
306
sleekxmpp/plugins/xep_0060.py
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
from __future__ import with_statement
|
||||||
|
from . import base
|
||||||
|
import logging
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
|
||||||
|
class xep_0060(base.base_plugin):
|
||||||
|
"""
|
||||||
|
XEP-0060 Publish Subscribe
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0060'
|
||||||
|
self.description = 'Publish-Subscribe'
|
||||||
|
|
||||||
|
def create_node(self, jid, node, config=None, collection=False):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||||
|
create = ET.Element('create')
|
||||||
|
create.set('node', node)
|
||||||
|
pubsub.append(create)
|
||||||
|
configure = ET.Element('configure')
|
||||||
|
if config is None:
|
||||||
|
submitform = self.xmpp.plugin['xep_0004'].makeForm('submit')
|
||||||
|
else:
|
||||||
|
submitform = config
|
||||||
|
if 'FORM_TYPE' in submitform.field:
|
||||||
|
submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config')
|
||||||
|
else:
|
||||||
|
submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config')
|
||||||
|
if collection:
|
||||||
|
if 'pubsub#node_type' in submitform.field:
|
||||||
|
submitform.field['pubsub#node_type'].setValue('collection')
|
||||||
|
else:
|
||||||
|
submitform.addField('pubsub#node_type', value='collection')
|
||||||
|
else:
|
||||||
|
if 'pubsub#node_type' in submitform.field:
|
||||||
|
submitform.field['pubsub#node_type'].setValue('leaf')
|
||||||
|
else:
|
||||||
|
submitform.addField('pubsub#node_type', value='leaf')
|
||||||
|
configure.append(submitform.getXML('submit'))
|
||||||
|
pubsub.append(configure)
|
||||||
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is False or result is None or result.get('type') == 'error': return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def subscribe(self, jid, node, bare=True, subscribee=None):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||||
|
subscribe = ET.Element('subscribe')
|
||||||
|
subscribe.attrib['node'] = node
|
||||||
|
if subscribee is None:
|
||||||
|
if bare:
|
||||||
|
subscribe.attrib['jid'] = self.xmpp.jid
|
||||||
|
else:
|
||||||
|
subscribe.attrib['jid'] = self.xmpp.fulljid
|
||||||
|
else:
|
||||||
|
subscribe.attrib['jid'] = subscribee
|
||||||
|
pubsub.append(subscribe)
|
||||||
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is False or result is None or result.get('type') == 'error': return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def unsubscribe(self, jid, node, bare=True, subscribee=None):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||||
|
unsubscribe = ET.Element('unsubscribe')
|
||||||
|
unsubscribe.attrib['node'] = node
|
||||||
|
if subscribee is None:
|
||||||
|
if bare:
|
||||||
|
unsubscribe.attrib['jid'] = self.xmpp.jid
|
||||||
|
else:
|
||||||
|
unsubscribe.attrib['jid'] = self.xmpp.fulljid
|
||||||
|
else:
|
||||||
|
unsubscribe.attrib['jid'] = subscribee
|
||||||
|
pubsub.append(unsubscribe)
|
||||||
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is False or result is None or result.get('type') == 'error': return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getNodeConfig(self, jid, node=None): # if no node, then grab default
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||||
|
if node is not None:
|
||||||
|
configure = ET.Element('configure')
|
||||||
|
configure.attrib['node'] = node
|
||||||
|
else:
|
||||||
|
configure = ET.Element('default')
|
||||||
|
pubsub.append(configure)
|
||||||
|
#TODO: Add configure support.
|
||||||
|
iq = self.xmpp.makeIqGet()
|
||||||
|
iq.append(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
#self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is None or result == False or result.get('type') == 'error':
|
||||||
|
logging.warning("got error instead of config")
|
||||||
|
return False
|
||||||
|
if node is not None:
|
||||||
|
form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x')
|
||||||
|
else:
|
||||||
|
form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x')
|
||||||
|
if not form or form is None:
|
||||||
|
logging.error("No form found.")
|
||||||
|
return False
|
||||||
|
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
||||||
|
|
||||||
|
def getNodeSubscriptions(self, jid, node):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||||
|
subscriptions = ET.Element('subscriptions')
|
||||||
|
subscriptions.attrib['node'] = node
|
||||||
|
pubsub.append(subscriptions)
|
||||||
|
iq = self.xmpp.makeIqGet()
|
||||||
|
iq.append(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is None or result == False or result.get('type') == 'error':
|
||||||
|
logging.warning("got error instead of config")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription')
|
||||||
|
if results is None:
|
||||||
|
return False
|
||||||
|
subs = {}
|
||||||
|
for sub in results:
|
||||||
|
subs[sub.get('jid')] = sub.get('subscription')
|
||||||
|
return subs
|
||||||
|
|
||||||
|
def getNodeAffiliations(self, jid, node):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||||
|
affiliations = ET.Element('affiliations')
|
||||||
|
affiliations.attrib['node'] = node
|
||||||
|
pubsub.append(affiliations)
|
||||||
|
iq = self.xmpp.makeIqGet()
|
||||||
|
iq.append(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is None or result == False or result.get('type') == 'error':
|
||||||
|
logging.warning("got error instead of config")
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation')
|
||||||
|
if results is None:
|
||||||
|
return False
|
||||||
|
subs = {}
|
||||||
|
for sub in results:
|
||||||
|
subs[sub.get('jid')] = sub.get('affiliation')
|
||||||
|
return subs
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def deleteNode(self, jid, node):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||||
|
iq = self.xmpp.makeIqSet()
|
||||||
|
delete = ET.Element('delete')
|
||||||
|
delete.attrib['node'] = node
|
||||||
|
pubsub.append(delete)
|
||||||
|
iq.append(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is not None and result is not False and result.attrib.get('type', 'error') != 'error':
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def setNodeConfig(self, jid, node, config):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||||
|
configure = ET.Element('configure')
|
||||||
|
configure.attrib['node'] = node
|
||||||
|
config = config.getXML('submit')
|
||||||
|
configure.append(config)
|
||||||
|
pubsub.append(configure)
|
||||||
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is None or result.get('type') == 'error':
|
||||||
|
print "---------- returning false, apparently"
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def setItem(self, jid, node, items=[]):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||||
|
publish = ET.Element('publish')
|
||||||
|
publish.attrib['node'] = node
|
||||||
|
for pub_item in items:
|
||||||
|
id, payload = pub_item
|
||||||
|
item = ET.Element('item')
|
||||||
|
if id is not None:
|
||||||
|
item.attrib['id'] = id
|
||||||
|
item.append(payload)
|
||||||
|
publish.append(item)
|
||||||
|
pubsub.append(publish)
|
||||||
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is None or result is False or result.get('type') == 'error': return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def deleteItem(self, jid, node, item):
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||||
|
retract = ET.Element('retract')
|
||||||
|
retract.attrib['node'] = node
|
||||||
|
itemn = ET.Element('item')
|
||||||
|
itemn.attrib['id'] = item
|
||||||
|
retract.append(itemn)
|
||||||
|
pubsub.append(retract)
|
||||||
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is None or result is False or result.get('type') == 'error': return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def addItem(self, jid, node, items=[]):
|
||||||
|
return setItem(jid, node, items)
|
||||||
|
|
||||||
|
def getNodes(self, jid):
|
||||||
|
response = self.xmpp.plugin['xep_0030'].getItems(jid)
|
||||||
|
items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
|
||||||
|
nodes = {}
|
||||||
|
if items is not None and items is not False:
|
||||||
|
for item in items:
|
||||||
|
nodes[item.get('node')] = item.get('name')
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def getItems(self, jid, node):
|
||||||
|
response = self.xmpp.plugin['xep_0030'].getItems(jid, node)
|
||||||
|
items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
|
||||||
|
nodeitems = []
|
||||||
|
if items is not None and items is not False:
|
||||||
|
for item in items:
|
||||||
|
nodeitems.append(item.get('node'))
|
||||||
|
return nodeitems
|
||||||
|
|
||||||
|
def addNodeToCollection(self, jid, child, parent=''):
|
||||||
|
config = self.getNodeConfig(jid, child)
|
||||||
|
if not config or config is None:
|
||||||
|
self.lasterror = "Config Error"
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
config.field['pubsub#collection'].setValue(parent)
|
||||||
|
except KeyError:
|
||||||
|
logging.warning("pubsub#collection doesn't exist in config, trying to add it")
|
||||||
|
config.addField('pubsub#collection', value=parent)
|
||||||
|
if not self.setNodeConfig(jid, child, config):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def modifyAffiliation(self, ps_jid, node, user_jid, affiliation):
|
||||||
|
if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'):
|
||||||
|
raise TypeError
|
||||||
|
pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
|
||||||
|
affs = ET.Element('affiliations')
|
||||||
|
affs.attrib['node'] = node
|
||||||
|
aff = ET.Element('affiliation')
|
||||||
|
aff.attrib['jid'] = user_jid
|
||||||
|
aff.attrib['affiliation'] = affiliation
|
||||||
|
affs.append(aff)
|
||||||
|
pubsub.append(affs)
|
||||||
|
iq = self.xmpp.makeIqSet(pubsub)
|
||||||
|
iq.attrib['to'] = ps_jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq id='%s'/>" % id)
|
||||||
|
if result is None or result is False or result.get('type') == 'error':
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def addNodeToCollection(self, jid, child, parent=''):
|
||||||
|
config = self.getNodeConfig(jid, child)
|
||||||
|
if not config or config is None:
|
||||||
|
self.lasterror = "Config Error"
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
config.field['pubsub#collection'].setValue(parent)
|
||||||
|
except KeyError:
|
||||||
|
logging.warning("pubsub#collection doesn't exist in config, trying to add it")
|
||||||
|
config.addField('pubsub#collection', value=parent)
|
||||||
|
if not self.setNodeConfig(jid, child, config):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def removeNodeFromCollection(self, jid, child):
|
||||||
|
self.addNodeToCollection(jid, child, '')
|
||||||
|
|
81
sleekxmpp/plugins/xep_0078.py
Normal file
81
sleekxmpp/plugins/xep_0078.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from __future__ import with_statement
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import logging
|
||||||
|
import sha
|
||||||
|
import base
|
||||||
|
|
||||||
|
|
||||||
|
class xep_0078(base.base_plugin):
|
||||||
|
"""
|
||||||
|
XEP-0078 NON-SASL Authentication
|
||||||
|
"""
|
||||||
|
def plugin_init(self):
|
||||||
|
self.description = "Non-SASL Authentication (broken)"
|
||||||
|
self.xep = "0078"
|
||||||
|
self.xmpp.add_start_handler(self.check_stream)
|
||||||
|
#disabling until I fix conflict with PLAIN
|
||||||
|
#self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth)
|
||||||
|
self.streamid = ''
|
||||||
|
|
||||||
|
def check_stream(self, xml):
|
||||||
|
self.streamid = xml.attrib['id']
|
||||||
|
if xml.get('version', '0') != '1.0':
|
||||||
|
self.auth()
|
||||||
|
|
||||||
|
def auth(self, xml=None):
|
||||||
|
logging.debug("Starting jabber:iq:auth Authentication")
|
||||||
|
auth_request = self.xmpp.makeIqGet()
|
||||||
|
auth_request_query = ET.Element('{jabber:iq:auth}query')
|
||||||
|
auth_request.attrib['to'] = self.xmpp.server
|
||||||
|
username = ET.Element('username')
|
||||||
|
username.text = self.xmpp.username
|
||||||
|
auth_request_query.append(username)
|
||||||
|
auth_request.append(auth_request_query)
|
||||||
|
result = self.xmpp.send(auth_request, self.xmpp.makeIqResult(self.xmpp.id))
|
||||||
|
rquery = result.find('{jabber:iq:auth}query')
|
||||||
|
attempt = self.xmpp.makeIqSet()
|
||||||
|
query = ET.Element('{jabber:iq:auth}query')
|
||||||
|
resource = ET.Element('resource')
|
||||||
|
resource.text = self.xmpp.resource
|
||||||
|
query.append(username)
|
||||||
|
query.append(resource)
|
||||||
|
if rquery.find('{jabber:iq:auth}digest') is None:
|
||||||
|
logging.warning("Authenticating via jabber:iq:auth Plain.")
|
||||||
|
password = ET.Element('password')
|
||||||
|
password.text = self.xmpp.password
|
||||||
|
query.append(password)
|
||||||
|
else:
|
||||||
|
logging.debug("Authenticating via jabber:iq:auth Digest")
|
||||||
|
digest = ET.Element('digest')
|
||||||
|
digest.text = sha.sha("%s%s" % (self.streamid, self.xmpp.password)).hexdigest()
|
||||||
|
query.append(digest)
|
||||||
|
attempt.append(query)
|
||||||
|
result = self.xmpp.send(attempt, self.xmpp.makeIq(self.xmpp.id))
|
||||||
|
if result.attrib['type'] == 'result':
|
||||||
|
with self.xmpp.lock:
|
||||||
|
self.xmpp.authenticated = True
|
||||||
|
self.xmpp.sessionstarted = True
|
||||||
|
self.xmpp.event("session_start")
|
||||||
|
else:
|
||||||
|
logging.info("Authentication failed")
|
||||||
|
self.xmpp.disconnect()
|
||||||
|
self.xmpp.event("failed_auth")
|
49
sleekxmpp/plugins/xep_0086.py
Normal file
49
sleekxmpp/plugins/xep_0086.py
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
|
||||||
|
from __future__ import with_statement
|
||||||
|
import base
|
||||||
|
import logging
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import copy
|
||||||
|
|
||||||
|
class xep_0086(base.base_plugin):
|
||||||
|
"""
|
||||||
|
XEP-0086 Error Condition Mappings
|
||||||
|
"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.xep = '0086'
|
||||||
|
self.description = 'Error Condition Mappings'
|
||||||
|
self.error_map = {
|
||||||
|
'bad-request':('modify','400'),
|
||||||
|
'conflict':('cancel','409'),
|
||||||
|
'feature-not-implemented':('cancel','501'),
|
||||||
|
'forbidden':('auth','403'),
|
||||||
|
'gone':('modify','302'),
|
||||||
|
'internal-server-error':('wait','500'),
|
||||||
|
'item-not-found':('cancel','404'),
|
||||||
|
'jid-malformed':('modify','400'),
|
||||||
|
'not-acceptable':('modify','406'),
|
||||||
|
'not-allowed':('cancel','405'),
|
||||||
|
'not-authorized':('auth','401'),
|
||||||
|
'payment-required':('auth','402'),
|
||||||
|
'recipient-unavailable':('wait','404'),
|
||||||
|
'redirect':('modify','302'),
|
||||||
|
'registration-required':('auth','407'),
|
||||||
|
'remote-server-not-found':('cancel','404'),
|
||||||
|
'remote-server-timeout':('wait','504'),
|
||||||
|
'resource-constraint':('wait','500'),
|
||||||
|
'service-unavailable':('cancel','503'),
|
||||||
|
'subscription-required':('auth','407'),
|
||||||
|
'undefined-condition':(None,'500'),
|
||||||
|
'unexpected-request':('wait','400')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def makeError(self, condition, cdata=None, errorType=None, text=None, customElem=None):
|
||||||
|
conditionElem = self.xmpp.makeStanzaErrorCondition(condition, cdata)
|
||||||
|
if errorType is None:
|
||||||
|
error = self.xmpp.makeStanzaError(conditionElem, self.error_map[condition][0], self.error_map[condition][1], text, customElem)
|
||||||
|
else:
|
||||||
|
error = self.xmpp.makeStanzaError(conditionElem, errorType, self.error_map[condition][1], text, customElem)
|
||||||
|
error.append(conditionElem)
|
||||||
|
return error
|
67
sleekxmpp/plugins/xep_0092.py
Normal file
67
sleekxmpp/plugins/xep_0092.py
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
Copyright (C) 2007 Nathanael C. Fritz
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
from . import base
|
||||||
|
from .. xmlstream.handler.xmlwaiter import XMLWaiter
|
||||||
|
|
||||||
|
class xep_0092(base.base_plugin):
|
||||||
|
"""
|
||||||
|
XEP-0092 Software Version
|
||||||
|
"""
|
||||||
|
def plugin_init(self):
|
||||||
|
self.description = "Software Version"
|
||||||
|
self.xep = "0092"
|
||||||
|
self.name = self.config.get('name', 'SleekXMPP')
|
||||||
|
self.version = self.config.get('version', '0.1-dev')
|
||||||
|
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
self.xmpp['xep_0030'].add_feature('jabber:iq:version')
|
||||||
|
|
||||||
|
def report_version(self, xml):
|
||||||
|
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
|
||||||
|
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||||
|
query = ET.Element('{jabber:iq:version}query')
|
||||||
|
name = ET.Element('name')
|
||||||
|
name.text = self.name
|
||||||
|
version = ET.Element('version')
|
||||||
|
version.text = self.version
|
||||||
|
query.append(name)
|
||||||
|
query.append(version)
|
||||||
|
iq.append(query)
|
||||||
|
self.xmpp.send(iq)
|
||||||
|
|
||||||
|
def getVersion(self, jid):
|
||||||
|
iq = self.xmpp.makeIqGet()
|
||||||
|
query = ET.Element('{jabber:iq:version}query')
|
||||||
|
iq.append(query)
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
iq.attrib['from'] = self.xmpp.fulljid
|
||||||
|
id = iq.get('id')
|
||||||
|
result = self.xmpp.send(iq, "<iq xmlns='%s' id='%s'/>" % (self.xmpp.default_ns, id))
|
||||||
|
if result and result is not None and result.get('type', 'error') != 'error':
|
||||||
|
qry = result.find('{jabber:iq:version}query')
|
||||||
|
version = {}
|
||||||
|
for child in qry.getchildren():
|
||||||
|
version[child.tag.split('}')[-1]] = child.text
|
||||||
|
return version
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
70
sleekxmpp/plugins/xep_0199.py
Normal file
70
sleekxmpp/plugins/xep_0199.py
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
"""
|
||||||
|
SleekXMPP: The Sleek XMPP Library
|
||||||
|
XEP-0199 (Ping) support
|
||||||
|
Copyright (C) 2007 Kevin Smith
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
from . import base
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class xep_0199(base.base_plugin):
|
||||||
|
"""XEP-0199 XMPP Ping"""
|
||||||
|
|
||||||
|
def plugin_init(self):
|
||||||
|
self.description = "XMPP Ping"
|
||||||
|
self.xep = "0199"
|
||||||
|
self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='http://www.xmpp.org/extensions/xep-0199.html#ns'/></iq>" % self.xmpp.default_ns, self.handler_ping)
|
||||||
|
self.running = False
|
||||||
|
if self.config.get('keepalive', True):
|
||||||
|
self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True)
|
||||||
|
|
||||||
|
def post_init(self):
|
||||||
|
self.xmpp['xep_0030'].add_feature('http://www.xmpp.org/extensions/xep-0199.html#ns')
|
||||||
|
|
||||||
|
def handler_pingserver(self, xml):
|
||||||
|
if not self.running:
|
||||||
|
time.sleep(self.config.get('frequency', 300))
|
||||||
|
while self.sendPing(self.xmpp.server, self.config.get('timeout', 30)) is not False:
|
||||||
|
time.sleep(self.config.get('frequency', 300))
|
||||||
|
logging.debug("Did not recieve ping back in time. Requesting Reconnect.")
|
||||||
|
self.xmpp.disconnect(reconnect=True)
|
||||||
|
|
||||||
|
def handler_ping(self, xml):
|
||||||
|
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
|
||||||
|
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||||
|
self.xmpp.send(iq)
|
||||||
|
|
||||||
|
def sendPing(self, jid, timeout = 30):
|
||||||
|
""" sendPing(jid, timeout)
|
||||||
|
Sends a ping to the specified jid, returning the time (in seconds)
|
||||||
|
to receive a reply, or None if no reply is received in timeout seconds.
|
||||||
|
"""
|
||||||
|
id = self.xmpp.getNewId()
|
||||||
|
iq = self.xmpp.makeIq(id)
|
||||||
|
iq.attrib['type'] = 'get'
|
||||||
|
iq.attrib['to'] = jid
|
||||||
|
ping = ET.Element('{http://www.xmpp.org/extensions/xep-0199.html#ns}ping')
|
||||||
|
iq.append(ping)
|
||||||
|
startTime = time.clock()
|
||||||
|
pingresult = self.xmpp.send(iq, self.xmpp.makeIq(id), timeout)
|
||||||
|
endTime = time.clock()
|
||||||
|
if pingresult == False:
|
||||||
|
#self.xmpp.disconnect(reconnect=True)
|
||||||
|
return False
|
||||||
|
return endTime - startTime
|
1
sleekxmpp/stanza/__init__.py
Normal file
1
sleekxmpp/stanza/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__all__ = ['presence']
|
0
sleekxmpp/stanza/iq.py
Normal file
0
sleekxmpp/stanza/iq.py
Normal file
0
sleekxmpp/stanza/message.py
Normal file
0
sleekxmpp/stanza/message.py
Normal file
21
sleekxmpp/stanza/presence.py
Normal file
21
sleekxmpp/stanza/presence.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from .. xmlstream.stanzabase import StanzaBase
|
||||||
|
from .. xmlstream import xmlstream as xmlstreammod
|
||||||
|
from .. xmlstream.matcher.xpath import MatchXPath
|
||||||
|
|
||||||
|
#_bases = [StanzaBase] + xmlstreammod.stanza_extensions.get('PresenceStanza', [])
|
||||||
|
|
||||||
|
#class PresenceStanza(*_bases):
|
||||||
|
class PresenceStanza(StanzaBase):
|
||||||
|
|
||||||
|
def __init__(self, stream, xml=None):
|
||||||
|
self.pfrom = ''
|
||||||
|
self.pto = ''
|
||||||
|
StanzaBase.__init__(self, stream, xml, xmlstreammod.stanza_extensions.get('PresenceStanza', []))
|
||||||
|
|
||||||
|
def fromXML(self, xml):
|
||||||
|
StanzaBase.fromXML(self, xml)
|
||||||
|
self.pfrom = xml.get('from')
|
||||||
|
self.pto = xml.get('to')
|
||||||
|
self.ptype = xml.get('type')
|
||||||
|
|
||||||
|
stanzas = ({'stanza_class': PresenceStanza, 'matcher': MatchXPath('{jabber:client}presence'), 'root': True},)
|
359
sleekxmpp/tests/testpubsub.py
Executable file
359
sleekxmpp/tests/testpubsub.py
Executable file
|
@ -0,0 +1,359 @@
|
||||||
|
"""
|
||||||
|
This file is part of SleekXMPP.
|
||||||
|
|
||||||
|
SleekXMPP is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
SleekXMPP is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with SleekXMPP; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sleekxmpp.clientxmpp
|
||||||
|
from optparse import OptionParser
|
||||||
|
from xml.etree import cElementTree as ET
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
import queue
|
||||||
|
|
||||||
|
|
||||||
|
class testps(sleekxmpp.clientxmpp.ClientXMPP):
|
||||||
|
def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], nodenum=0, pshost=None):
|
||||||
|
sleekxmpp.clientxmpp.ClientXMPP.__init__(self, jid, password, ssl, plugin_config, plugin_whitelist)
|
||||||
|
self.registerPlugin('xep_0004')
|
||||||
|
self.registerPlugin('xep_0030')
|
||||||
|
self.registerPlugin('xep_0060')
|
||||||
|
self.registerPlugin('xep_0092')
|
||||||
|
self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, threaded=True)
|
||||||
|
self.add_event_handler("session_start", self.start, threaded=True)
|
||||||
|
self.add_handler("<iq type='error' />", self.handleError)
|
||||||
|
self.events = queue.Queue()
|
||||||
|
self.default_config = None
|
||||||
|
self.ps = self.plugin['xep_0060']
|
||||||
|
self.node = "pstestnode_%s"
|
||||||
|
self.pshost = pshost
|
||||||
|
if pshost is None:
|
||||||
|
self.pshost = self.server
|
||||||
|
self.nodenum = int(nodenum)
|
||||||
|
self.leafnode = self.nodenum + 1
|
||||||
|
self.collectnode = self.nodenum + 2
|
||||||
|
self.lasterror = ''
|
||||||
|
self.sprintchars = 0
|
||||||
|
self.defaultconfig = None
|
||||||
|
self.tests = ['test_defaultConfig', 'test_createDefaultNode', 'test_getNodes', 'test_deleteNode', 'test_createWithConfig', 'test_reconfigureNode', 'test_subscribeToNode', 'test_addItem', 'test_updateItem', 'test_deleteItem', 'test_unsubscribeNode', 'test_createCollection', 'test_subscribeCollection', 'test_addNodeCollection', 'test_deleteNodeCollection', 'test_addCollectionNode', 'test_deleteCollectionNode', 'test_unsubscribeNodeCollection', 'test_deleteCollection']
|
||||||
|
self.passed = 0
|
||||||
|
self.width = 120
|
||||||
|
|
||||||
|
def start(self, event):
|
||||||
|
#TODO: make this configurable
|
||||||
|
self.getRoster()
|
||||||
|
self.sendPresence(ppriority=20)
|
||||||
|
self.test_all()
|
||||||
|
|
||||||
|
def sprint(self, msg, end=False, color=False):
|
||||||
|
length = len(msg)
|
||||||
|
if color:
|
||||||
|
if color == "red":
|
||||||
|
color = "1;31"
|
||||||
|
elif color == "green":
|
||||||
|
color = "0;32"
|
||||||
|
msg = "%s%s%s" % ("\033[%sm" % color, msg, "\033[0m")
|
||||||
|
if not end:
|
||||||
|
sys.stdout.write(msg)
|
||||||
|
self.sprintchars += length
|
||||||
|
else:
|
||||||
|
self.sprint("%s%s" % ("." * (self.width - self.sprintchars - length), msg))
|
||||||
|
print('')
|
||||||
|
self.sprintchars = 0
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
def pubsubEventHandler(self, xml):
|
||||||
|
for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}items/{http://jabber.org/protocol/pubsub#event}item'):
|
||||||
|
self.events.put(item.get('id', '__unknown__'))
|
||||||
|
for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}items/{http://jabber.org/protocol/pubsub#event}retract'):
|
||||||
|
self.events.put(item.get('id', '__unknown__'))
|
||||||
|
for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}collection/{http://jabber.org/protocol/pubsub#event}disassociate'):
|
||||||
|
self.events.put(item.get('node', '__unknown__'))
|
||||||
|
for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}collection/{http://jabber.org/protocol/pubsub#event}associate'):
|
||||||
|
self.events.put(item.get('node', '__unknown__'))
|
||||||
|
|
||||||
|
def handleError(self, xml):
|
||||||
|
error = xml.find('{jabber:client}error')
|
||||||
|
self.lasterror = error.getchildren()[0].tag.split('}')[-1]
|
||||||
|
|
||||||
|
def test_all(self):
|
||||||
|
print("Running Publish-Subscribe Tests")
|
||||||
|
version = self.plugin['xep_0092'].getVersion(self.pshost)
|
||||||
|
if version:
|
||||||
|
print("%s %s on %s" % (version.get('name', 'Unknown Server'), version.get('version', 'v?'), version.get('os', 'Unknown OS')))
|
||||||
|
print("=" * self.width)
|
||||||
|
for test in self.tests:
|
||||||
|
testfunc = getattr(self, test)
|
||||||
|
self.sprint("%s" % testfunc.__doc__)
|
||||||
|
if testfunc():
|
||||||
|
self.sprint("Passed", True, "green")
|
||||||
|
self.passed += 1
|
||||||
|
else:
|
||||||
|
if not self.lasterror:
|
||||||
|
self.lasterror = 'No response'
|
||||||
|
self.sprint("Failed (%s)" % self.lasterror, True, "red")
|
||||||
|
self.lasterror = ''
|
||||||
|
print("=" * self.width)
|
||||||
|
self.sprint("Cleaning up...")
|
||||||
|
#self.ps.deleteNode(self.pshost, self.node % self.nodenum)
|
||||||
|
self.ps.deleteNode(self.pshost, self.node % self.leafnode)
|
||||||
|
#self.ps.deleteNode(self.pshost, self.node % self.collectnode)
|
||||||
|
self.sprint("Done", True, "green")
|
||||||
|
self.disconnect()
|
||||||
|
self.sprint("%s" % self.passed, False, "green")
|
||||||
|
self.sprint("/%s Passed -- " % len(self.tests))
|
||||||
|
if len(self.tests) - self.passed:
|
||||||
|
self.sprint("%s" % (len(self.tests) - self.passed), False, "red")
|
||||||
|
else:
|
||||||
|
self.sprint("%s" % (len(self.tests) - self.passed), False, "green")
|
||||||
|
self.sprint(" Failed Tests")
|
||||||
|
print
|
||||||
|
#print "%s/%s Passed -- %s Failed Tests" % (self.passed, len(self.tests), len(self.tests) - self.passed)
|
||||||
|
|
||||||
|
def test_defaultConfig(self):
|
||||||
|
"Retreiving default configuration"
|
||||||
|
result = self.ps.getNodeConfig(self.pshost)
|
||||||
|
if result is False or result is None:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
self.defaultconfig = result
|
||||||
|
try:
|
||||||
|
self.defaultconfig.field['pubsub#access_model'].setValue('open')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
self.defaultconfig.field['pubsub#notify_retract'].setValue(True)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_createDefaultNode(self):
|
||||||
|
"Creating default node"
|
||||||
|
return self.ps.create_node(self.pshost, self.node % self.nodenum)
|
||||||
|
|
||||||
|
def test_getNodes(self):
|
||||||
|
"Getting list of nodes"
|
||||||
|
self.ps.getNodes(self.pshost)
|
||||||
|
self.ps.getItems(self.pshost, 'blog')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_deleteNode(self):
|
||||||
|
"Deleting node"
|
||||||
|
return self.ps.deleteNode(self.pshost, self.node % self.nodenum)
|
||||||
|
|
||||||
|
def test_createWithConfig(self):
|
||||||
|
"Creating node with config"
|
||||||
|
if self.defaultconfig is None:
|
||||||
|
self.lasterror = "No Avail Config"
|
||||||
|
return False
|
||||||
|
return self.ps.create_node(self.pshost, self.node % self.leafnode, self.defaultconfig)
|
||||||
|
|
||||||
|
def test_reconfigureNode(self):
|
||||||
|
"Retrieving node config and reconfiguring"
|
||||||
|
nconfig = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
|
||||||
|
if nconfig == False:
|
||||||
|
return False
|
||||||
|
return self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, nconfig)
|
||||||
|
|
||||||
|
def test_subscribeToNode(self):
|
||||||
|
"Subscribing to node"
|
||||||
|
return self.ps.subscribe(self.pshost, self.node % self.leafnode)
|
||||||
|
|
||||||
|
def test_addItem(self):
|
||||||
|
"Adding item, waiting for notification"
|
||||||
|
item = ET.Element('test')
|
||||||
|
result = self.ps.setItem(self.pshost, self.node % self.leafnode, {'test_node1': item})
|
||||||
|
if result == False:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
event = self.events.get(True, 10)
|
||||||
|
except queue.Empty:
|
||||||
|
return False
|
||||||
|
if event == 'test_node1':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_updateItem(self):
|
||||||
|
"Updating item, waiting for notification"
|
||||||
|
item = ET.Element('test')
|
||||||
|
item.attrib['crap'] = 'yup, right here'
|
||||||
|
result = self.ps.setItem(self.pshost, self.node % self.leafnode, {'test_node1': item})
|
||||||
|
if result == False:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
event = self.events.get(True, 10)
|
||||||
|
except queue.Empty:
|
||||||
|
return False
|
||||||
|
if event == 'test_node1':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_deleteItem(self):
|
||||||
|
"Deleting item, waiting for notification"
|
||||||
|
result = self.ps.deleteItem(self.pshost, self.node % self.leafnode, 'test_node1')
|
||||||
|
if result == False:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
event = self.events.get(True, 10)
|
||||||
|
except queue.Empty:
|
||||||
|
self.lasterror = "No Notification"
|
||||||
|
return False
|
||||||
|
if event == 'test_node1':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_unsubscribeNode(self):
|
||||||
|
"Unsubscribing from node"
|
||||||
|
return self.ps.unsubscribe(self.pshost, self.node % self.leafnode)
|
||||||
|
|
||||||
|
def test_createCollection(self):
|
||||||
|
"Creating collection node"
|
||||||
|
return self.ps.create_node(self.pshost, self.node % self.collectnode, self.defaultconfig, True)
|
||||||
|
|
||||||
|
def test_subscribeCollection(self):
|
||||||
|
"Subscribing to collection node"
|
||||||
|
return self.ps.subscribe(self.pshost, self.node % self.collectnode)
|
||||||
|
|
||||||
|
def test_addNodeCollection(self):
|
||||||
|
"Assigning node to collection, waiting for notification"
|
||||||
|
config = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
|
||||||
|
if not config or config is None:
|
||||||
|
self.lasterror = "Config Error"
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
config.field['pubsub#collection'].setValue(self.node % self.collectnode)
|
||||||
|
except KeyError:
|
||||||
|
self.sprint("...Missing Field...", False, "red")
|
||||||
|
config.addField('pubsub#collection', value=self.node % self.collectnode)
|
||||||
|
if not self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, config):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
event = self.events.get(True, 10)
|
||||||
|
except queue.Empty:
|
||||||
|
self.lasterror = "No Notification"
|
||||||
|
return False
|
||||||
|
if event == self.node % self.leafnode:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_deleteNodeCollection(self):
|
||||||
|
"Removing node assignment to collection, waiting for notification"
|
||||||
|
config = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode)
|
||||||
|
if not config or config is None:
|
||||||
|
self.lasterror = "Config Error"
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
config.field['pubsub#collection'].delValue(self.node % self.collectnode)
|
||||||
|
except KeyError:
|
||||||
|
self.sprint("...Missing Field...", False, "red")
|
||||||
|
config.addField('pubsub#collection', value='')
|
||||||
|
if not self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, config):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
event = self.events.get(True, 10)
|
||||||
|
except queue.Empty:
|
||||||
|
self.lasterror = "No Notification"
|
||||||
|
return False
|
||||||
|
if event == self.node % self.leafnode:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_addCollectionNode(self):
|
||||||
|
"Assigning node from collection, waiting for notification"
|
||||||
|
config = self.ps.getNodeConfig(self.pshost, self.node % self.collectnode)
|
||||||
|
if not config or config is None:
|
||||||
|
self.lasterror = "Config Error"
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
config.field['pubsub#children'].setValue(self.node % self.leafnode)
|
||||||
|
except KeyError:
|
||||||
|
self.sprint("...Missing Field...", False, "red")
|
||||||
|
config.addField('pubsub#children', value=self.node % self.leafnode)
|
||||||
|
if not self.ps.setNodeConfig(self.pshost, self.node % self.collectnode, config):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
event = self.events.get(True, 10)
|
||||||
|
except queue.Empty:
|
||||||
|
self.lasterror = "No Notification"
|
||||||
|
return False
|
||||||
|
if event == self.node % self.leafnode:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_deleteCollectionNode(self):
|
||||||
|
"Removing node from collection, waiting for notification"
|
||||||
|
config = self.ps.getNodeConfig(self.pshost, self.node % self.collectnode)
|
||||||
|
if not config or config is None:
|
||||||
|
self.lasterror = "Config Error"
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
config.field['pubsub#children'].delValue(self.node % self.leafnode)
|
||||||
|
except KeyError:
|
||||||
|
self.sprint("...Missing Field...", False, "red")
|
||||||
|
config.addField('pubsub#children', value='')
|
||||||
|
if not self.ps.setNodeConfig(self.pshost, self.node % self.collectnode, config):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
event = self.events.get(True, 10)
|
||||||
|
except queue.Empty:
|
||||||
|
self.lasterror = "No Notification"
|
||||||
|
return False
|
||||||
|
if event == self.node % self.leafnode:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def test_unsubscribeNodeCollection(self):
|
||||||
|
"Unsubscribing from collection"
|
||||||
|
return self.ps.unsubscribe(self.pshost, self.node % self.collectnode)
|
||||||
|
|
||||||
|
def test_deleteCollection(self):
|
||||||
|
"Deleting collection"
|
||||||
|
return self.ps.deleteNode(self.pshost, self.node % self.collectnode)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
#parse command line arguements
|
||||||
|
optp = OptionParser()
|
||||||
|
optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO)
|
||||||
|
optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
|
||||||
|
optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO)
|
||||||
|
optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
|
||||||
|
optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
|
||||||
|
optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
|
||||||
|
opts,args = optp.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||||
|
|
||||||
|
#load xml config
|
||||||
|
logging.info("Loading config file: %s" % opts.configfile)
|
||||||
|
config = ET.parse(os.path.expanduser(opts.configfile)).find('auth')
|
||||||
|
|
||||||
|
#init
|
||||||
|
logging.info("Logging in as %s" % config.attrib['jid'])
|
||||||
|
|
||||||
|
|
||||||
|
plugin_config = {}
|
||||||
|
plugin_config['xep_0092'] = {'name': 'SleekXMPP Example', 'version': '0.1-dev'}
|
||||||
|
plugin_config['xep_0199'] = {'keepalive': True, 'timeout': 30, 'frequency': 300}
|
||||||
|
|
||||||
|
con = testps(config.attrib['jid'], config.attrib['pass'], plugin_config=plugin_config, plugin_whitelist=[], nodenum=opts.nodenum, pshost=opts.pubsub)
|
||||||
|
if not config.get('server', None):
|
||||||
|
# we don't know the server, but the lib can probably figure it out
|
||||||
|
con.connect()
|
||||||
|
else:
|
||||||
|
con.connect((config.attrib['server'], 5222))
|
||||||
|
con.process()
|
||||||
|
print("")
|
0
sleekxmpp/xmlstream/__init__.py
Normal file
0
sleekxmpp/xmlstream/__init__.py
Normal file
0
sleekxmpp/xmlstream/handler/__init__.py
Normal file
0
sleekxmpp/xmlstream/handler/__init__.py
Normal file
18
sleekxmpp/xmlstream/handler/base.py
Normal file
18
sleekxmpp/xmlstream/handler/base.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
class BaseHandler(object):
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, name, matcher):
|
||||||
|
self.name = name
|
||||||
|
self._destroy = False
|
||||||
|
self._payload = None
|
||||||
|
self._matcher = matcher
|
||||||
|
|
||||||
|
def match(self, xml):
|
||||||
|
return self._matcher.match(xml)
|
||||||
|
|
||||||
|
def run(self, payload):
|
||||||
|
self._payload = payload
|
||||||
|
|
||||||
|
def checkDelete(self):
|
||||||
|
return self._destroy
|
20
sleekxmpp/xmlstream/handler/callback.py
Normal file
20
sleekxmpp/xmlstream/handler/callback.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from . import base
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class Callback(base.BaseHandler):
|
||||||
|
|
||||||
|
def __init__(self, name, matcher, pointer, thread=False, once=False):
|
||||||
|
base.BaseHandler.__init__(self, name, matcher)
|
||||||
|
self._pointer = pointer
|
||||||
|
self._thread = thread
|
||||||
|
self._once = once
|
||||||
|
|
||||||
|
def run(self, payload):
|
||||||
|
base.BaseHandler.run(self, payload)
|
||||||
|
if self._thread:
|
||||||
|
x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,))
|
||||||
|
x.start()
|
||||||
|
else:
|
||||||
|
self._pointer(payload)
|
||||||
|
if self._once:
|
||||||
|
self._destroy = True
|
21
sleekxmpp/xmlstream/handler/waiter.py
Normal file
21
sleekxmpp/xmlstream/handler/waiter.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from . import base
|
||||||
|
import Queue
|
||||||
|
import logging
|
||||||
|
|
||||||
|
class Waiter(base.BaseHandler):
|
||||||
|
|
||||||
|
def __init__(self, name, matcher):
|
||||||
|
base.BaseHandler.__init__(self, name, matcher)
|
||||||
|
self._payload = Queue.Queue()
|
||||||
|
|
||||||
|
def run(self, payload):
|
||||||
|
self._payload.put(payload)
|
||||||
|
|
||||||
|
def wait(self, timeout=60):
|
||||||
|
try:
|
||||||
|
return self._payload.get(True, timeout)
|
||||||
|
except Queue.Empty:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def checkDelete(self):
|
||||||
|
return True
|
7
sleekxmpp/xmlstream/handler/xmlcallback.py
Normal file
7
sleekxmpp/xmlstream/handler/xmlcallback.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import threading
|
||||||
|
from . callback import Callback
|
||||||
|
|
||||||
|
class XMLCallback(Callback):
|
||||||
|
|
||||||
|
def run(self, payload):
|
||||||
|
Callback.run(self, payload.xml)
|
6
sleekxmpp/xmlstream/handler/xmlwaiter.py
Normal file
6
sleekxmpp/xmlstream/handler/xmlwaiter.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from . waiter import Waiter
|
||||||
|
|
||||||
|
class XMLWaiter(Waiter):
|
||||||
|
|
||||||
|
def run(self, payload):
|
||||||
|
Waiter.run(self, payload.xml)
|
0
sleekxmpp/xmlstream/matcher/__init__.py
Normal file
0
sleekxmpp/xmlstream/matcher/__init__.py
Normal file
8
sleekxmpp/xmlstream/matcher/base.py
Normal file
8
sleekxmpp/xmlstream/matcher/base.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
class MatcherBase(object):
|
||||||
|
|
||||||
|
def __init__(self, criteria):
|
||||||
|
self._criteria = criteria
|
||||||
|
|
||||||
|
def match(self, xml):
|
||||||
|
return False
|
10
sleekxmpp/xmlstream/matcher/many.py
Normal file
10
sleekxmpp/xmlstream/matcher/many.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from . import base
|
||||||
|
from xml.etree import cElementTree
|
||||||
|
|
||||||
|
class MatchMany(base.MatcherBase):
|
||||||
|
|
||||||
|
def match(self, xml):
|
||||||
|
for m in self._criteria:
|
||||||
|
if m.match(xml):
|
||||||
|
return True
|
||||||
|
return False
|
43
sleekxmpp/xmlstream/matcher/xmlmask.py
Normal file
43
sleekxmpp/xmlstream/matcher/xmlmask.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
from . import base
|
||||||
|
from xml.etree import cElementTree
|
||||||
|
from xml.parsers.expat import ExpatError
|
||||||
|
|
||||||
|
class MatchXMLMask(base.MatcherBase):
|
||||||
|
|
||||||
|
def __init__(self, criteria):
|
||||||
|
base.MatcherBase.__init__(self, criteria)
|
||||||
|
if type(criteria) == type(''):
|
||||||
|
self._criteria = cElementTree.fromstring(self._criteria)
|
||||||
|
self.default_ns = 'jabber:client'
|
||||||
|
|
||||||
|
def setDefaultNS(self, ns):
|
||||||
|
self.default_ns = ns
|
||||||
|
|
||||||
|
def match(self, xml):
|
||||||
|
return self.maskcmp(xml, self._criteria, True)
|
||||||
|
|
||||||
|
def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'):
|
||||||
|
"""maskcmp(xmlobj, maskobj):
|
||||||
|
Compare etree xml object to etree xml object mask"""
|
||||||
|
#TODO require namespaces
|
||||||
|
if source == None: #if element not found (happens during recursive check below)
|
||||||
|
return False
|
||||||
|
if type(maskobj) == type(str()): #if the mask is a string, make it an xml obj
|
||||||
|
try:
|
||||||
|
maskobj = cElementTree.fromstring(maskobj)
|
||||||
|
except ExpatError:
|
||||||
|
logging.log(logging.WARNING, "Expat error: %s\nIn parsing: %s" % ('', maskobj))
|
||||||
|
if not use_ns and source.tag.split('}', 1)[-1] != maskobj.tag.split('}', 1)[-1]: # strip off ns and compare
|
||||||
|
return False
|
||||||
|
if use_ns and (source.tag != maskobj.tag and "{%s}%s" % (self.default_ns, maskobj.tag) != source.tag ):
|
||||||
|
return False
|
||||||
|
if maskobj.text and source.text != maskobj.text:
|
||||||
|
return False
|
||||||
|
for attr_name in maskobj.attrib: #compare attributes
|
||||||
|
if source.attrib.get(attr_name, "__None__") != maskobj.attrib[attr_name]:
|
||||||
|
return False
|
||||||
|
#for subelement in maskobj.getiterator()[1:]: #recursively compare subelements
|
||||||
|
for subelement in maskobj: #recursively compare subelements
|
||||||
|
if not self.maskcmp(source.find(subelement.tag), subelement, use_ns):
|
||||||
|
return False
|
||||||
|
return True
|
11
sleekxmpp/xmlstream/matcher/xpath.py
Normal file
11
sleekxmpp/xmlstream/matcher/xpath.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from . import base
|
||||||
|
from xml.etree import cElementTree
|
||||||
|
|
||||||
|
class MatchXPath(base.MatcherBase):
|
||||||
|
|
||||||
|
def match(self, xml):
|
||||||
|
x = cElementTree.Element('x')
|
||||||
|
x.append(xml)
|
||||||
|
if x.find(self._criteria) is not None:
|
||||||
|
return True
|
||||||
|
return False
|
37
sleekxmpp/xmlstream/stanzabase.py
Normal file
37
sleekxmpp/xmlstream/stanzabase.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
|
||||||
|
|
||||||
|
class StanzaBase(object):
|
||||||
|
|
||||||
|
MATCHER = MatchXPath("")
|
||||||
|
|
||||||
|
def __init__(self, stream, xml=None, extensions=[]):
|
||||||
|
self.extensions = extensions
|
||||||
|
self.p = {} #plugins
|
||||||
|
|
||||||
|
self.xml = xml
|
||||||
|
self.stream = stream
|
||||||
|
if xml is not None:
|
||||||
|
self.fromXML(xml)
|
||||||
|
|
||||||
|
def fromXML(self, xml):
|
||||||
|
"Initialize based on incoming XML"
|
||||||
|
self._processXML(xml)
|
||||||
|
for ext in self.extensions:
|
||||||
|
ext.fromXML(self, xml)
|
||||||
|
|
||||||
|
|
||||||
|
def _processXML(self, xml, cur_ns=''):
|
||||||
|
if '}' in xml.tag:
|
||||||
|
ns,tag = xml.tag[1:].split('}')
|
||||||
|
else:
|
||||||
|
tag = xml.tag
|
||||||
|
|
||||||
|
def toXML(self, xml):
|
||||||
|
"Set outgoing XML"
|
||||||
|
|
||||||
|
def extend(self, extension_class, xml=None):
|
||||||
|
"Initialize extension"
|
||||||
|
|
||||||
|
def match(self, xml):
|
||||||
|
return self.MATCHER.match(xml)
|
52
sleekxmpp/xmlstream/statemachine.py
Normal file
52
sleekxmpp/xmlstream/statemachine.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from __future__ import with_statement
|
||||||
|
import threading
|
||||||
|
|
||||||
|
class StateMachine(object):
|
||||||
|
|
||||||
|
def __init__(self, states=[], groups=[]):
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.__state = {}
|
||||||
|
self.__default_state = {}
|
||||||
|
self.__group = {}
|
||||||
|
self.addStates(states)
|
||||||
|
self.addGroups(groups)
|
||||||
|
|
||||||
|
def addStates(self, states):
|
||||||
|
with self.lock:
|
||||||
|
for state in states:
|
||||||
|
if state in self.__state or state in self.__group:
|
||||||
|
raise IndexError("The state or group '%s' is already in the StateMachine." % state)
|
||||||
|
self.__state[state] = states[state]
|
||||||
|
self.__default_state[state] = states[state]
|
||||||
|
|
||||||
|
def addGroups(self, groups):
|
||||||
|
with self.lock:
|
||||||
|
for gstate in groups:
|
||||||
|
if gstate in self.__state or gstate in self.__group:
|
||||||
|
raise IndexError("The key or group '%s' is already in the StateMachine." % gstate)
|
||||||
|
for state in groups[gstate]:
|
||||||
|
if self.__state.has_key(state):
|
||||||
|
raise IndexError("The group %s contains a key %s which is not set in the StateMachine." % (gstate, state))
|
||||||
|
self.__group[gstate] = groups[gstate]
|
||||||
|
|
||||||
|
def set(self, state, status):
|
||||||
|
with self.lock:
|
||||||
|
if state in self.__state:
|
||||||
|
self.__state[state] = bool(status)
|
||||||
|
else:
|
||||||
|
raise KeyError("StateMachine does not contain state %s." % state)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
if key in self.__group:
|
||||||
|
for state in self.__group[key]:
|
||||||
|
if not self.__state[state]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
return self.__state[key]
|
||||||
|
|
||||||
|
def __getattr__(self, attr):
|
||||||
|
return self.__getitem__(attr)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.__state = self.__default_state
|
||||||
|
|
23
sleekxmpp/xmlstream/test.py
Normal file
23
sleekxmpp/xmlstream/test.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import xmlstream
|
||||||
|
import time
|
||||||
|
import socket
|
||||||
|
from handler.callback import Callback
|
||||||
|
from matcher.xpath import MatchXPath
|
||||||
|
|
||||||
|
def server():
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(('localhost', 5228))
|
||||||
|
s.listen(1)
|
||||||
|
servers = []
|
||||||
|
while True:
|
||||||
|
conn, addr = s.accept()
|
||||||
|
server = xmlstream.XMLStream(conn, 'localhost', 5228)
|
||||||
|
server.registerHandler(Callback('test', MatchXPath('test'), testHandler))
|
||||||
|
server.process()
|
||||||
|
servers.append(server)
|
||||||
|
|
||||||
|
def testHandler(xml):
|
||||||
|
print("weeeeeeeee!")
|
||||||
|
|
||||||
|
server()
|
2
sleekxmpp/xmlstream/test.xml
Normal file
2
sleekxmpp/xmlstream/test.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<stream>
|
||||||
|
</stream>
|
13
sleekxmpp/xmlstream/testclient.py
Normal file
13
sleekxmpp/xmlstream/testclient.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.connect(('localhost', 5228))
|
||||||
|
s.send("<stream>")
|
||||||
|
#s.flush()
|
||||||
|
s.send("<test/>")
|
||||||
|
s.send("<test/>")
|
||||||
|
s.send("<test/>")
|
||||||
|
s.send("</stream>")
|
||||||
|
#s.flush()
|
||||||
|
s.close()
|
388
sleekxmpp/xmlstream/xmlstream.py
Normal file
388
sleekxmpp/xmlstream/xmlstream.py
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
from __future__ import with_statement
|
||||||
|
import Queue
|
||||||
|
from . import statemachine
|
||||||
|
from . stanzabase import StanzaBase
|
||||||
|
from xml.etree import cElementTree
|
||||||
|
from xml.parsers import expat
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import thread
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import types
|
||||||
|
import xml.sax.saxutils
|
||||||
|
|
||||||
|
ssl_support = True
|
||||||
|
try:
|
||||||
|
from tlslite.api import *
|
||||||
|
except ImportError:
|
||||||
|
ssl_support = False
|
||||||
|
|
||||||
|
|
||||||
|
class RestartStream(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CloseStream(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
stanza_extensions = {}
|
||||||
|
|
||||||
|
class _fileobject(object): # we still need this because Socket.makefile is broken in python2.5 (but it works fine in 3.0)
|
||||||
|
|
||||||
|
def __init__(self, sock, mode='rb', bufsize=-1):
|
||||||
|
self._sock = sock
|
||||||
|
if bufsize <= 0:
|
||||||
|
bufsize = 1024
|
||||||
|
self.bufsize = bufsize
|
||||||
|
self.softspace = False
|
||||||
|
|
||||||
|
def read(self, size=-1):
|
||||||
|
if size <= 0:
|
||||||
|
size = sys.maxint
|
||||||
|
blocks = []
|
||||||
|
#while size > 0:
|
||||||
|
# b = self._sock.recv(min(size, self.bufsize))
|
||||||
|
# size -= len(b)
|
||||||
|
# if not b:
|
||||||
|
# break
|
||||||
|
# blocks.append(b)
|
||||||
|
# print size
|
||||||
|
#return "".join(blocks)
|
||||||
|
buff = self._sock.recv(self.bufsize)
|
||||||
|
logging.debug("RECV: %s" % buff)
|
||||||
|
return buff
|
||||||
|
|
||||||
|
def readline(self, size=-1):
|
||||||
|
return self.read(size)
|
||||||
|
if size < 0:
|
||||||
|
size = sys.maxint
|
||||||
|
blocks = []
|
||||||
|
read_size = min(20, size)
|
||||||
|
found = 0
|
||||||
|
while size and not found:
|
||||||
|
b = self._sock.recv(read_size, MSG_PEEK)
|
||||||
|
if not b:
|
||||||
|
break
|
||||||
|
found = b.find('\n') + 1
|
||||||
|
length = found or len(b)
|
||||||
|
size -= length
|
||||||
|
blocks.append(self._sock.recv(length))
|
||||||
|
read_size = min(read_size * 2, size, self.bufsize)
|
||||||
|
return "".join(blocks)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
self._sock.sendall(str(data))
|
||||||
|
|
||||||
|
def writelines(self, lines):
|
||||||
|
# This version mimics the current writelines, which calls
|
||||||
|
# str() on each line, but comments that we should reject
|
||||||
|
# non-string non-buffers. Let's omit the next line.
|
||||||
|
lines = [str(s) for s in lines]
|
||||||
|
self._sock.sendall(''.join(lines))
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._sock.close()
|
||||||
|
|
||||||
|
|
||||||
|
class XMLStream(object):
|
||||||
|
"A connection manager with XML events."
|
||||||
|
|
||||||
|
def __init__(self, socket=None, host='', port=0, escape_quotes=False):
|
||||||
|
global ssl_support
|
||||||
|
self.ssl_support = ssl_support
|
||||||
|
self.escape_quotes = escape_quotes
|
||||||
|
self.state = statemachine.StateMachine()
|
||||||
|
self.state.addStates({'connected':False, 'is client':False, 'ssl':False, 'tls':False, 'reconnect':True, 'processing':False}) #set initial states
|
||||||
|
|
||||||
|
self.setSocket(socket)
|
||||||
|
self.address = (host, int(port))
|
||||||
|
|
||||||
|
self.__thread = {}
|
||||||
|
|
||||||
|
self.__root_stanza = {}
|
||||||
|
self.__stanza = {}
|
||||||
|
self.__stanza_extension = {}
|
||||||
|
self.__handlers = []
|
||||||
|
|
||||||
|
self.__tls_socket = None
|
||||||
|
self.use_ssl = False
|
||||||
|
self.use_tls = False
|
||||||
|
|
||||||
|
self.stream_header = "<stream>"
|
||||||
|
self.stream_footer = "</stream>"
|
||||||
|
|
||||||
|
self.namespace_map = {}
|
||||||
|
|
||||||
|
def setSocket(self, socket):
|
||||||
|
"Set the socket"
|
||||||
|
self.socket = socket
|
||||||
|
if socket is not None:
|
||||||
|
self.filesocket = socket.makefile('rb', 0) # ElementTree.iterparse requires a file. 0 buffer files have to be binary
|
||||||
|
self.state.set('connected', True)
|
||||||
|
|
||||||
|
|
||||||
|
def setFileSocket(self, filesocket):
|
||||||
|
self.filesocket = filesocket
|
||||||
|
|
||||||
|
def connect(self, host='', port=0, use_ssl=False, use_tls=True):
|
||||||
|
"Link to connectTCP"
|
||||||
|
return self.connectTCP(host, port, use_ssl, use_tls)
|
||||||
|
|
||||||
|
def connectTCP(self, host='', port=0, use_ssl=None, use_tls=None, reattempt=True):
|
||||||
|
"Connect and create socket"
|
||||||
|
while reattempt and not self.state['connected']:
|
||||||
|
if host and port:
|
||||||
|
self.address = (host, int(port))
|
||||||
|
if use_ssl is not None:
|
||||||
|
self.use_ssl = use_ssl
|
||||||
|
if use_tls is not None:
|
||||||
|
self.use_tls = use_tls
|
||||||
|
self.state.set('is client', True)
|
||||||
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
if self.use_ssl and self.ssl_support:
|
||||||
|
logging.debug("Socket Wrapped for SSL")
|
||||||
|
self.socket = ssl.wrap_socket(self.socket)
|
||||||
|
try:
|
||||||
|
self.socket.connect(self.address)
|
||||||
|
self.state.set('connected', True)
|
||||||
|
return True
|
||||||
|
except socket.error,(errno, strerror):
|
||||||
|
logging.error("Could not connect. Socket Error #%s: %s" % (errno, strerror))
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
def connectUnix(self, filepath):
|
||||||
|
"Connect to Unix file and create socket"
|
||||||
|
|
||||||
|
def startTLS(self):
|
||||||
|
"Handshakes for TLS"
|
||||||
|
#self.socket = ssl.wrap_socket(self.socket, ssl_version=ssl.PROTOCOL_TLSv1, do_handshake_on_connect=False)
|
||||||
|
#self.socket.do_handshake()
|
||||||
|
if self.ssl_support:
|
||||||
|
self.realsocket = self.socket
|
||||||
|
self.socket = TLSConnection(self.socket)
|
||||||
|
self.socket.handshakeClientCert()
|
||||||
|
self.file = _fileobject(self.socket)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.warning("Tried to enable TLS, but tlslite module not found.")
|
||||||
|
return False
|
||||||
|
raise RestartStream()
|
||||||
|
|
||||||
|
def process(self, threaded=True):
|
||||||
|
#self.__thread['process'] = threading.Thread(name='process', target=self._process)
|
||||||
|
#self.__thread['process'].start()
|
||||||
|
if threaded:
|
||||||
|
thread.start_new(self._process, tuple())
|
||||||
|
else:
|
||||||
|
self._process()
|
||||||
|
|
||||||
|
def _process(self):
|
||||||
|
"Start processing the socket."
|
||||||
|
firstrun = True
|
||||||
|
while firstrun or self.state['reconnect']:
|
||||||
|
self.state.set('processing', True)
|
||||||
|
firstrun = False
|
||||||
|
try:
|
||||||
|
if self.state['is client']:
|
||||||
|
self.sendRaw(self.stream_header)
|
||||||
|
while self.__readXML():
|
||||||
|
if self.state['is client']:
|
||||||
|
self.sendRaw(self.stream_header)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.debug("Keyboard Escape Detected")
|
||||||
|
self.state.set('processing', False)
|
||||||
|
self.disconnect()
|
||||||
|
raise
|
||||||
|
except:
|
||||||
|
self.state.set('processing', False)
|
||||||
|
traceback.print_exc()
|
||||||
|
self.disconnect(reconnect=True)
|
||||||
|
if self.state['reconnect']:
|
||||||
|
self.reconnect()
|
||||||
|
self.state.set('processing', False)
|
||||||
|
#self.__thread['readXML'] = threading.Thread(name='readXML', target=self.__readXML)
|
||||||
|
#self.__thread['readXML'].start()
|
||||||
|
#self.__thread['spawnEvents'] = threading.Thread(name='spawnEvents', target=self.__spawnEvents)
|
||||||
|
#self.__thread['spawnEvents'].start()
|
||||||
|
|
||||||
|
def __readXML(self):
|
||||||
|
"Parses the incoming stream, adding to xmlin queue as it goes"
|
||||||
|
#build cElementTree object from expat was we go
|
||||||
|
#self.filesocket = self.socket.makefile('rb',0) #this is broken in python2.5, but works in python3.0
|
||||||
|
self.filesocket = _fileobject(self.socket)
|
||||||
|
edepth = 0
|
||||||
|
root = None
|
||||||
|
for (event, xmlobj) in cElementTree.iterparse(self.filesocket, ('end', 'start')):
|
||||||
|
if edepth == 0: # and xmlobj.tag.split('}', 1)[-1] == self.basetag:
|
||||||
|
if event == 'start':
|
||||||
|
root = xmlobj
|
||||||
|
self.start_stream_handler(root)
|
||||||
|
if event == 'end':
|
||||||
|
edepth += -1
|
||||||
|
if edepth == 0 and event == 'end':
|
||||||
|
return False
|
||||||
|
elif edepth == 1:
|
||||||
|
#self.xmlin.put(xmlobj)
|
||||||
|
try:
|
||||||
|
self.__spawnEvent(xmlobj)
|
||||||
|
except RestartStream:
|
||||||
|
return True
|
||||||
|
except CloseStream:
|
||||||
|
return False
|
||||||
|
if root:
|
||||||
|
root.clear()
|
||||||
|
if event == 'start':
|
||||||
|
edepth += 1
|
||||||
|
|
||||||
|
def sendRaw(self, data):
|
||||||
|
logging.debug("SEND: %s" % data)
|
||||||
|
if type(data) == type(u''):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
try:
|
||||||
|
self.socket.send(data)
|
||||||
|
except socket.error,(errno, strerror):
|
||||||
|
logging.error("Disconnected. Socket Error #%s: %s" % (errno,strerror))
|
||||||
|
self.state.set('connected', False)
|
||||||
|
self.disconnect(reconnect=True)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def disconnect(self, reconnect=False):
|
||||||
|
self.state.set('reconnect', reconnect)
|
||||||
|
if self.state['connected']:
|
||||||
|
self.sendRaw(self.stream_footer)
|
||||||
|
#send end of stream
|
||||||
|
#wait for end of stream back
|
||||||
|
try:
|
||||||
|
self.socket.close()
|
||||||
|
self.filesocket.close()
|
||||||
|
self.socket.shutdown(socket.SHUT_RDWR)
|
||||||
|
except socket.error,(errno,strerror):
|
||||||
|
logging.warning("Error while disconnecting. Socket Error #%s: %s" % (errno, strerror))
|
||||||
|
if self.state['processing']:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def reconnect(self):
|
||||||
|
self.state.set('tls',False)
|
||||||
|
self.state.set('ssl',False)
|
||||||
|
time.sleep(1)
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
def __spawnEvent(self, xmlobj):
|
||||||
|
"watching xmlOut and processes handlers"
|
||||||
|
#convert XML into Stanza
|
||||||
|
logging.debug("PROCESSING: %s" % xmlobj.tag)
|
||||||
|
stanza = None
|
||||||
|
for stanza_class in self.__root_stanza:
|
||||||
|
if self.__root_stanza[stanza_class].match(xmlobj):
|
||||||
|
stanza = stanza_class(self, xmlobj)
|
||||||
|
break
|
||||||
|
if stanza is None:
|
||||||
|
stanza = StanzaBase(self, xmlobj)
|
||||||
|
for handler in self.__handlers:
|
||||||
|
if handler.match(xmlobj):
|
||||||
|
handler.run(stanza)
|
||||||
|
if handler.checkDelete(): self.__handlers.pop(self.__handlers.index(handler))
|
||||||
|
|
||||||
|
#loop through handlers and test match
|
||||||
|
#spawn threads as necessary, call handlers, sending Stanza
|
||||||
|
|
||||||
|
def registerHandler(self, handler, before=None, after=None):
|
||||||
|
"Add handler with matcher class and parameters."
|
||||||
|
self.__handlers.append(handler)
|
||||||
|
|
||||||
|
def removeHandler(self, name):
|
||||||
|
"Removes the handler."
|
||||||
|
idx = 0
|
||||||
|
for handler in self.__handlers:
|
||||||
|
if handler.name == name:
|
||||||
|
self.__handlers.pop(idx)
|
||||||
|
return
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
def registerStanza(self, matcher, stanza_class, root=True):
|
||||||
|
"Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects."
|
||||||
|
if root:
|
||||||
|
self.__root_stanza[stanza_class] = matcher
|
||||||
|
else:
|
||||||
|
self.__stanza[stanza_class] = matcher
|
||||||
|
|
||||||
|
def registerStanzaExtension(self, stanza_class, stanza_extension):
|
||||||
|
if stanza_class not in stanza_extensions:
|
||||||
|
stanza_extensions[stanza_class] = [stanza_extension]
|
||||||
|
else:
|
||||||
|
stanza_extensions[stanza_class].append(stanza_extension)
|
||||||
|
|
||||||
|
def removeStanza(self, stanza_class, root=False):
|
||||||
|
"Removes the stanza's registration."
|
||||||
|
if root:
|
||||||
|
del self.__root_stanza[stanza_class]
|
||||||
|
else:
|
||||||
|
del self.__stanza[stanza_class]
|
||||||
|
|
||||||
|
def removeStanzaExtension(self, stanza_class, stanza_extension):
|
||||||
|
stanza_extension[stanza_class].pop(stanza_extension)
|
||||||
|
|
||||||
|
def tostring(self, xml, xmlns='', stringbuffer=''):
|
||||||
|
newoutput = [stringbuffer]
|
||||||
|
#TODO respect ET mapped namespaces
|
||||||
|
itag = xml.tag.split('}', 1)[-1]
|
||||||
|
if '}' in xml.tag:
|
||||||
|
ixmlns = xml.tag.split('}', 1)[0][1:]
|
||||||
|
else:
|
||||||
|
ixmlns = ''
|
||||||
|
nsbuffer = ''
|
||||||
|
if xmlns != ixmlns and ixmlns != '':
|
||||||
|
if ixmlns in self.namespace_map:
|
||||||
|
if self.namespace_map[ixmlns] != '':
|
||||||
|
itag = "%s:%s" % (self.namespace_map[ixmlns], itag)
|
||||||
|
else:
|
||||||
|
nsbuffer = """ xmlns="%s\"""" % ixmlns
|
||||||
|
newoutput.append("<%s" % itag)
|
||||||
|
newoutput.append(nsbuffer)
|
||||||
|
for attrib in xml.attrib:
|
||||||
|
newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib])))
|
||||||
|
if len(xml) or xml.text or xml.tail:
|
||||||
|
newoutput.append(">")
|
||||||
|
if xml.text:
|
||||||
|
newoutput.append(self.xmlesc(xml.text))
|
||||||
|
if len(xml):
|
||||||
|
for child in xml.getchildren():
|
||||||
|
newoutput.append(self.tostring(child, ixmlns))
|
||||||
|
newoutput.append("</%s>" % (itag, ))
|
||||||
|
if xml.tail:
|
||||||
|
newoutput.append(self.xmlesc(xml.tail))
|
||||||
|
elif xml.text:
|
||||||
|
newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag))
|
||||||
|
else:
|
||||||
|
newoutput.append(" />")
|
||||||
|
return ''.join(newoutput)
|
||||||
|
|
||||||
|
def xmlesc(self, text):
|
||||||
|
if type(text) != types.UnicodeType:
|
||||||
|
text = list(unicode(text, 'utf-8', 'ignore'))
|
||||||
|
else:
|
||||||
|
text = list(text)
|
||||||
|
cc = 0
|
||||||
|
matches = ('&', '<', '"', '>', "'")
|
||||||
|
for c in text:
|
||||||
|
if c in matches:
|
||||||
|
if c == '&':
|
||||||
|
text[cc] = u'&'
|
||||||
|
elif c == '<':
|
||||||
|
text[cc] = u'<'
|
||||||
|
elif c == '>':
|
||||||
|
text[cc] = u'>'
|
||||||
|
elif c == "'":
|
||||||
|
text[cc] = u'''
|
||||||
|
elif self.escape_quotes:
|
||||||
|
text[cc] = u'"'
|
||||||
|
cc += 1
|
||||||
|
return ''.join(text)
|
||||||
|
|
||||||
|
def start_stream_handler(self, xml):
|
||||||
|
"""Meant to be overridden"""
|
||||||
|
pass
|
Loading…
Reference in a new issue