commit 96b103b27599e5af247c1e3b95d62c80c1e32a63 Author: Nathan Fritz Date: Wed Jun 3 22:56:51 2009 +0000 moved seesmic branch to trunk diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..3a39d83 --- /dev/null +++ b/MANIFEST @@ -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 diff --git a/example.py b/example.py new file mode 100644 index 0000000..dc7066e --- /dev/null +++ b/example.py @@ -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.") diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..4b983b1 --- /dev/null +++ b/ez_setup.py @@ -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:]) + + + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7dbd619 --- /dev/null +++ b/setup.py @@ -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' ], + ) + diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py new file mode 100644 index 0000000..d7eff57 --- /dev/null +++ b/sleekxmpp/__init__.py @@ -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 = """""" % (self.server,self.default_ns) + self.stream_footer = "" + #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("" % self.default_ns), self._handlePresenceSubscribe, thread=True)) + self.registerFeature("", self.handler_starttls, True) + self.registerFeature("", self.handler_sasl_auth, True) + self.registerFeature("", self.handler_bind_resource) + self.registerFeature("", 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("", 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("", self.handler_auth_success) + self.add_handler("", 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("""%s""" % 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) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py new file mode 100644 index 0000000..ae2b063 --- /dev/null +++ b/sleekxmpp/basexmpp.py @@ -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("" % self.default_ns),MatchXMLMask("" % self.default_ns),MatchXMLMask("" % self.default_ns))), self._handleMessage)) + self.registerHandler(Callback('Presence', MatchMany((MatchXMLMask("" % self.default_ns),MatchXMLMask("" % self.default_ns),MatchXMLMask("" % self.default_ns))), self._handlePresence)) + self.registerHandler(Callback('PresenceSubscribe', MatchMany((MatchXMLMask("" % self.default_ns),MatchXMLMask("" % 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('' + mhtml + '') + 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 diff --git a/sleekxmpp/component_example.py b/sleekxmpp/component_example.py new file mode 100644 index 0000000..0480237 --- /dev/null +++ b/sleekxmpp/component_example.py @@ -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.") diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py new file mode 100755 index 0000000..72111e3 --- /dev/null +++ b/sleekxmpp/componentxmpp.py @@ -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 = "" % jid + self.stream_footer = "" + self.server_host = host + self.server_port = port + self.set_jid(jid) + self.secret = secret + self.registerHandler(Callback('PresenceProbe', MatchXMLMask("" % self.default_ns), self._handlePresenceProbe)) + self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake)) + self.registerHandler(Callback('PresenceSubscription', MatchMany(\ + (MatchXMLMask("" % self.default_ns), \ + MatchXMLMask("" % self.default_ns), \ + MatchXMLMask("" % self.default_ns), \ + MatchXMLMask("" % 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) diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py new file mode 100644 index 0000000..1868365 --- /dev/null +++ b/sleekxmpp/plugins/__init__.py @@ -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'] diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py new file mode 100644 index 0000000..685833f --- /dev/null +++ b/sleekxmpp/plugins/base.py @@ -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 diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py new file mode 100644 index 0000000..8bc4423 --- /dev/null +++ b/sleekxmpp/plugins/gmail_notify.py @@ -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("" % 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) diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py new file mode 100644 index 0000000..c85b09a --- /dev/null +++ b/sleekxmpp/plugins/xep_0004.py @@ -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("", 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 + diff --git a/sleekxmpp/plugins/xep_0009.py b/sleekxmpp/plugins/xep_0009.py new file mode 100644 index 0000000..c6b7b5d --- /dev/null +++ b/sleekxmpp/plugins/xep_0009.py @@ -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) #... + 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 = {} #{'':['',...],...} + 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("", self._callMethod) + self.xmpp.add_handler("", self._callResult) + self.xmpp.add_handler("", 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 diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py new file mode 100644 index 0000000..d379530 --- /dev/null +++ b/sleekxmpp/plugins/xep_0030.py @@ -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("" % self.xmpp.default_ns, self.info_handler) + self.xmpp.add_handler("" % 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.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 diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py new file mode 100644 index 0000000..a85bfec --- /dev/null +++ b/sleekxmpp/plugins/xep_0045.py @@ -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("" % self.xmpp.default_ns, self.handle_groupchat_message) + self.xmpp.add_handler("", 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.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.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.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.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() diff --git a/sleekxmpp/plugins/xep_0050.py b/sleekxmpp/plugins/xep_0050.py new file mode 100644 index 0000000..bbfd1c4 --- /dev/null +++ b/sleekxmpp/plugins/xep_0050.py @@ -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("" % self.xmpp.default_ns, self.handler_command) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_next, threaded=True) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_cancel) + self.xmpp.add_handler("" % 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 diff --git a/sleekxmpp/plugins/xep_0060.py b/sleekxmpp/plugins/xep_0060.py new file mode 100644 index 0000000..015c221 --- /dev/null +++ b/sleekxmpp/plugins/xep_0060.py @@ -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, "" % 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, "" % 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, "" % 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("" % id, self.handlerCreateNodeResponse) + result = self.xmpp.send(iq, "" % 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, "" % 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, "" % 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, "" % 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, "" % 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, "" % 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, "" % 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, "" % 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, '') + diff --git a/sleekxmpp/plugins/xep_0078.py b/sleekxmpp/plugins/xep_0078.py new file mode 100644 index 0000000..28aaeb2 --- /dev/null +++ b/sleekxmpp/plugins/xep_0078.py @@ -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("", 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") diff --git a/sleekxmpp/plugins/xep_0086.py b/sleekxmpp/plugins/xep_0086.py new file mode 100644 index 0000000..6871ef3 --- /dev/null +++ b/sleekxmpp/plugins/xep_0086.py @@ -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 diff --git a/sleekxmpp/plugins/xep_0092.py b/sleekxmpp/plugins/xep_0092.py new file mode 100644 index 0000000..97346d9 --- /dev/null +++ b/sleekxmpp/plugins/xep_0092.py @@ -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("" % 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, "" % (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 + diff --git a/sleekxmpp/plugins/xep_0199.py b/sleekxmpp/plugins/xep_0199.py new file mode 100644 index 0000000..cab84ac --- /dev/null +++ b/sleekxmpp/plugins/xep_0199.py @@ -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("" % 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 diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py new file mode 100644 index 0000000..765748c --- /dev/null +++ b/sleekxmpp/stanza/__init__.py @@ -0,0 +1 @@ +__all__ = ['presence'] diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py new file mode 100644 index 0000000..e69de29 diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py new file mode 100644 index 0000000..e69de29 diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py new file mode 100644 index 0000000..0733e9a --- /dev/null +++ b/sleekxmpp/stanza/presence.py @@ -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},) diff --git a/sleekxmpp/tests/testpubsub.py b/sleekxmpp/tests/testpubsub.py new file mode 100755 index 0000000..3f00518 --- /dev/null +++ b/sleekxmpp/tests/testpubsub.py @@ -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("", self.pubsubEventHandler, threaded=True) + self.add_event_handler("session_start", self.start, threaded=True) + self.add_handler("", 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("") diff --git a/sleekxmpp/xmlstream/__init__.py b/sleekxmpp/xmlstream/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sleekxmpp/xmlstream/handler/__init__.py b/sleekxmpp/xmlstream/handler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py new file mode 100644 index 0000000..810aac9 --- /dev/null +++ b/sleekxmpp/xmlstream/handler/base.py @@ -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 diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py new file mode 100644 index 0000000..e3ef8cc --- /dev/null +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -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 diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py new file mode 100644 index 0000000..7c06ddf --- /dev/null +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -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 diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py new file mode 100644 index 0000000..50d3d5f --- /dev/null +++ b/sleekxmpp/xmlstream/handler/xmlcallback.py @@ -0,0 +1,7 @@ +import threading +from . callback import Callback + +class XMLCallback(Callback): + + def run(self, payload): + Callback.run(self, payload.xml) diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py new file mode 100644 index 0000000..9b2b339 --- /dev/null +++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py @@ -0,0 +1,6 @@ +from . waiter import Waiter + +class XMLWaiter(Waiter): + + def run(self, payload): + Waiter.run(self, payload.xml) diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py new file mode 100644 index 0000000..97e4465 --- /dev/null +++ b/sleekxmpp/xmlstream/matcher/base.py @@ -0,0 +1,8 @@ + +class MatcherBase(object): + + def __init__(self, criteria): + self._criteria = criteria + + def match(self, xml): + return False diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py new file mode 100644 index 0000000..42e92b2 --- /dev/null +++ b/sleekxmpp/xmlstream/matcher/many.py @@ -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 diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py new file mode 100644 index 0000000..02a644c --- /dev/null +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -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 diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py new file mode 100644 index 0000000..b141dd8 --- /dev/null +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -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 diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py new file mode 100644 index 0000000..5232ff5 --- /dev/null +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -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) diff --git a/sleekxmpp/xmlstream/statemachine.py b/sleekxmpp/xmlstream/statemachine.py new file mode 100644 index 0000000..66aa358 --- /dev/null +++ b/sleekxmpp/xmlstream/statemachine.py @@ -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 + diff --git a/sleekxmpp/xmlstream/test.py b/sleekxmpp/xmlstream/test.py new file mode 100644 index 0000000..a45fb8b --- /dev/null +++ b/sleekxmpp/xmlstream/test.py @@ -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() diff --git a/sleekxmpp/xmlstream/test.xml b/sleekxmpp/xmlstream/test.xml new file mode 100644 index 0000000..d20dd82 --- /dev/null +++ b/sleekxmpp/xmlstream/test.xml @@ -0,0 +1,2 @@ + + diff --git a/sleekxmpp/xmlstream/testclient.py b/sleekxmpp/xmlstream/testclient.py new file mode 100644 index 0000000..50eb6c5 --- /dev/null +++ b/sleekxmpp/xmlstream/testclient.py @@ -0,0 +1,13 @@ +import socket +import time + +s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +s.connect(('localhost', 5228)) +s.send("") +#s.flush() +s.send("") +s.send("") +s.send("") +s.send("") +#s.flush() +s.close() diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py new file mode 100644 index 0000000..ad2c5a1 --- /dev/null +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -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 = "" + self.stream_footer = "" + + 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("" % (itag, )) + if xml.tail: + newoutput.append(self.xmlesc(xml.tail)) + elif xml.text: + newoutput.append(">%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