aboutsummaryrefslogtreecommitdiffstats
path: root/sleekxmpp
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp')
-rw-r--r--sleekxmpp/__init__.py16
-rw-r--r--sleekxmpp/basexmpp.py640
-rw-r--r--sleekxmpp/clientxmpp.py433
-rw-r--r--sleekxmpp/componentxmpp.py138
-rw-r--r--sleekxmpp/exceptions.py49
-rw-r--r--sleekxmpp/plugins/__init__.py9
-rw-r--r--sleekxmpp/plugins/base.py26
-rw-r--r--sleekxmpp/plugins/gmail_notify.py146
-rw-r--r--sleekxmpp/plugins/jobs.py46
-rw-r--r--sleekxmpp/plugins/old_0004.py417
-rw-r--r--sleekxmpp/plugins/stanza_pubsub.py555
-rw-r--r--sleekxmpp/plugins/xep_0004.py392
-rw-r--r--sleekxmpp/plugins/xep_0009.py277
-rw-r--r--sleekxmpp/plugins/xep_0030.py327
-rw-r--r--sleekxmpp/plugins/xep_0033.py161
-rw-r--r--sleekxmpp/plugins/xep_0045.py333
-rw-r--r--sleekxmpp/plugins/xep_0050.py133
-rw-r--r--sleekxmpp/plugins/xep_0060.py309
-rw-r--r--sleekxmpp/plugins/xep_0078.py69
-rw-r--r--sleekxmpp/plugins/xep_0085.py101
-rw-r--r--sleekxmpp/plugins/xep_0086.py49
-rw-r--r--sleekxmpp/plugins/xep_0092.py56
-rw-r--r--sleekxmpp/plugins/xep_0128.py51
-rw-r--r--sleekxmpp/plugins/xep_0199.py59
-rw-r--r--sleekxmpp/stanza/__init__.py13
-rw-r--r--sleekxmpp/stanza/atom.py26
-rw-r--r--sleekxmpp/stanza/error.py141
-rw-r--r--sleekxmpp/stanza/htmlim.py97
-rw-r--r--sleekxmpp/stanza/iq.py183
-rw-r--r--sleekxmpp/stanza/message.py165
-rw-r--r--sleekxmpp/stanza/nick.py89
-rw-r--r--sleekxmpp/stanza/presence.py186
-rw-r--r--sleekxmpp/stanza/rootstanza.py66
-rw-r--r--sleekxmpp/stanza/roster.py125
-rw-r--r--sleekxmpp/test/__init__.py11
-rw-r--r--sleekxmpp/test/livesocket.py145
-rw-r--r--sleekxmpp/test/mocksocket.py140
-rw-r--r--sleekxmpp/test/sleektest.py763
-rw-r--r--sleekxmpp/thirdparty/__init__.py0
-rw-r--r--sleekxmpp/thirdparty/statemachine.py287
-rw-r--r--sleekxmpp/xmlstream/__init__.py19
-rw-r--r--sleekxmpp/xmlstream/filesocket.py41
-rw-r--r--sleekxmpp/xmlstream/handler/__init__.py14
-rw-r--r--sleekxmpp/xmlstream/handler/base.py89
-rw-r--r--sleekxmpp/xmlstream/handler/callback.py84
-rw-r--r--sleekxmpp/xmlstream/handler/waiter.py98
-rw-r--r--sleekxmpp/xmlstream/handler/xmlcallback.py36
-rw-r--r--sleekxmpp/xmlstream/handler/xmlwaiter.py33
-rw-r--r--sleekxmpp/xmlstream/jid.py123
-rw-r--r--sleekxmpp/xmlstream/matcher/__init__.py16
-rw-r--r--sleekxmpp/xmlstream/matcher/base.py34
-rw-r--r--sleekxmpp/xmlstream/matcher/id.py32
-rw-r--r--sleekxmpp/xmlstream/matcher/many.py40
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py38
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py155
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py79
-rw-r--r--sleekxmpp/xmlstream/scheduler.py202
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py1162
-rw-r--r--sleekxmpp/xmlstream/test.py23
-rw-r--r--sleekxmpp/xmlstream/test.xml2
-rw-r--r--sleekxmpp/xmlstream/testclient.py13
-rw-r--r--sleekxmpp/xmlstream/tostring/__init__.py19
-rw-r--r--sleekxmpp/xmlstream/tostring/tostring.py95
-rw-r--r--sleekxmpp/xmlstream/tostring/tostring26.py101
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py892
65 files changed, 10669 insertions, 0 deletions
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py
new file mode 100644
index 0000000..20f4367
--- /dev/null
+++ b/sleekxmpp/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.basexmpp import BaseXMPP
+from sleekxmpp.clientxmpp import ClientXMPP
+from sleekxmpp.componentxmpp import ComponentXMPP
+from sleekxmpp.stanza import Message, Presence, Iq
+from sleekxmpp.xmlstream.handler import *
+from sleekxmpp.xmlstream import XMLStream, RestartStream
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
new file mode 100644
index 0000000..1e8441a
--- /dev/null
+++ b/sleekxmpp/basexmpp.py
@@ -0,0 +1,640 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import with_statement, unicode_literals
+
+import sys
+import copy
+import logging
+
+import sleekxmpp
+from sleekxmpp import plugins
+
+from sleekxmpp.stanza import Message, Presence, Iq, Error
+from sleekxmpp.stanza.roster import Roster
+from sleekxmpp.stanza.nick import Nick
+from sleekxmpp.stanza.htmlim import HTMLIM
+
+from sleekxmpp.xmlstream import XMLStream, JID, tostring
+from sleekxmpp.xmlstream import ET, register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+
+
+# Flag indicating if DNS SRV records are available for use.
+SRV_SUPPORT = True
+try:
+ import dns.resolver
+except:
+ SRV_SUPPORT = False
+
+
+# In order to make sure that Unicode is handled properly
+# in Python 2.x, reset the default encoding.
+if sys.version_info < (3, 0):
+ reload(sys)
+ sys.setdefaultencoding('utf8')
+
+
+class BaseXMPP(XMLStream):
+
+ """
+ The BaseXMPP class adapts the generic XMLStream class for use
+ with XMPP. It also provides a plugin mechanism to easily extend
+ and add support for new XMPP features.
+
+ Attributes:
+ auto_authorize -- Manage automatically accepting roster
+ subscriptions.
+ auto_subscribe -- Manage automatically requesting mutual
+ subscriptions.
+ is_component -- Indicates if this stream is for an XMPP component.
+ jid -- The XMPP JID for this stream.
+ plugin -- A dictionary of loaded plugins.
+ plugin_config -- A dictionary of plugin configurations.
+ plugin_whitelist -- A list of approved plugins.
+ sentpresence -- Indicates if an initial presence has been sent.
+ roster -- A dictionary containing subscribed JIDs and
+ their presence statuses.
+
+ Methods:
+ Iq -- Factory for creating an Iq stanzas.
+ Message -- Factory for creating Message stanzas.
+ Presence -- Factory for creating Presence stanzas.
+ get -- Return a plugin given its name.
+ make_iq -- Create and initialize an Iq stanza.
+ make_iq_error -- Create an Iq stanza of type 'error'.
+ make_iq_get -- Create an Iq stanza of type 'get'.
+ make_iq_query -- Create an Iq stanza with a given query.
+ make_iq_result -- Create an Iq stanza of type 'result'.
+ make_iq_set -- Create an Iq stanza of type 'set'.
+ make_message -- Create and initialize a Message stanza.
+ make_presence -- Create and initialize a Presence stanza.
+ make_query_roster -- Create a roster query.
+ process -- Overrides XMLStream.process.
+ register_plugin -- Load and configure a plugin.
+ register_plugins -- Load and configure multiple plugins.
+ send_message -- Create and send a Message stanza.
+ send_presence -- Create and send a Presence stanza.
+ send_presence_subscribe -- Send a subscription request.
+ """
+
+ def __init__(self, default_ns='jabber:client'):
+ """
+ Adapt an XML stream for use with XMPP.
+
+ Arguments:
+ default_ns -- Ensure that the correct default XML namespace
+ is used during initialization.
+ """
+ XMLStream.__init__(self)
+
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.registerPlugin = self.register_plugin
+ self.makeIq = self.make_iq
+ self.makeIqGet = self.make_iq_get
+ self.makeIqResult = self.make_iq_result
+ self.makeIqSet = self.make_iq_set
+ self.makeIqError = self.make_iq_error
+ self.makeIqQuery = self.make_iq_query
+ self.makeQueryRoster = self.make_query_roster
+ self.makeMessage = self.make_message
+ self.makePresence = self.make_presence
+ self.sendMessage = self.send_message
+ self.sendPresence = self.send_presence
+ self.sendPresenceSubscription = self.send_presence_subscription
+
+ self.default_ns = default_ns
+ self.stream_ns = 'http://etherx.jabber.org/streams'
+
+ self.boundjid = JID("")
+
+ self.plugin = {}
+ self.roster = {}
+ self.is_component = False
+ self.auto_authorize = True
+ self.auto_subscribe = True
+
+ self.sentpresence = False
+
+ self.register_handler(
+ Callback('IM',
+ MatchXPath('{%s}message/{%s}body' % (self.default_ns,
+ self.default_ns)),
+ self._handle_message))
+ self.register_handler(
+ Callback('Presence',
+ MatchXPath("{%s}presence" % self.default_ns),
+ self._handle_presence))
+
+ self.add_event_handler('presence_subscribe',
+ self._handle_subscribe)
+ self.add_event_handler('disconnected',
+ self._handle_disconnected)
+
+ # Set up the XML stream with XMPP's root stanzas.
+ self.registerStanza(Message)
+ self.registerStanza(Iq)
+ self.registerStanza(Presence)
+
+ # Initialize a few default stanza plugins.
+ register_stanza_plugin(Iq, Roster)
+ register_stanza_plugin(Message, Nick)
+ register_stanza_plugin(Message, HTMLIM)
+
+ def process(self, *args, **kwargs):
+ """
+ Ensure that plugin inter-dependencies are handled before starting
+ event processing.
+
+ Overrides XMLStream.process.
+ """
+ for name in self.plugin:
+ if not self.plugin[name].post_inited:
+ self.plugin[name].post_init()
+ return XMLStream.process(self, *args, **kwargs)
+
+ def register_plugin(self, plugin, pconfig={}, module=None):
+ """
+ Register and configure a plugin for use in this stream.
+
+ Arguments:
+ plugin -- The name of the plugin class. Plugin names must
+ be unique.
+ pconfig -- A dictionary of configuration data for the plugin.
+ Defaults to an empty dictionary.
+ module -- Optional refence to the module containing the plugin
+ class if using custom plugins.
+ """
+ try:
+ # Import the given module that contains the plugin.
+ if not module:
+ module = sleekxmpp.plugins
+ module = __import__("%s.%s" % (module.__name__, plugin),
+ globals(), locals(), [plugin])
+ if isinstance(module, str):
+ # We probably want to load a module from outside
+ # the sleekxmpp package, so leave out the globals().
+ module = __import__(module, fromlist=[plugin])
+
+ # Load the plugin class from the module.
+ self.plugin[plugin] = getattr(module, plugin)(self, pconfig)
+
+ # Let XEP implementing plugins have some extra logging info.
+ xep = ''
+ if hasattr(self.plugin[plugin], 'xep'):
+ xep = "(XEP-%s) " % self.plugin[plugin].xep
+
+ desc = (xep, self.plugin[plugin].description)
+ logging.debug("Loaded Plugin %s%s" % desc)
+ except:
+ logging.exception("Unable to load plugin: %s", plugin)
+
+ def register_plugins(self):
+ """
+ Register and initialize all built-in plugins.
+
+ Optionally, the list of plugins loaded may be limited to those
+ contained in self.plugin_whitelist.
+
+ Plugin configurations stored in self.plugin_config will be used.
+ """
+ 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.register_plugin(plugin,
+ self.plugin_config.get(plugin, {}))
+ else:
+ raise NameError("Plugin %s not in plugins.__all__." % plugin)
+
+ # Resolve plugin inter-dependencies.
+ for plugin in self.plugin:
+ self.plugin[plugin].post_init()
+
+ def __getitem__(self, key):
+ """
+ Return a plugin given its name, if it has been registered.
+ """
+ if key in self.plugin:
+ return self.plugin[key]
+ else:
+ logging.warning("""Plugin "%s" is not loaded.""" % key)
+ return False
+
+ def get(self, key, default):
+ """
+ Return a plugin given its name, if it has been registered.
+ """
+ return self.plugin.get(key, default)
+
+ def Message(self, *args, **kwargs):
+ """Create a Message stanza associated with this stream."""
+ return Message(self, *args, **kwargs)
+
+ def Iq(self, *args, **kwargs):
+ """Create an Iq stanza associated with this stream."""
+ return Iq(self, *args, **kwargs)
+
+ def Presence(self, *args, **kwargs):
+ """Create a Presence stanza associated with this stream."""
+ return Presence(self, *args, **kwargs)
+
+ def make_iq(self, id=0, ifrom=None):
+ """
+ Create a new Iq stanza with a given Id and from JID.
+
+ Arguments:
+ id -- An ideally unique ID value for this stanza thread.
+ Defaults to 0.
+ ifrom -- The from JID to use for this stanza.
+ """
+ return self.Iq()._set_stanza_values({'id': str(id),
+ 'from': ifrom})
+
+ def make_iq_get(self, queryxmlns=None):
+ """
+ Create an Iq stanza of type 'get'.
+
+ Optionally, a query element may be added.
+
+ Arguments:
+ queryxmlns -- The namespace of the query to use.
+ """
+ return self.Iq()._set_stanza_values({'type': 'get',
+ 'query': queryxmlns})
+
+ def make_iq_result(self, id):
+ """
+ Create an Iq stanza of type 'result' with the given ID value.
+
+ Arguments:
+ id -- An ideally unique ID value. May use self.new_id().
+ """
+ return self.Iq()._set_stanza_values({'id': id,
+ 'type': 'result'})
+
+ def make_iq_set(self, sub=None):
+ """
+ Create an Iq stanza of type 'set'.
+
+ Optionally, a substanza may be given to use as the
+ stanza's payload.
+
+ Arguments:
+ sub -- A stanza or XML object to use as the Iq's payload.
+ """
+ iq = self.Iq()._set_stanza_values({'type': 'set'})
+ if sub != None:
+ iq.append(sub)
+ return iq
+
+ def make_iq_error(self, id, type='cancel',
+ condition='feature-not-implemented', text=None):
+ """
+ Create an Iq stanza of type 'error'.
+
+ Arguments:
+ id -- An ideally unique ID value. May use self.new_id().
+ type -- The type of the error, such as 'cancel' or 'modify'.
+ Defaults to 'cancel'.
+ condition -- The error condition.
+ Defaults to 'feature-not-implemented'.
+ text -- A message describing the cause of the error.
+ """
+ iq = self.Iq()._set_stanza_values({'id': id})
+ iq['error']._set_stanza_values({'type': type,
+ 'condition': condition,
+ 'text': text})
+ return iq
+
+ def make_iq_query(self, iq=None, xmlns=''):
+ """
+ Create or modify an Iq stanza to use the given
+ query namespace.
+
+ Arguments:
+ iq -- Optional Iq stanza to modify. A new
+ stanza is created otherwise.
+ xmlns -- The query's namespace.
+ """
+ if not iq:
+ iq = self.Iq()
+ iq['query'] = xmlns
+ return iq
+
+ def make_query_roster(self, iq=None):
+ """
+ Create a roster query element.
+
+ Arguments:
+ iq -- Optional Iq stanza to modify. A new stanza
+ is created otherwise.
+ """
+ if iq:
+ iq['query'] = 'jabber:iq:roster'
+ return ET.Element("{jabber:iq:roster}query")
+
+ def make_message(self, mto, mbody=None, msubject=None, mtype=None,
+ mhtml=None, mfrom=None, mnick=None):
+ """
+ Create and initialize a new Message stanza.
+
+ Arguments:
+ mto -- The recipient of the message.
+ mbody -- The main contents of the message.
+ msubject -- Optional subject for the message.
+ mtype -- The message's type, such as 'chat' or 'groupchat'.
+ mhtml -- Optional HTML body content.
+ mfrom -- The sender of the message. If sending from a client,
+ be aware that some servers require that the full JID
+ of the sender be used.
+ mnick -- Optional nickname of the sender.
+ """
+ message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
+ message['body'] = mbody
+ message['subject'] = msubject
+ if mnick is not None:
+ message['nick'] = mnick
+ if mhtml is not None:
+ message['html']['body'] = mhtml
+ return message
+
+ def make_presence(self, pshow=None, pstatus=None, ppriority=None,
+ pto=None, ptype=None, pfrom=None):
+ """
+ Create and initialize a new Presence stanza.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ ptype -- The type of presence, such as 'subscribe'.
+ pfrom -- The sender of the presence.
+ """
+ presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
+ if pshow is not None:
+ presence['type'] = pshow
+ if pfrom is None:
+ presence['from'] = self.boundjid.full
+ presence['priority'] = ppriority
+ presence['status'] = pstatus
+ return presence
+
+ def send_message(self, mto, mbody, msubject=None, mtype=None,
+ mhtml=None, mfrom=None, mnick=None):
+ """
+ Create, initialize, and send a Message stanza.
+
+
+ """
+ self.makeMessage(mto, mbody, msubject, mtype,
+ mhtml, mfrom, mnick).send()
+
+ def send_presence(self, pshow=None, pstatus=None, ppriority=None,
+ pto=None, pfrom=None, ptype=None):
+ """
+ Create, initialize, and send a Presence stanza.
+
+ Arguments:
+ pshow -- The presence's show value.
+ pstatus -- The presence's status message.
+ ppriority -- This connections' priority.
+ pto -- The recipient of a directed presence.
+ ptype -- The type of presence, such as 'subscribe'.
+ pfrom -- The sender of the presence.
+ """
+ self.makePresence(pshow, pstatus, ppriority, pto,
+ ptype=ptype, pfrom=pfrom).send()
+ # Unexpected errors may occur if
+ if not self.sentpresence:
+ self.event('sent_presence')
+ self.sentpresence = True
+
+ def send_presence_subscription(self, pto, pfrom=None,
+ ptype='subscribe', pnick=None):
+ """
+ Create, initialize, and send a Presence stanza of type 'subscribe'.
+
+ Arguments:
+ pto -- The recipient of a directed presence.
+ pfrom -- The sender of the presence.
+ ptype -- The type of presence. Defaults to 'subscribe'.
+ pnick -- Nickname of the presence's sender.
+ """
+ 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)
+ presence.send()
+
+ @property
+ def jid(self):
+ """
+ Attribute accessor for bare jid
+ """
+ logging.warning("jid property deprecated. Use boundjid.bare")
+ return self.boundjid.bare
+
+ @jid.setter
+ def jid(self, value):
+ logging.warning("jid property deprecated. Use boundjid.bare")
+ self.boundjid.bare = value
+
+ @property
+ def fulljid(self):
+ """
+ Attribute accessor for full jid
+ """
+ logging.warning("fulljid property deprecated. Use boundjid.full")
+ return self.boundjid.full
+
+ @fulljid.setter
+ def fulljid(self, value):
+ logging.warning("fulljid property deprecated. Use boundjid.full")
+ self.boundjid.full = value
+
+ @property
+ def resource(self):
+ """
+ Attribute accessor for jid resource
+ """
+ logging.warning("resource property deprecated. Use boundjid.resource")
+ return self.boundjid.resource
+
+ @resource.setter
+ def resource(self, value):
+ logging.warning("fulljid property deprecated. Use boundjid.full")
+ self.boundjid.resource = value
+
+ @property
+ def username(self):
+ """
+ Attribute accessor for jid usernode
+ """
+ logging.warning("username property deprecated. Use boundjid.user")
+ return self.boundjid.user
+
+ @username.setter
+ def username(self, value):
+ logging.warning("username property deprecated. Use boundjid.user")
+ self.boundjid.user = value
+
+ @property
+ def server(self):
+ """
+ Attribute accessor for jid host
+ """
+ logging.warning("server property deprecated. Use boundjid.host")
+ return self.boundjid.server
+
+ @server.setter
+ def server(self, value):
+ logging.warning("server property deprecated. Use boundjid.host")
+ self.boundjid.server = value
+
+ def set_jid(self, jid):
+ """Rip a JID apart and claim it as our own."""
+ logging.debug("setting jid to %s" % jid)
+ self.boundjid.full = jid
+
+ def getjidresource(self, fulljid):
+ if '/' in fulljid:
+ return fulljid.split('/', 1)[-1]
+ else:
+ return ''
+
+ def getjidbare(self, fulljid):
+ return fulljid.split('/', 1)[0]
+
+ def _handle_disconnected(self, event):
+ """When disconnected, reset the roster"""
+ self.roster = {}
+
+ def _handle_message(self, msg):
+ """Process incoming message stanzas."""
+ self.event('message', msg)
+
+ def _handle_presence(self, presence):
+ """
+ Process incoming presence stanzas.
+
+ Update the roster with presence information.
+ """
+ self.event("presence_%s" % presence['type'], presence)
+
+ # Check for changes in subscription state.
+ if presence['type'] in ('subscribe', 'subscribed',
+ 'unsubscribe', 'unsubscribed'):
+ self.event('changed_subscription', presence)
+ return
+ elif not presence['type'] in ('available', 'unavailable') and \
+ not presence['type'] in presence.showtypes:
+ return
+
+ # Strip the information from the stanza.
+ jid = presence['from'].bare
+ resource = presence['from'].resource
+ show = presence['type']
+ status = presence['status']
+ priority = presence['priority']
+
+ was_offline = False
+ got_online = False
+ old_roster = self.roster.get(jid, {}).get(resource, {})
+
+ # Create a new roster entry if needed.
+ if not jid in self.roster:
+ self.roster[jid] = {'groups': [],
+ 'name': '',
+ 'subscription': 'none',
+ 'presence': {},
+ 'in_roster': False}
+
+ # Alias to simplify some references.
+ connections = self.roster[jid]['presence']
+
+ # Determine if the user has just come online.
+ if not resource in connections:
+ if show == 'available' or show in presence.showtypes:
+ got_online = True
+ was_offline = True
+ connections[resource] = {}
+
+ if connections[resource].get('show', 'unavailable') == 'unavailable':
+ was_offline = True
+
+ # Update the roster's state for this JID's resource.
+ connections[resource] = {'show': show,
+ 'status': status,
+ 'priority': priority}
+
+ name = self.roster[jid].get('name', '')
+
+ # Remove unneeded state information after a resource
+ # disconnects. Determine if this was the last connection
+ # for the JID.
+ if show == 'unavailable':
+ logging.debug("%s %s got offline" % (jid, resource))
+ del connections[resource]
+
+ if not connections and not self.roster[jid]['in_roster']:
+ del self.roster[jid]
+ if not was_offline:
+ self.event("got_offline", presence)
+ else:
+ return False
+
+ name = '(%s) ' % name if name else ''
+
+ # Presence state has changed.
+ self.event("changed_status", presence)
+ if got_online:
+ self.event("got_online", presence)
+ logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource,
+ show, status))
+
+ def _handle_subscribe(self, presence):
+ """
+ Automatically managage subscription requests.
+
+ Subscription behavior is controlled by the settings
+ self.auto_authorize and self.auto_subscribe.
+
+ auto_auth auto_sub Result:
+ True True Create bi-directional subsriptions.
+ True False Create only directed subscriptions.
+ False * Decline all subscriptions.
+ None * Disable automatic handling and use
+ a custom handler.
+ """
+ presence.reply()
+ presence['to'] = presence['to'].bare
+
+ # We are using trinary logic, so conditions have to be
+ # more explicit than usual.
+ if self.auto_authorize == True:
+ presence['type'] = 'subscribed'
+ presence.send()
+ if self.auto_subscribe:
+ presence['type'] = 'subscribe'
+ presence.send()
+ elif self.auto_authorize == False:
+ presence['type'] = 'unsubscribed'
+ presence.send()
+
+# Restore the old, lowercased name for backwards compatibility.
+basexmpp = BaseXMPP
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
new file mode 100644
index 0000000..dc4d0e4
--- /dev/null
+++ b/sleekxmpp/clientxmpp.py
@@ -0,0 +1,433 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+import logging
+import base64
+import sys
+import hashlib
+import random
+import threading
+
+from sleekxmpp import plugins
+from sleekxmpp import stanza
+from sleekxmpp.basexmpp import BaseXMPP
+from sleekxmpp.stanza import Message, Presence, Iq
+from sleekxmpp.xmlstream import XMLStream, RestartStream
+from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+
+# Flag indicating if DNS SRV records are available for use.
+SRV_SUPPORT = True
+try:
+ import dns.resolver
+except:
+ SRV_SUPPORT = False
+
+
+class ClientXMPP(BaseXMPP):
+
+ """
+ SleekXMPP's client class.
+
+ Use only for good, not for evil.
+
+ Attributes:
+
+ Methods:
+ connect -- Overrides XMLStream.connect.
+ del_roster_item -- Delete a roster item.
+ get_roster -- Retrieve the roster from the server.
+ register_feature -- Register a stream feature.
+ update_roster -- Update a roster item.
+ """
+
+ def __init__(self, jid, password, ssl=False, plugin_config={},
+ plugin_whitelist=[], escape_quotes=True):
+ """
+ Create a new SleekXMPP client.
+
+ Arguments:
+ jid -- The JID of the XMPP user account.
+ password -- The password for the XMPP user account.
+ ssl -- Deprecated.
+ plugin_config -- A dictionary of plugin configurations.
+ plugin_whitelist -- A list of approved plugins that will be loaded
+ when calling register_plugins.
+ escape_quotes -- Deprecated.
+ """
+ BaseXMPP.__init__(self, 'jabber:client')
+
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.updateRoster = self.update_roster
+ self.delRosterItem = self.del_roster_item
+ self.getRoster = self.get_roster
+ self.registerFeature = self.register_feature
+
+ self.set_jid(jid)
+ self.password = password
+ self.escape_quotes = escape_quotes
+ self.plugin_config = plugin_config
+ self.plugin_whitelist = plugin_whitelist
+ self.srv_support = SRV_SUPPORT
+
+ self.session_started_event = threading.Event()
+ self.session_started_event.clear()
+
+ self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % (
+ self.boundjid.host,
+ "xmlns:stream='%s'" % self.stream_ns,
+ "xmlns='%s'" % self.default_ns)
+ self.stream_footer = "</stream:stream>"
+
+ self.features = []
+ self.registered_features = []
+
+ #TODO: Use stream state here
+ self.authenticated = False
+ self.sessionstarted = False
+ self.bound = False
+ self.bindfail = False
+ self.add_event_handler('connected', self.handle_connected)
+
+ self.register_handler(
+ Callback('Stream Features',
+ MatchXPath('{%s}features' % self.stream_ns),
+ self._handle_stream_features))
+ self.register_handler(
+ Callback('Roster Update',
+ MatchXPath('{%s}iq/{%s}query' % (
+ self.default_ns,
+ 'jabber:iq:roster')),
+ self._handle_roster))
+
+ self.register_feature(
+ "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />",
+ self._handle_starttls, True)
+ self.register_feature(
+ "<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />",
+ self._handle_sasl_auth, True)
+ self.register_feature(
+ "<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />",
+ self._handle_bind_resource)
+ self.register_feature(
+ "<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />",
+ self._handle_start_session)
+
+ def handle_connected(self, event=None):
+ #TODO: Use stream state here
+ self.authenticated = False
+ self.sessionstarted = False
+ self.bound = False
+ self.bindfail = False
+ self.schedule("session timeout checker", 15,
+ self._session_timeout_check)
+
+ def _session_timeout_check(self):
+ if not self.session_started_event.isSet():
+ logging.debug("Session start has taken more than 15 seconds")
+ self.disconnect(reconnect=self.auto_reconnect)
+
+ def connect(self, address=tuple()):
+ """
+ Connect to the XMPP server.
+
+ When no address is given, a SRV lookup for the server will
+ be attempted. If that fails, the server user in the JID
+ will be used.
+
+ Arguments:
+ address -- A tuple containing the server's host and port.
+ """
+ self.session_started_event.clear()
+ if not address or len(address) < 2:
+ if not self.srv_support:
+ 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:
+ xmpp_srv = "_xmpp-client._tcp.%s" % self.server
+ answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV)
+ except dns.resolver.NXDOMAIN:
+ logging.debug("No appropriate SRV record found." + \
+ " Using JID server name.")
+ else:
+ # Pick a random server, weighted by priority.
+
+ addresses = {}
+ intmax = 0
+ for answer in answers:
+ intmax += answer.priority
+ addresses[intmax] = (answer.target.to_text()[:-1],
+ answer.port)
+ #python3 returns a generator for dictionary keys
+ priorities = [x for x in addresses.keys()]
+ 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, use the server from the JID.
+ address = (self.boundjid.host, 5222)
+
+ return XMLStream.connect(self, address[0], address[1], use_tls=True)
+
+ def register_feature(self, mask, pointer, breaker=False):
+ """
+ Register a stream feature.
+
+ Arguments:
+ mask -- An XML string matching the feature's element.
+ pointer -- The function to execute if the feature is received.
+ breaker -- Indicates if feature processing should halt with
+ this feature. Defaults to False.
+ """
+ self.registered_features.append((MatchXMLMask(mask),
+ pointer,
+ breaker))
+
+ def update_roster(self, jid, name=None, subscription=None, groups=[]):
+ """
+ Add or change a roster item.
+
+ Arguments:
+ jid -- The JID of the entry to modify.
+ name -- The user's nickname for this JID.
+ subscription -- The subscription status. May be one of
+ 'to', 'from', 'both', or 'none'. If set
+ to 'remove', the entry will be deleted.
+ groups -- The roster groups that contain this item.
+ """
+ iq = self.Iq()._set_stanza_values({'type': 'set'})
+ iq['roster']['items'] = {jid: {'name': name,
+ 'subscription': subscription,
+ 'groups': groups}}
+ response = iq.send()
+ return response['type'] == 'result'
+
+ def del_roster_item(self, jid):
+ """
+ Remove an item from the roster by setting its subscription
+ status to 'remove'.
+
+ Arguments:
+ jid -- The JID of the item to remove.
+ """
+ return self.update_roster(jid, subscription='remove')
+
+ def get_roster(self):
+ """Request the roster from the server."""
+ iq = self.Iq()._set_stanza_values({'type': 'get'}).enable('roster')
+ response = iq.send()
+ self._handle_roster(response, request=True)
+
+ def _handle_stream_features(self, features):
+ """
+ Process the received stream features.
+
+ Arguments:
+ features -- The features stanza.
+ """
+ # Record all of the features.
+ self.features = []
+ for sub in features.xml:
+ self.features.append(sub.tag)
+
+ # Process the features.
+ for sub in features.xml:
+ for feature in self.registered_features:
+ mask, handler, halt = feature
+ if mask.match(sub):
+ if handler(sub) and halt:
+ # Don't continue if the feature was
+ # marked as a breaker.
+ return True
+
+ def _handle_starttls(self, xml):
+ """
+ Handle notification that the server supports TLS.
+
+ Arguments:
+ xml -- The STARTLS proceed element.
+ """
+ if not self.authenticated and self.ssl_support:
+ tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls'
+ self.add_handler("<proceed xmlns='%s' />" % tls_ns,
+ self._handle_tls_start,
+ name='TLS Proceed',
+ instream=True)
+ self.send_xml(xml)
+ return True
+ else:
+ logging.warning("The module tlslite is required to log in" +\
+ " to some servers, and has not been found.")
+ return False
+
+ def _handle_tls_start(self, xml):
+ """
+ Handle encrypting the stream using TLS.
+
+ Restarts the stream.
+ """
+ logging.debug("Starting TLS")
+ if self.start_tls():
+ raise RestartStream()
+
+ def _handle_sasl_auth(self, xml):
+ """
+ Handle authenticating using SASL.
+
+ Arguments:
+ xml -- The SASL mechanisms stanza.
+ """
+ if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
+ return False
+
+ logging.debug("Starting SASL Auth")
+ sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl'
+ self.add_handler("<success xmlns='%s' />" % sasl_ns,
+ self._handle_auth_success,
+ name='SASL Sucess',
+ instream=True)
+ self.add_handler("<failure xmlns='%s' />" % sasl_ns,
+ self._handle_auth_fail,
+ name='SASL Failure',
+ instream=True)
+
+ sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns)
+ if sasl_mechs:
+ for sasl_mech in sasl_mechs:
+ self.features.append("sasl:%s" % sasl_mech.text)
+ if 'sasl:PLAIN' in self.features and self.boundjid.user:
+ if sys.version_info < (3, 0):
+ user = bytes(self.boundjid.user)
+ password = bytes(self.password)
+ else:
+ user = bytes(self.boundjid.user, 'utf-8')
+ password = bytes(self.password, 'utf-8')
+
+ auth = base64.b64encode(b'\x00' + user + \
+ b'\x00' + password).decode('utf-8')
+
+ self.send("<auth xmlns='%s' mechanism='PLAIN'>%s</auth>" % (
+ sasl_ns,
+ auth))
+ elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user:
+ self.send("<auth xmlns='%s' mechanism='%s' />" % (
+ sasl_ns,
+ 'ANONYMOUS'))
+ else:
+ logging.error("No appropriate login method.")
+ self.disconnect()
+ return True
+
+ def _handle_auth_success(self, xml):
+ """
+ SASL authentication succeeded. Restart the stream.
+
+ Arguments:
+ xml -- The SASL authentication success element.
+ """
+ self.authenticated = True
+ self.features = []
+ raise RestartStream()
+
+ def _handle_auth_fail(self, xml):
+ """
+ SASL authentication failed. Disconnect and shutdown.
+
+ Arguments:
+ xml -- The SASL authentication failure element.
+ """
+ logging.info("Authentication failed.")
+ self.event("failed_auth", direct=True)
+ self.disconnect()
+
+ def _handle_bind_resource(self, xml):
+ """
+ Handle requesting a specific resource.
+
+ Arguments:
+ xml -- The bind feature element.
+ """
+ logging.debug("Requesting resource: %s" % self.boundjid.resource)
+ xml.clear()
+ iq = self.Iq(stype='set')
+ if self.boundjid.resource:
+ res = ET.Element('resource')
+ res.text = self.boundjid.resource
+ xml.append(res)
+ iq.append(xml)
+ response = iq.send()
+
+ bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind'
+ self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns,
+ bind_ns)).text)
+ self.bound = True
+ logging.info("Node set to: %s" % self.boundjid.fulljid)
+ session_ns = 'urn:ietf:params:xml:ns:xmpp-session'
+ if "{%s}session" % session_ns not in self.features or self.bindfail:
+ logging.debug("Established Session")
+ self.sessionstarted = True
+ self.session_started_event.set()
+ self.event("session_start")
+
+ def _handle_start_session(self, xml):
+ """
+ Handle the start of the session.
+
+ Arguments:
+ xml -- The session feature element.
+ """
+ if self.authenticated and self.bound:
+ iq = self.makeIqSet(xml)
+ response = iq.send()
+ logging.debug("Established Session")
+ self.sessionstarted = True
+ self.session_started_event.set()
+ self.event("session_start")
+ else:
+ # Bind probably hasn't happened yet.
+ self.bindfail = True
+
+ def _handle_roster(self, iq, request=False):
+ """
+ Update the roster after receiving a roster stanza.
+
+ Arguments:
+ iq -- The roster stanza.
+ request -- Indicates if this stanza is a response
+ to a request for the roster.
+ """
+ if iq['type'] == 'set' or (iq['type'] == 'result' and request):
+ for jid in iq['roster']['items']:
+ if not jid in self.roster:
+ self.roster[jid] = {'groups': [],
+ 'name': '',
+ 'subscription': 'none',
+ 'presence': {},
+ 'in_roster': True}
+ self.roster[jid].update(iq['roster']['items'][jid])
+
+ self.event("roster_update", iq)
+ if iq['type'] == 'set':
+ iq.reply()
+ iq.enable('roster')
+ iq.send()
diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py
new file mode 100644
index 0000000..3fcb88d
--- /dev/null
+++ b/sleekxmpp/componentxmpp.py
@@ -0,0 +1,138 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import absolute_import
+
+import logging
+import base64
+import sys
+import hashlib
+
+from sleekxmpp import plugins
+from sleekxmpp import stanza
+from sleekxmpp.basexmpp import BaseXMPP, SRV_SUPPORT
+from sleekxmpp.xmlstream import XMLStream, RestartStream
+from sleekxmpp.xmlstream import StanzaBase, ET
+from sleekxmpp.xmlstream.matcher import *
+from sleekxmpp.xmlstream.handler import *
+
+
+class ComponentXMPP(BaseXMPP):
+
+ """
+ SleekXMPP's basic XMPP server component.
+
+ Use only for good, not for evil.
+
+ Methods:
+ connect -- Overrides XMLStream.connect.
+ incoming_filter -- Overrides XMLStream.incoming_filter.
+ start_stream_handler -- Overrides XMLStream.start_stream_handler.
+ """
+
+ def __init__(self, jid, secret, host, port,
+ plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
+ """
+ Arguments:
+ jid -- The JID of the component.
+ secret -- The secret or password for the component.
+ host -- The server accepting the component.
+ port -- The port used to connect to the server.
+ plugin_config -- A dictionary of plugin configurations.
+ plugin_whitelist -- A list of desired plugins to load
+ when using register_plugins.
+ use_js_ns -- Indicates if the 'jabber:client' namespace
+ should be used instead of the standard
+ 'jabber:component:accept' namespace.
+ Defaults to False.
+ """
+ if use_jc_ns:
+ default_ns = 'jabber:client'
+ else:
+ default_ns = 'jabber:component:accept'
+ BaseXMPP.__init__(self, default_ns)
+
+ self.auto_authorize = None
+ self.stream_header = "<stream:stream %s %s to='%s'>" % (
+ 'xmlns="jabber:component:accept"',
+ 'xmlns:stream="%s"' % self.stream_ns,
+ jid)
+ self.stream_footer = "</stream:stream>"
+ self.server_host = host
+ self.server_port = port
+ self.set_jid(jid)
+ self.secret = secret
+ self.plugin_config = plugin_config
+ self.plugin_whitelist = plugin_whitelist
+ self.is_component = True
+
+ self.register_handler(
+ Callback('Handshake',
+ MatchXPath('{jabber:component:accept}handshake'),
+ self._handle_handshake))
+
+ def connect(self):
+ """
+ Connect to the server.
+
+ Overrides XMLStream.connect.
+ """
+ logging.debug("Connecting to %s:%s" % (self.server_host,
+ self.server_port))
+ return XMLStream.connect(self, self.server_host,
+ self.server_port)
+
+ def incoming_filter(self, xml):
+ """
+ Pre-process incoming XML stanzas by converting any 'jabber:client'
+ namespaced elements to the component's default namespace.
+
+ Overrides XMLStream.incoming_filter.
+
+ Arguments:
+ xml -- The XML stanza to pre-process.
+ """
+ if xml.tag.startswith('{jabber:client}'):
+ xml.tag = xml.tag.replace('jabber:client', self.default_ns)
+
+ # The incoming_filter call is only made on top level stanza
+ # elements. So we manually continue filtering on sub-elements.
+ for sub in xml:
+ self.incoming_filter(sub)
+
+ return xml
+
+ def start_stream_handler(self, xml):
+ """
+ Once the streams are established, attempt to handshake
+ with the server to be accepted as a component.
+
+ Overrides XMLStream.start_stream_handler.
+
+ Arguments:
+ xml -- The incoming stream's root element.
+ """
+ # Construct a hash of the stream ID and the component secret.
+ sid = xml.get('id', '')
+ pre_hash = '%s%s' % (sid, self.secret)
+ if sys.version_info >= (3, 0):
+ # Handle Unicode byte encoding in Python 3.
+ pre_hash = bytes(pre_hash, 'utf-8')
+
+ handshake = ET.Element('{jabber:component:accept}handshake')
+ handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
+ self.send_xml(handshake)
+
+ def _handle_handshake(self, xml):
+ """
+ The handshake has been accepted.
+
+ Arguments:
+ xml -- The reply handshake stanza.
+ """
+ self.event("session_start")
diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py
new file mode 100644
index 0000000..d3988b4
--- /dev/null
+++ b/sleekxmpp/exceptions.py
@@ -0,0 +1,49 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+class XMPPError(Exception):
+
+ """
+ A generic exception that may be raised while processing an XMPP stanza
+ to indicate that an error response stanza should be sent.
+
+ The exception method for stanza objects extending RootStanza will create
+ an error stanza and initialize any additional substanzas using the
+ extension information included in the exception.
+
+ Meant for use in SleekXMPP plugins and applications using SleekXMPP.
+ """
+
+ def __init__(self, condition='undefined-condition', text=None, etype=None,
+ extension=None, extension_ns=None, extension_args=None):
+ """
+ Create a new XMPPError exception.
+
+ Extension information can be included to add additional XML elements
+ to the generated error stanza.
+
+ Arguments:
+ condition -- The XMPP defined error condition.
+ text -- Human readable text describing the error.
+ etype -- The XMPP error type, such as cancel or modify.
+ extension -- Tag name of the extension's XML content.
+ extension_ns -- XML namespace of the extensions' XML content.
+ extension_args -- Content and attributes for the extension
+ element. Same as the additional arguments to
+ the ET.Element constructor.
+ """
+ if extension_args is None:
+ extension_args = {}
+
+ self.condition = condition
+ self.text = text
+ self.etype = etype
+ self.extension = extension
+ self.extension_ns = extension_ns
+ self.extension_args = extension_args
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
new file mode 100644
index 0000000..36fa177
--- /dev/null
+++ b/sleekxmpp/plugins/__init__.py
@@ -0,0 +1,9 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+__all__ = ['xep_0004', 'xep_0030', 'xep_0033', 'xep_0045', 'xep_0050',
+'xep_0078', 'xep_0085', '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..254397e
--- /dev/null
+++ b/sleekxmpp/plugins/base.py
@@ -0,0 +1,26 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+class base_plugin(object):
+
+ def __init__(self, xmpp, config):
+ self.xep = 'base'
+ self.description = 'Base Plugin'
+ self.xmpp = xmpp
+ self.config = config
+ self.post_inited = False
+ self.enable = config.get('enable', True)
+ if self.enable:
+ self.plugin_init()
+
+ def plugin_init(self):
+ pass
+
+ def post_init(self):
+ self.post_inited = True
diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py
new file mode 100644
index 0000000..7e44234
--- /dev/null
+++ b/sleekxmpp/plugins/gmail_notify.py
@@ -0,0 +1,146 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.iq import Iq
+
+
+class GmailQuery(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'query'
+ plugin_attrib = 'gmail'
+ interfaces = set(('newer-than-time', 'newer-than-tid', 'q', 'search'))
+
+ def getSearch(self):
+ return self['q']
+
+ def setSearch(self, search):
+ self['q'] = search
+
+ def delSearch(self):
+ del self['q']
+
+
+class MailBox(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mailbox'
+ plugin_attrib = 'mailbox'
+ interfaces = set(('result-time', 'total-matched', 'total-estimate',
+ 'url', 'threads', 'matched', 'estimate'))
+
+ def getThreads(self):
+ threads = []
+ for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace,
+ MailThread.name)):
+ threads.append(MailThread(xml=threadXML, parent=None))
+ return threads
+
+ def getMatched(self):
+ return self['total-matched']
+
+ def getEstimate(self):
+ return self['total-estimate'] == '1'
+
+
+class MailThread(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mail-thread-info'
+ plugin_attrib = 'thread'
+ interfaces = set(('tid', 'participation', 'messages', 'date',
+ 'senders', 'url', 'labels', 'subject', 'snippet'))
+ sub_interfaces = set(('labels', 'subject', 'snippet'))
+
+ def getSenders(self):
+ senders = []
+ sendersXML = self.xml.find('{%s}senders' % self.namespace)
+ if sendersXML is not None:
+ for senderXML in sendersXML.findall('{%s}sender' % self.namespace):
+ senders.append(MailSender(xml=senderXML, parent=None))
+ return senders
+
+
+class MailSender(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'sender'
+ plugin_attrib = 'sender'
+ interfaces = set(('address', 'name', 'originator', 'unread'))
+
+ def getOriginator(self):
+ return self.xml.attrib.get('originator', '0') == '1'
+
+ def getUnread(self):
+ return self.xml.attrib.get('unread', '0') == '1'
+
+
+class NewMail(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'new-mail'
+ plugin_attrib = 'new-mail'
+
+
+class gmail_notify(base.base_plugin):
+ """
+ Google Talk: Gmail Notifications
+ """
+
+ def plugin_init(self):
+ self.description = 'Google Talk: Gmail Notifications'
+
+ self.xmpp.registerHandler(
+ Callback('Gmail Result',
+ MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
+ MailBox.namespace,
+ MailBox.name)),
+ self.handle_gmail))
+
+ self.xmpp.registerHandler(
+ Callback('Gmail New Mail',
+ MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
+ NewMail.namespace,
+ NewMail.name)),
+ self.handle_new_mail))
+
+ registerStanzaPlugin(Iq, GmailQuery)
+ registerStanzaPlugin(Iq, MailBox)
+ registerStanzaPlugin(Iq, NewMail)
+
+ self.last_result_time = None
+
+ def handle_gmail(self, iq):
+ mailbox = iq['mailbox']
+ approx = ' approximately' if mailbox['estimated'] else ''
+ logging.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched']))
+ self.last_result_time = mailbox['result-time']
+ self.xmpp.event('gmail_messages', iq)
+
+ def handle_new_mail(self, iq):
+ logging.info("Gmail: New emails received!")
+ self.xmpp.event('gmail_notify')
+ self.checkEmail()
+
+ def getEmail(self, query=None):
+ return self.search(query)
+
+ def checkEmail(self):
+ return self.search(newer=self.last_result_time)
+
+ def search(self, query=None, newer=None):
+ if query is None:
+ logging.info("Gmail: Checking for new emails")
+ else:
+ logging.info('Gmail: Searching for emails matching: "%s"' % query)
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = self.xmpp.jid
+ iq['gmail']['q'] = query
+ iq['gmail']['newer-than-time'] = newer
+ return iq.send()
diff --git a/sleekxmpp/plugins/jobs.py b/sleekxmpp/plugins/jobs.py
new file mode 100644
index 0000000..c52e524
--- /dev/null
+++ b/sleekxmpp/plugins/jobs.py
@@ -0,0 +1,46 @@
+from . import base
+import logging
+from xml.etree import cElementTree as ET
+import types
+
+class jobs(base.base_plugin):
+ def plugin_init(self):
+ self.xep = 'pubsubjob'
+ self.description = "Job distribution over Pubsub"
+
+ def post_init(self):
+ pass
+ #TODO add event
+
+ def createJobNode(self, host, jid, node, config=None):
+ pass
+
+ def createJob(self, host, node, jobid=None, payload=None):
+ return self.xmpp.plugin['xep_0060'].setItem(host, node, ((jobid, payload),))
+
+ def claimJob(self, host, node, jobid, ifrom=None):
+ return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}claimed'))
+
+ def unclaimJob(self, host, node, jobid):
+ return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}unclaimed'))
+
+ def finishJob(self, host, node, jobid, payload=None):
+ finished = ET.Element('{http://andyet.net/protocol/pubsubjob}finished')
+ if payload is not None:
+ finished.append(payload)
+ return self._setState(host, node, jobid, finished)
+
+ def _setState(self, host, node, jobid, state, ifrom=None):
+ iq = self.xmpp.Iq()
+ iq['to'] = host
+ if ifrom: iq['from'] = ifrom
+ iq['type'] = 'set'
+ iq['psstate']['node'] = node
+ iq['psstate']['item'] = jobid
+ iq['psstate']['payload'] = state
+ result = iq.send()
+ if result is None or type(result) == types.BooleanType or result['type'] != 'result':
+ logging.error("Unable to change %s:%s to %s" % (node, jobid, state))
+ return False
+ return True
+
diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py
new file mode 100644
index 0000000..651408a
--- /dev/null
+++ b/sleekxmpp/plugins/old_0004.py
@@ -0,0 +1,417 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+from . import base
+import logging
+from xml.etree import cElementTree as ET
+import copy
+import logging
+#TODO support item groups and results
+
+class old_0004(base.base_plugin):
+
+ def plugin_init(self):
+ self.xep = '0004'
+ self.description = '*Deprecated Data Forms'
+ self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform, name='Old Message Form')
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
+ logging.warning("This implementation of XEP-0004 is deprecated.")
+
+ 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(ftype=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 merge(self, form2):
+ form1 = Form(ftype=self.type)
+ form1.fromXML(self.getXML(self.type))
+ for field in form2.fields:
+ if not field.var in form1.field:
+ form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value)
+ else:
+ form1.field[field.var].value = field.value
+ for option, label in field.options:
+ if (option, label) not in form1.field[field.var].options:
+ form1.fields[field.var].addOption(option, label)
+ return form1
+
+ def copy(self):
+ newform = Form(ftype=self.type)
+ newform.fromXML(self.getXML(self.type))
+ return newform
+
+ def update(self, form):
+ values = form.getValues()
+ for var in values:
+ if var in self.fields:
+ self.fields[var].setValue(self.fields[var])
+
+ 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 setValues(self, values={}):
+ for field in values:
+ if field in self.field:
+ if isinstance(values[field], list) or isinstance(values[field], tuple):
+ for value in values[field]:
+ self.field[field].setValue(value)
+ else:
+ self.field[field].setValue(values[field])
+
+ 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):
+ 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('{jabber:x:data}title')
+ title.text = self.title
+ form.append(title)
+ if self.instructions and self.type == 'form':
+ instructions = ET.Element('{jabber:x:data}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('{jabber:x:data}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.type == 'boolean':
+ if value in ('1', 1, True, 'true', 'True', 'yes'):
+ value = True
+ else:
+ value = False
+ 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('{jabber:x:data}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('{jabber:x:data}option')
+ optionxml.attrib['label'] = option[1]
+ optionval = ET.Element('{jabber:x:data}value')
+ optionval.text = option[0]
+ optionxml.append(optionval)
+ field.append(optionxml)
+ if self.required:
+ required = ET.Element('{jabber:x:data}required')
+ field.append(required)
+ if self.desc:
+ desc = ET.Element('{jabber:x:data}desc')
+ desc.text = self.desc
+ field.append(desc)
+ for value in self.value:
+ valuexml = ET.Element('{jabber:x:data}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/stanza_pubsub.py b/sleekxmpp/plugins/stanza_pubsub.py
new file mode 100644
index 0000000..2d809a3
--- /dev/null
+++ b/sleekxmpp/plugins/stanza_pubsub.py
@@ -0,0 +1,555 @@
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.iq import Iq
+from .. stanza.message import Message
+from .. basexmpp import basexmpp
+from .. xmlstream.xmlstream import XMLStream
+import logging
+from . import xep_0004
+
+
+class PubsubState(ElementBase):
+ namespace = 'http://jabber.org/protocol/psstate'
+ name = 'state'
+ plugin_attrib = 'psstate'
+ interfaces = set(('node', 'item', 'payload'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setPayload(self, value):
+ self.xml.append(value)
+
+ def getPayload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def delPayload(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+registerStanzaPlugin(Iq, PubsubState)
+
+class PubsubStateEvent(ElementBase):
+ namespace = 'http://jabber.org/protocol/psstate#event'
+ name = 'event'
+ plugin_attrib = 'psstate_event'
+ intefaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Message, PubsubStateEvent)
+registerStanzaPlugin(PubsubStateEvent, PubsubState)
+
+class Pubsub(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'pubsub'
+ plugin_attrib = 'pubsub'
+ interfaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Iq, Pubsub)
+
+class PubsubOwner(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'pubsub'
+ plugin_attrib = 'pubsub_owner'
+ interfaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Iq, PubsubOwner)
+
+class Affiliation(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'affiliation'
+ plugin_attrib = name
+ interfaces = set(('node', 'affiliation'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+class Affiliations(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'affiliations'
+ plugin_attrib = 'affiliations'
+ interfaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (Affiliation,)
+
+ def append(self, affiliation):
+ if not isinstance(affiliation, Affiliation):
+ raise TypeError
+ self.xml.append(affiliation.xml)
+ return self.iterables.append(affiliation)
+
+registerStanzaPlugin(Pubsub, Affiliations)
+
+
+class Subscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('jid', 'node', 'subscription', 'subid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setjid(self, value):
+ self._setattr('jid', str(value))
+
+ def getjid(self):
+ return jid(self._getattr('jid'))
+
+registerStanzaPlugin(Pubsub, Subscription)
+
+class Subscriptions(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscriptions'
+ plugin_attrib = 'subscriptions'
+ interfaces = set(tuple())
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (Subscription,)
+
+registerStanzaPlugin(Pubsub, Subscriptions)
+
+class OptionalSetting(object):
+ interfaces = set(('required',))
+
+ def setRequired(self, value):
+ value = bool(value)
+ if value and not self['required']:
+ self.xml.append(ET.Element("{%s}required" % self.namespace))
+ elif not value and self['required']:
+ self.delRequired()
+
+ def getRequired(self):
+ required = self.xml.find("{%s}required" % self.namespace)
+ if required is not None:
+ return True
+ else:
+ return False
+
+ def delRequired(self):
+ required = self.xml.find("{%s}required" % self.namespace)
+ if required is not None:
+ self.xml.remove(required)
+
+
+class SubscribeOptions(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscribe-options'
+ plugin_attrib = 'suboptions'
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ interfaces = set(('required',))
+
+registerStanzaPlugin(Subscription, SubscribeOptions)
+
+class Item(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'item'
+ plugin_attrib = name
+ interfaces = set(('id', 'payload'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setPayload(self, value):
+ self.xml.append(value)
+
+ def getPayload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def delPayload(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+class Items(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'items'
+ plugin_attrib = 'items'
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (Item,)
+
+registerStanzaPlugin(Pubsub, Items)
+
+class Create(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'create'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Pubsub, Create)
+
+#class Default(ElementBase):
+# namespace = 'http://jabber.org/protocol/pubsub'
+# name = 'default'
+# plugin_attrib = name
+# interfaces = set(('node', 'type'))
+# plugin_attrib_map = {}
+# plugin_tag_map = {}
+#
+# def getType(self):
+# t = self._getAttr('type')
+# if not t: t == 'leaf'
+# return t
+#
+#registerStanzaPlugin(Pubsub, Default)
+
+class Publish(Items):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'publish'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (Item,)
+
+registerStanzaPlugin(Pubsub, Publish)
+
+class Retract(Items):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'retract'
+ plugin_attrib = name
+ interfaces = set(('node', 'notify'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Pubsub, Retract)
+
+class Unsubscribe(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'unsubscribe'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+class Subscribe(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'subscribe'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(Pubsub, Subscribe)
+
+class Configure(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'configure'
+ plugin_attrib = name
+ interfaces = set(('node', 'type'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def getType(self):
+ t = self._getAttr('type')
+ if not t: t == 'leaf'
+ return t
+
+registerStanzaPlugin(Pubsub, Configure)
+registerStanzaPlugin(Configure, xep_0004.Form)
+
+class DefaultConfig(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'default'
+ plugin_attrib = 'default'
+ interfaces = set(('node', 'type', 'config'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def __init__(self, *args, **kwargs):
+ ElementBase.__init__(self, *args, **kwargs)
+
+ def getType(self):
+ t = self._getAttr('type')
+ if not t: t = 'leaf'
+ return t
+
+ def getConfig(self):
+ return self['form']
+
+ def setConfig(self, value):
+ self['form'].setStanzaValues(value.getStanzaValues())
+ return self
+
+registerStanzaPlugin(PubsubOwner, DefaultConfig)
+registerStanzaPlugin(DefaultConfig, xep_0004.Form)
+
+class Options(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub'
+ name = 'options'
+ plugin_attrib = 'options'
+ interfaces = set(('jid', 'node', 'options'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def __init__(self, *args, **kwargs):
+ ElementBase.__init__(self, *args, **kwargs)
+
+ def getOptions(self):
+ config = self.xml.find('{jabber:x:data}x')
+ form = xep_0004.Form()
+ if config is not None:
+ form.fromXML(config)
+ return form
+
+ def setOptions(self, value):
+ self.xml.append(value.getXML())
+ return self
+
+ def delOptions(self):
+ config = self.xml.find('{jabber:x:data}x')
+ self.xml.remove(config)
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(Pubsub, Options)
+registerStanzaPlugin(Subscribe, Options)
+
+class OwnerAffiliations(Affiliations):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def append(self, affiliation):
+ if not isinstance(affiliation, OwnerAffiliation):
+ raise TypeError
+ self.xml.append(affiliation.xml)
+ return self.affiliations.append(affiliation)
+
+registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
+
+class OwnerAffiliation(Affiliation):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('affiliation', 'jid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+class OwnerConfigure(Configure):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node', 'config'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(PubsubOwner, OwnerConfigure)
+
+class OwnerDefault(OwnerConfigure):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node', 'config'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def getConfig(self):
+ return self['form']
+
+ def setConfig(self, value):
+ self['form'].setStanzaValues(value.getStanzaValues())
+ return self
+
+registerStanzaPlugin(PubsubOwner, OwnerDefault)
+registerStanzaPlugin(OwnerDefault, xep_0004.Form)
+
+class OwnerDelete(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'delete'
+ plugin_attrib = 'delete'
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ interfaces = set(('node',))
+
+registerStanzaPlugin(PubsubOwner, OwnerDelete)
+
+class OwnerPurge(ElementBase, OptionalSetting):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'purge'
+ plugin_attrib = name
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(PubsubOwner, OwnerPurge)
+
+class OwnerRedirect(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'redirect'
+ plugin_attrib = name
+ interfaces = set(('node', 'jid'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(OwnerDelete, OwnerRedirect)
+
+class OwnerSubscriptions(Subscriptions):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def append(self, subscription):
+ if not isinstance(subscription, OwnerSubscription):
+ raise TypeError
+ self.xml.append(subscription.xml)
+ return self.subscriptions.append(subscription)
+
+registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
+
+class OwnerSubscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#owner'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('jid', 'subscription'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('from'))
+
+class Event(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'event'
+ plugin_attrib = 'pubsub_event'
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Message, Event)
+
+class EventItem(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'item'
+ plugin_attrib = 'item'
+ interfaces = set(('id', 'payload'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setPayload(self, value):
+ self.xml.append(value)
+
+ def getPayload(self):
+ childs = self.xml.getchildren()
+ if len(childs) > 0:
+ return childs[0]
+
+ def delPayload(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+
+class EventRetract(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'retract'
+ plugin_attrib = 'retract'
+ interfaces = set(('id',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+class EventItems(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'items'
+ plugin_attrib = 'items'
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = (EventItem, EventRetract)
+
+registerStanzaPlugin(Event, EventItems)
+
+class EventCollection(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'collection'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Event, EventCollection)
+
+class EventAssociate(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'associate'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(EventCollection, EventAssociate)
+
+class EventDisassociate(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'disassociate'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(EventCollection, EventDisassociate)
+
+class EventConfiguration(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'configuration'
+ plugin_attrib = name
+ interfaces = set(('node', 'config'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Event, EventConfiguration)
+registerStanzaPlugin(EventConfiguration, xep_0004.Form)
+
+class EventPurge(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'purge'
+ plugin_attrib = name
+ interfaces = set(('node',))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+registerStanzaPlugin(Event, EventPurge)
+
+class EventSubscription(ElementBase):
+ namespace = 'http://jabber.org/protocol/pubsub#event'
+ name = 'subscription'
+ plugin_attrib = name
+ interfaces = set(('node','expiry', 'jid', 'subid', 'subscription'))
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+
+ def setJid(self, value):
+ self._setAttr('jid', str(value))
+
+ def getJid(self):
+ return JID(self._getAttr('jid'))
+
+registerStanzaPlugin(Event, EventSubscription)
diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py
new file mode 100644
index 0000000..e8dba74
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0004.py
@@ -0,0 +1,392 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import copy
+from . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.message import Message
+import types
+
+
+class Form(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'x'
+ plugin_attrib = 'form'
+ interfaces = set(('fields', 'instructions', 'items', 'reported', 'title', 'type', 'values'))
+ sub_interfaces = set(('title',))
+ form_types = set(('cancel', 'form', 'result', 'submit'))
+
+ def __init__(self, *args, **kwargs):
+ title = None
+ if 'title' in kwargs:
+ title = kwargs['title']
+ del kwargs['title']
+ ElementBase.__init__(self, *args, **kwargs)
+ if title is not None:
+ self['title'] = title
+ self.field = FieldAccessor(self)
+
+ def setup(self, xml=None):
+ if ElementBase.setup(self, xml): #if we had to generate xml
+ self['type'] = 'form'
+
+ def addField(self, var='', ftype=None, label='', desc='', required=False, value=None, options=None, **kwargs):
+ kwtype = kwargs.get('type', None)
+ if kwtype is None:
+ kwtype = ftype
+
+ field = FormField(parent=self)
+ field['var'] = var
+ field['type'] = kwtype
+ field['label'] = label
+ field['desc'] = desc
+ field['required'] = required
+ field['value'] = value
+ if options is not None:
+ field['options'] = options
+ return field
+
+ def getXML(self, type='submit'):
+ logging.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py")
+ return self.xml
+
+ def fromXML(self, xml):
+ logging.warning("Form.fromXML() is deprecated API compatibility with plugins/old_0004.py")
+ n = Form(xml=xml)
+ return n
+
+ def addItem(self, values):
+ itemXML = ET.Element('{%s}item' % self.namespace)
+ self.xml.append(itemXML)
+ reported_vars = self['reported'].keys()
+ for var in reported_vars:
+ fieldXML = ET.Element('{%s}field' % FormField.namespace)
+ itemXML.append(fieldXML)
+ field = FormField(xml=fieldXML)
+ field['var'] = var
+ field['value'] = values.get(var, None)
+
+ def addReported(self, var, ftype=None, label='', desc='', **kwargs):
+ kwtype = kwargs.get('type', None)
+ if kwtype is None:
+ kwtype = ftype
+ reported = self.xml.find('{%s}reported' % self.namespace)
+ if reported is None:
+ reported = ET.Element('{%s}reported' % self.namespace)
+ self.xml.append(reported)
+ fieldXML = ET.Element('{%s}field' % FormField.namespace)
+ reported.append(fieldXML)
+ field = FormField(xml=fieldXML)
+ field['var'] = var
+ field['type'] = kwtype
+ field['label'] = label
+ field['desc'] = desc
+ return field
+
+ def cancel(self):
+ self['type'] = 'cancel'
+
+ def delFields(self):
+ fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
+ for fieldXML in fieldsXML:
+ self.xml.remove(fieldXML)
+
+ def delInstructions(self):
+ instsXML = self.xml.findall('{%s}instructions')
+ for instXML in instsXML:
+ self.xml.remove(instXML)
+
+ def delItems(self):
+ itemsXML = self.xml.find('{%s}item' % self.namespace)
+ for itemXML in itemsXML:
+ self.xml.remove(itemXML)
+
+ def delReported(self):
+ reportedXML = self.xml.find('{%s}reported' % self.namespace)
+ if reportedXML is not None:
+ self.xml.remove(reportedXML)
+
+ def getFields(self, use_dict=False):
+ fields = {} if use_dict else []
+ fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
+ for fieldXML in fieldsXML:
+ field = FormField(xml=fieldXML)
+ if use_dict:
+ fields[field['var']] = field
+ else:
+ fields.append((field['var'], field))
+ return fields
+
+ def getInstructions(self):
+ instructions = ''
+ instsXML = self.xml.findall('{%s}instructions' % self.namespace)
+ return "\n".join([instXML.text for instXML in instsXML])
+
+ def getItems(self):
+ items = []
+ itemsXML = self.xml.findall('{%s}item' % self.namespace)
+ for itemXML in itemsXML:
+ item = {}
+ fieldsXML = itemXML.findall('{%s}field' % FormField.namespace)
+ for fieldXML in fieldsXML:
+ field = FormField(xml=fieldXML)
+ item[field['var']] = field['value']
+ items.append(item)
+ return items
+
+ def getReported(self):
+ fields = {}
+ fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
+ FormField.namespace))
+ for fieldXML in fieldsXML:
+ field = FormField(xml=fieldXML)
+ fields[field['var']] = field
+ return fields
+
+ def getValues(self):
+ values = {}
+ fields = self.getFields(use_dict=True)
+ for var in fields:
+ values[var] = fields[var]['value']
+ return values
+
+ def reply(self):
+ if self['type'] == 'form':
+ self['type'] = 'submit'
+ elif self['type'] == 'submit':
+ self['type'] = 'result'
+
+ def setFields(self, fields, default=None):
+ del self['fields']
+ for field_data in fields:
+ var = field_data[0]
+ field = field_data[1]
+ field['var'] = var
+
+ self.addField(**field)
+
+ def setInstructions(self, instructions):
+ del self['instructions']
+ if instructions in [None, '']:
+ return
+ instructions = instructions.split('\n')
+ for instruction in instructions:
+ inst = ET.Element('{%s}instructions' % self.namespace)
+ inst.text = instruction
+ self.xml.append(inst)
+
+ def setItems(self, items):
+ for item in items:
+ self.addItem(item)
+
+ def setReported(self, reported, default=None):
+ for var in reported:
+ field = reported[var]
+ field['var'] = var
+ self.addReported(var, **field)
+
+ def setValues(self, values):
+ fields = self.getFields(use_dict=True)
+ for field in values:
+ fields[field]['value'] = values[field]
+
+ def merge(self, other):
+ new = copy.copy(self)
+ if type(other) == types.DictType:
+ new.setValues(other)
+ return new
+ nfields = new.getFields(use_dict=True)
+ ofields = other.getFields(use_dict=True)
+ nfields.update(ofields)
+ new.setFields([(x, nfields[x]) for x in nfields])
+ return new
+
+class FieldAccessor(object):
+ def __init__(self, form):
+ self.form = form
+
+ def __getitem__(self, key):
+ return self.form.getFields(use_dict=True)[key]
+
+ def __contains__(self, key):
+ return key in self.form.getFields(use_dict=True)
+
+ def has_key(self, key):
+ return key in self.form.getFields(use_dict=True)
+
+
+class FormField(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'field'
+ plugin_attrib = 'field'
+ interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var'))
+ sub_interfaces = set(('desc',))
+ field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi',
+ 'list-single', 'text-multi', 'text-private', 'text-single'))
+ multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi'))
+ multi_line_types = set(('hidden', 'text-multi'))
+ option_types = set(('list-multi', 'list-single'))
+ true_values = set((True, '1', 'true'))
+
+ def addOption(self, label='', value=''):
+ if self['type'] in self.option_types:
+ opt = FieldOption(parent=self)
+ opt['label'] = label
+ opt['value'] = value
+ else:
+ raise ValueError("Cannot add options to a %s field." % self['type'])
+
+ def delOptions(self):
+ optsXML = self.xml.findall('{%s}option' % self.namespace)
+ for optXML in optsXML:
+ self.xml.remove(optXML)
+
+ def delRequired(self):
+ reqXML = self.xml.find('{%s}required' % self.namespace)
+ if reqXML is not None:
+ self.xml.remove(reqXML)
+
+ def delValue(self):
+ valsXML = self.xml.findall('{%s}value' % self.namespace)
+ for valXML in valsXML:
+ self.xml.remove(valXML)
+
+ def getAnswer(self):
+ return self.getValue()
+
+ def getOptions(self):
+ options = []
+ optsXML = self.xml.findall('{%s}option' % self.namespace)
+ for optXML in optsXML:
+ opt = FieldOption(xml=optXML)
+ options.append({'label': opt['label'], 'value':opt['value']})
+ return options
+
+ def getRequired(self):
+ reqXML = self.xml.find('{%s}required' % self.namespace)
+ return reqXML is not None
+
+ def getValue(self):
+ valsXML = self.xml.findall('{%s}value' % self.namespace)
+ if len(valsXML) == 0:
+ return None
+ elif self['type'] == 'boolean':
+ return valsXML[0].text in self.true_values
+ elif self['type'] in self.multi_value_types:
+ values = []
+ for valXML in valsXML:
+ if valXML.text is None:
+ valXML.text = ''
+ values.append(valXML.text)
+ if self['type'] == 'text-multi':
+ values = "\n".join(values)
+ return values
+ else:
+ return valsXML[0].text
+
+ def setAnswer(self, answer):
+ self.setValue(answer)
+
+ def setFalse(self):
+ self.setValue(False)
+
+ def setOptions(self, options):
+ for value in options:
+ if isinstance(value, dict):
+ self.addOption(**value)
+ else:
+ self.addOption(value=value)
+
+ def setRequired(self, required):
+ exists = self.getRequired()
+ if not exists and required:
+ self.xml.append(ET.Element('{%s}required' % self.namespace))
+ elif exists and not required:
+ self.delRequired()
+
+ def setTrue(self):
+ self.setValue(True)
+
+ def setValue(self, value):
+ self.delValue()
+ valXMLName = '{%s}value' % self.namespace
+
+ if self['type'] == 'boolean':
+ if value in self.true_values:
+ valXML = ET.Element(valXMLName)
+ valXML.text = '1'
+ self.xml.append(valXML)
+ else:
+ valXML = ET.Element(valXMLName)
+ valXML.text = '0'
+ self.xml.append(valXML)
+ elif self['type'] in self.multi_value_types or self['type'] in ['', None]:
+ if self['type'] in self.multi_line_types and isinstance(value, str):
+ value = value.split('\n')
+ if not isinstance(value, list):
+ value = [value]
+ for val in value:
+ if self['type'] in ['', None] and val in self.true_values:
+ val = '1'
+ valXML = ET.Element(valXMLName)
+ valXML.text = val
+ self.xml.append(valXML)
+ else:
+ if isinstance(value, list):
+ raise ValueError("Cannot add multiple values to a %s field." % self['type'])
+ valXML = ET.Element(valXMLName)
+ valXML.text = value
+ self.xml.append(valXML)
+
+
+class FieldOption(ElementBase):
+ namespace = 'jabber:x:data'
+ name = 'option'
+ plugin_attrib = 'option'
+ interfaces = set(('label', 'value'))
+ sub_interfaces = set(('value',))
+
+
+class xep_0004(base.base_plugin):
+ """
+ XEP-0004: Data Forms
+ """
+
+ def plugin_init(self):
+ self.xep = '0004'
+ self.description = 'Data Forms'
+
+ self.xmpp.registerHandler(
+ Callback('Data Form',
+ MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns,
+ Form.namespace)),
+ self.handle_form))
+
+ registerStanzaPlugin(FormField, FieldOption)
+ registerStanzaPlugin(Form, FormField)
+ registerStanzaPlugin(Message, Form)
+
+ def makeForm(self, ftype='form', title='', instructions=''):
+ f = Form()
+ f['type'] = ftype
+ f['title'] = title
+ f['instructions'] = instructions
+ return f
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
+
+ def handle_form(self, message):
+ self.xmpp.event("message_xform", message)
+
+ def buildForm(self, xml):
+ return Form(xml=xml)
diff --git a/sleekxmpp/plugins/xep_0009.py b/sleekxmpp/plugins/xep_0009.py
new file mode 100644
index 0000000..625b03f
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009.py
@@ -0,0 +1,277 @@
+"""
+XEP-0009 XMPP Remote Procedure Calls
+"""
+from __future__ import with_statement
+from . import base
+import logging
+from xml.etree import cElementTree as ET
+import copy
+import time
+import base64
+
+def py2xml(*args):
+ params = ET.Element("params")
+ for x in args:
+ param = ET.Element("param")
+ param.append(_py2xml(x))
+ params.append(param) #<params><param>...
+ return params
+
+def _py2xml(*args):
+ for x in args:
+ val = ET.Element("value")
+ if type(x) is int:
+ i4 = ET.Element("i4")
+ i4.text = str(x)
+ val.append(i4)
+ if type(x) is bool:
+ boolean = ET.Element("boolean")
+ boolean.text = str(int(x))
+ val.append(boolean)
+ elif type(x) is str:
+ string = ET.Element("string")
+ string.text = x
+ val.append(string)
+ elif type(x) is float:
+ double = ET.Element("double")
+ double.text = str(x)
+ val.append(double)
+ elif type(x) is rpcbase64:
+ b64 = ET.Element("Base64")
+ b64.text = x.encoded()
+ val.append(b64)
+ elif type(x) is rpctime:
+ iso = ET.Element("dateTime.iso8601")
+ iso.text = str(x)
+ val.append(iso)
+ elif type(x) is list:
+ array = ET.Element("array")
+ data = ET.Element("data")
+ for y in x:
+ data.append(_py2xml(y))
+ array.append(data)
+ val.append(array)
+ elif type(x) is dict:
+ struct = ET.Element("struct")
+ for y in x.keys():
+ member = ET.Element("member")
+ name = ET.Element("name")
+ name.text = y
+ member.append(name)
+ member.append(_py2xml(x[y]))
+ struct.append(member)
+ val.append(struct)
+ return val
+
+def xml2py(params):
+ vals = []
+ for param in params.findall('param'):
+ vals.append(_xml2py(param.find('value')))
+ return vals
+
+def _xml2py(value):
+ if value.find('i4') is not None:
+ return int(value.find('i4').text)
+ if value.find('int') is not None:
+ return int(value.find('int').text)
+ if value.find('boolean') is not None:
+ return bool(value.find('boolean').text)
+ if value.find('string') is not None:
+ return value.find('string').text
+ if value.find('double') is not None:
+ return float(value.find('double').text)
+ if value.find('Base64') is not None:
+ return rpcbase64(value.find('Base64').text)
+ if value.find('dateTime.iso8601') is not None:
+ return rpctime(value.find('dateTime.iso8601'))
+ if value.find('struct') is not None:
+ struct = {}
+ for member in value.find('struct').findall('member'):
+ struct[member.find('name').text] = _xml2py(member.find('value'))
+ return struct
+ if value.find('array') is not None:
+ array = []
+ for val in value.find('array').find('data').findall('value'):
+ array.append(_xml2py(val))
+ return array
+ raise ValueError()
+
+class rpcbase64(object):
+ def __init__(self, data):
+ #base 64 encoded string
+ self.data = data
+
+ def decode(self):
+ return base64.decodestring(data)
+
+ def __str__(self):
+ return self.decode()
+
+ def encoded(self):
+ return self.data
+
+class rpctime(object):
+ def __init__(self,data=None):
+ #assume string data is in iso format YYYYMMDDTHH:MM:SS
+ if type(data) is str:
+ self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
+ elif type(data) is time.struct_time:
+ self.timestamp = data
+ elif data is None:
+ self.timestamp = time.gmtime()
+ else:
+ raise ValueError()
+
+ def iso8601(self):
+ #return a iso8601 string
+ return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
+
+ def __str__(self):
+ return self.iso8601()
+
+class JabberRPCEntry(object):
+ def __init__(self,call):
+ self.call = call
+ self.result = None
+ self.error = None
+ self.allow = {} #{'<jid>':['<resource1>',...],...}
+ self.deny = {}
+
+ def check_acl(self, jid, resource):
+ #Check for deny
+ if jid in self.deny.keys():
+ if self.deny[jid] == None or resource in self.deny[jid]:
+ return False
+ #Check for allow
+ if allow == None:
+ return True
+ if jid in self.allow.keys():
+ if self.allow[jid] == None or resource in self.allow[jid]:
+ return True
+ return False
+
+ def acl_allow(self, jid, resource):
+ if jid == None:
+ self.allow = None
+ elif resource == None:
+ self.allow[jid] = None
+ elif jid in self.allow.keys():
+ self.allow[jid].append(resource)
+ else:
+ self.allow[jid] = [resource]
+
+ def acl_deny(self, jid, resource):
+ if jid == None:
+ self.deny = None
+ elif resource == None:
+ self.deny[jid] = None
+ elif jid in self.deny.keys():
+ self.deny[jid].append(resource)
+ else:
+ self.deny[jid] = [resource]
+
+ def call_method(self, args):
+ ret = self.call(*args)
+
+class xep_0009(base.base_plugin):
+
+ def plugin_init(self):
+ self.xep = '0009'
+ self.description = 'Jabber-RPC'
+ self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callMethod, name='Jabber RPC Call')
+ self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callResult, name='Jabber RPC Result')
+ self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callError, name='Jabber RPC Error')
+ self.entries = {}
+ self.activeCalls = []
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
+ self.xmpp.plugin['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..a9d8d6a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0030.py
@@ -0,0 +1,327 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.iq import Iq
+
+class DiscoInfo(ElementBase):
+ namespace = 'http://jabber.org/protocol/disco#info'
+ name = 'query'
+ plugin_attrib = 'disco_info'
+ interfaces = set(('node', 'features', 'identities'))
+
+ def getFeatures(self):
+ features = []
+ featuresXML = self.xml.findall('{%s}feature' % self.namespace)
+ for feature in featuresXML:
+ features.append(feature.attrib['var'])
+ return features
+
+ def setFeatures(self, features):
+ self.delFeatures()
+ for name in features:
+ self.addFeature(name)
+
+ def delFeatures(self):
+ featuresXML = self.xml.findall('{%s}feature' % self.namespace)
+ for feature in featuresXML:
+ self.xml.remove(feature)
+
+ def addFeature(self, feature):
+ featureXML = ET.Element('{%s}feature' % self.namespace,
+ {'var': feature})
+ self.xml.append(featureXML)
+
+ def delFeature(self, feature):
+ featuresXML = self.xml.findall('{%s}feature' % self.namespace)
+ for featureXML in featuresXML:
+ if featureXML.attrib['var'] == feature:
+ self.xml.remove(featureXML)
+
+ def getIdentities(self):
+ ids = []
+ idsXML = self.xml.findall('{%s}identity' % self.namespace)
+ for idXML in idsXML:
+ idData = (idXML.attrib['category'],
+ idXML.attrib['type'],
+ idXML.attrib.get('name', ''))
+ ids.append(idData)
+ return ids
+
+ def setIdentities(self, ids):
+ self.delIdentities()
+ for idData in ids:
+ self.addIdentity(*idData)
+
+ def delIdentities(self):
+ idsXML = self.xml.findall('{%s}identity' % self.namespace)
+ for idXML in idsXML:
+ self.xml.remove(idXML)
+
+ def addIdentity(self, category, id_type, name=''):
+ idXML = ET.Element('{%s}identity' % self.namespace,
+ {'category': category,
+ 'type': id_type,
+ 'name': name})
+ self.xml.append(idXML)
+
+ def delIdentity(self, category, id_type, name=''):
+ idsXML = self.xml.findall('{%s}identity' % self.namespace)
+ for idXML in idsXML:
+ idData = (idXML.attrib['category'],
+ idXML.attrib['type'])
+ delId = (category, id_type)
+ if idData == delId:
+ self.xml.remove(idXML)
+
+
+class DiscoItems(ElementBase):
+ namespace = 'http://jabber.org/protocol/disco#items'
+ name = 'query'
+ plugin_attrib = 'disco_items'
+ interfaces = set(('node', 'items'))
+
+ def getItems(self):
+ items = []
+ itemsXML = self.xml.findall('{%s}item' % self.namespace)
+ for item in itemsXML:
+ itemData = (item.attrib['jid'],
+ item.attrib.get('node'),
+ item.attrib.get('name'))
+ items.append(itemData)
+ return items
+
+ def setItems(self, items):
+ self.delItems()
+ for item in items:
+ self.addItem(*item)
+
+ def delItems(self):
+ itemsXML = self.xml.findall('{%s}item' % self.namespace)
+ for item in itemsXML:
+ self.xml.remove(item)
+
+ def addItem(self, jid, node='', name=''):
+ itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid})
+ if name:
+ itemXML.attrib['name'] = name
+ if node:
+ itemXML.attrib['node'] = node
+ self.xml.append(itemXML)
+
+ def delItem(self, jid, node=''):
+ itemsXML = self.xml.findall('{%s}item' % self.namespace)
+ for itemXML in itemsXML:
+ itemData = (itemXML.attrib['jid'],
+ itemXML.attrib.get('node', ''))
+ itemDel = (jid, node)
+ if itemData == itemDel:
+ self.xml.remove(itemXML)
+
+
+class DiscoNode(object):
+ """
+ Collection object for grouping info and item information
+ into nodes.
+ """
+ def __init__(self, name):
+ self.name = name
+ self.info = DiscoInfo()
+ self.items = DiscoItems()
+
+ self.info['node'] = name
+ self.items['node'] = name
+
+ # This is a bit like poor man's inheritance, but
+ # to simplify adding information to the node we
+ # map node functions to either the info or items
+ # stanza objects.
+ #
+ # We don't want to make DiscoNode inherit from
+ # DiscoInfo and DiscoItems because DiscoNode is
+ # not an actual stanza, and doing so would create
+ # confusion and potential bugs.
+
+ self._map(self.items, 'items', ['get', 'set', 'del'])
+ self._map(self.items, 'item', ['add', 'del'])
+ self._map(self.info, 'identities', ['get', 'set', 'del'])
+ self._map(self.info, 'identity', ['add', 'del'])
+ self._map(self.info, 'features', ['get', 'set', 'del'])
+ self._map(self.info, 'feature', ['add', 'del'])
+
+ def isEmpty(self):
+ """
+ Test if the node contains any information. Useful for
+ determining if a node can be deleted.
+ """
+ ids = self.getIdentities()
+ features = self.getFeatures()
+ items = self.getItems()
+
+ if not ids and not features and not items:
+ return True
+ return False
+
+ def _map(self, obj, interface, access):
+ """
+ Map functions of the form obj.accessInterface
+ to self.accessInterface for each given access type.
+ """
+ interface = interface.title()
+ for access_type in access:
+ method = access_type + interface
+ if hasattr(obj, method):
+ setattr(self, method, getattr(obj, method))
+
+
+class xep_0030(base.base_plugin):
+ """
+ XEP-0030 Service Discovery
+ """
+
+ def plugin_init(self):
+ self.xep = '0030'
+ self.description = 'Service Discovery'
+
+ self.xmpp.registerHandler(
+ Callback('Disco Items',
+ MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
+ DiscoItems.namespace)),
+ self.handle_item_query))
+
+ self.xmpp.registerHandler(
+ Callback('Disco Info',
+ MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
+ DiscoInfo.namespace)),
+ self.handle_info_query))
+
+ registerStanzaPlugin(Iq, DiscoInfo)
+ registerStanzaPlugin(Iq, DiscoItems)
+
+ self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
+ self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
+
+ self.nodes = {'main': DiscoNode('main')}
+
+ def add_node(self, node):
+ if node not in self.nodes:
+ self.nodes[node] = DiscoNode(node)
+
+ def del_node(self, node):
+ if node in self.nodes:
+ del self.nodes[node]
+
+ def handle_item_query(self, iq):
+ if iq['type'] == 'get':
+ logging.debug("Items requested by %s" % iq['from'])
+ self.xmpp.event('disco_items_request', iq)
+ elif iq['type'] == 'result':
+ logging.debug("Items result from %s" % iq['from'])
+ self.xmpp.event('disco_items', iq)
+
+ def handle_info_query(self, iq):
+ if iq['type'] == 'get':
+ logging.debug("Info requested by %s" % iq['from'])
+ self.xmpp.event('disco_info_request', iq)
+ elif iq['type'] == 'result':
+ logging.debug("Info result from %s" % iq['from'])
+ self.xmpp.event('disco_info', iq)
+
+ def handle_disco_info(self, iq, forwarded=False):
+ """
+ A default handler for disco#info requests. If another
+ handler is registered, this one will defer and not run.
+ """
+ handlers = self.xmpp.event_handlers['disco_info_request']
+ if not forwarded and len(handlers) > 1:
+ return
+
+ node_name = iq['disco_info']['node']
+ if not node_name:
+ node_name = 'main'
+
+ logging.debug("Using default handler for disco#info on node '%s'." % node_name)
+
+ if node_name in self.nodes:
+ node = self.nodes[node_name]
+ iq.reply().setPayload(node.info.xml).send()
+ else:
+ logging.debug("Node %s requested, but does not exist." % node_name)
+ iq.reply().error().setPayload(iq['disco_info'].xml)
+ iq['error']['code'] = '404'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'item-not-found'
+ iq.send()
+
+ def handle_disco_items(self, iq, forwarded=False):
+ """
+ A default handler for disco#items requests. If another
+ handler is registered, this one will defer and not run.
+
+ If this handler is called by your own custom handler with
+ forwarded set to True, then it will run as normal.
+ """
+ handlers = self.xmpp.event_handlers['disco_items_request']
+ if not forwarded and len(handlers) > 1:
+ return
+
+ node_name = iq['disco_items']['node']
+ if not node_name:
+ node_name = 'main'
+
+ logging.debug("Using default handler for disco#items on node '%s'." % node_name)
+
+ if node_name in self.nodes:
+ node = self.nodes[node_name]
+ iq.reply().setPayload(node.items.xml).send()
+ else:
+ logging.debug("Node %s requested, but does not exist." % node_name)
+ iq.reply().error().setPayload(iq['disco_items'].xml)
+ iq['error']['code'] = '404'
+ iq['error']['type'] = 'cancel'
+ iq['error']['condition'] = 'item-not-found'
+ iq.send()
+
+ # Older interface methods for backwards compatibility
+
+ def getInfo(self, jid, node='', dfrom=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = jid
+ iq['from'] = dfrom
+ iq['disco_info']['node'] = node
+ return iq.send()
+
+ def getItems(self, jid, node='', dfrom=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = jid
+ iq['from'] = dfrom
+ iq['disco_items']['node'] = node
+ return iq.send()
+
+ def add_feature(self, feature, node='main'):
+ self.add_node(node)
+ self.nodes[node].addFeature(feature)
+
+ def add_identity(self, category='', itype='', name='', node='main'):
+ self.add_node(node)
+ self.nodes[node].addIdentity(category=category,
+ id_type=itype,
+ name=name)
+
+ def add_item(self, jid=None, name='', node='main', subnode=''):
+ self.add_node(node)
+ self.add_node(subnode)
+ if jid is None:
+ jid = self.xmpp.fulljid
+ self.nodes[node].addItem(jid=jid, name=name, node=subnode)
diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py
new file mode 100644
index 0000000..c0c4d89
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0033.py
@@ -0,0 +1,161 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.message import Message
+
+
+class Addresses(ElementBase):
+ namespace = 'http://jabber.org/protocol/address'
+ name = 'addresses'
+ plugin_attrib = 'addresses'
+ interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
+
+ def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False):
+ address = Address(parent=self)
+ address['type'] = atype
+ address['jid'] = jid
+ address['node'] = node
+ address['uri'] = uri
+ address['desc'] = desc
+ address['delivered'] = delivered
+ return address
+
+ def getAddresses(self, atype=None):
+ addresses = []
+ for addrXML in self.xml.findall('{%s}address' % Address.namespace):
+ # ElementTree 1.2.6 does not support [@attr='value'] in findall
+ if atype is None or addrXML.attrib.get('type') == atype:
+ addresses.append(Address(xml=addrXML, parent=None))
+ return addresses
+
+ def setAddresses(self, addresses, set_type=None):
+ self.delAddresses(set_type)
+ for addr in addresses:
+ addr = dict(addr)
+ # Remap 'type' to 'atype' to match the add method
+ if set_type is not None:
+ addr['type'] = set_type
+ curr_type = addr.get('type', None)
+ if curr_type is not None:
+ del addr['type']
+ addr['atype'] = curr_type
+ self.addAddress(**addr)
+
+ def delAddresses(self, atype=None):
+ if atype is None:
+ return
+ for addrXML in self.xml.findall('{%s}address' % Address.namespace):
+ # ElementTree 1.2.6 does not support [@attr='value'] in findall
+ if addrXML.attrib.get('type') == atype:
+ self.xml.remove(addrXML)
+
+ # --------------------------------------------------------------
+
+ def delBcc(self):
+ self.delAddresses('bcc')
+
+ def delCc(self):
+ self.delAddresses('cc')
+
+ def delNoreply(self):
+ self.delAddresses('noreply')
+
+ def delReplyroom(self):
+ self.delAddresses('replyroom')
+
+ def delReplyto(self):
+ self.delAddresses('replyto')
+
+ def delTo(self):
+ self.delAddresses('to')
+
+ # --------------------------------------------------------------
+
+ def getBcc(self):
+ return self.getAddresses('bcc')
+
+ def getCc(self):
+ return self.getAddresses('cc')
+
+ def getNoreply(self):
+ return self.getAddresses('noreply')
+
+ def getReplyroom(self):
+ return self.getAddresses('replyroom')
+
+ def getReplyto(self):
+ return self.getAddresses('replyto')
+
+ def getTo(self):
+ return self.getAddresses('to')
+
+ # --------------------------------------------------------------
+
+ def setBcc(self, addresses):
+ self.setAddresses(addresses, 'bcc')
+
+ def setCc(self, addresses):
+ self.setAddresses(addresses, 'cc')
+
+ def setNoreply(self, addresses):
+ self.setAddresses(addresses, 'noreply')
+
+ def setReplyroom(self, addresses):
+ self.setAddresses(addresses, 'replyroom')
+
+ def setReplyto(self, addresses):
+ self.setAddresses(addresses, 'replyto')
+
+ def setTo(self, addresses):
+ self.setAddresses(addresses, 'to')
+
+
+class Address(ElementBase):
+ namespace = 'http://jabber.org/protocol/address'
+ name = 'address'
+ plugin_attrib = 'address'
+ interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri'))
+ address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
+
+ def getDelivered(self):
+ return self.xml.attrib.get('delivered', False)
+
+ def setDelivered(self, delivered):
+ if delivered:
+ self.xml.attrib['delivered'] = "true"
+ else:
+ del self['delivered']
+
+ def setUri(self, uri):
+ if uri:
+ del self['jid']
+ del self['node']
+ self.xml.attrib['uri'] = uri
+ elif 'uri' in self.xml.attrib:
+ del self.xml.attrib['uri']
+
+
+class xep_0033(base.base_plugin):
+ """
+ XEP-0033: Extended Stanza Addressing
+ """
+
+ def plugin_init(self):
+ self.xep = '0033'
+ self.description = 'Extended Stanza Addressing'
+
+ registerStanzaPlugin(Message, Addresses)
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace)
diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py
new file mode 100644
index 0000000..bf472a4
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0045.py
@@ -0,0 +1,333 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+from __future__ import with_statement
+from . import base
+import logging
+from xml.etree import cElementTree as ET
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID
+from .. stanza.presence import Presence
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.matcher.xmlmask import MatchXMLMask
+
+class MUCPresence(ElementBase):
+ name = 'x'
+ namespace = 'http://jabber.org/protocol/muc#user'
+ plugin_attrib = 'muc'
+ interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
+ affiliations = set(('', ))
+ roles = set(('', ))
+
+ def getXMLItem(self):
+ item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
+ if item is None:
+ item = ET.Element('{http://jabber.org/protocol/muc#user}item')
+ self.xml.append(item)
+ return item
+
+ def getAffiliation(self):
+ #TODO if no affilation, set it to the default and return default
+ item = self.getXMLItem()
+ return item.get('affiliation', '')
+
+ def setAffiliation(self, value):
+ item = self.getXMLItem()
+ #TODO check for valid affiliation
+ item.attrib['affiliation'] = value
+ return self
+
+ def delAffiliation(self):
+ item = self.getXMLItem()
+ #TODO set default affiliation
+ if 'affiliation' in item.attrib: del item.attrib['affiliation']
+ return self
+
+ def getJid(self):
+ item = self.getXMLItem()
+ return JID(item.get('jid', ''))
+
+ def setJid(self, value):
+ item = self.getXMLItem()
+ if not isinstance(value, str):
+ value = str(value)
+ item.attrib['jid'] = value
+ return self
+
+ def delJid(self):
+ item = self.getXMLItem()
+ if 'jid' in item.attrib: del item.attrib['jid']
+ return self
+
+ def getRole(self):
+ item = self.getXMLItem()
+ #TODO get default role, set default role if none
+ return item.get('role', '')
+
+ def setRole(self, value):
+ item = self.getXMLItem()
+ #TODO check for valid role
+ item.attrib['role'] = value
+ return self
+
+ def delRole(self):
+ item = self.getXMLItem()
+ #TODO set default role
+ if 'role' in item.attrib: del item.attrib['role']
+ return self
+
+ def getNick(self):
+ return self.parent()['from'].resource
+
+ def getRoom(self):
+ return self.parent()['from'].bare
+
+ def setNick(self, value):
+ logging.warning("Cannot set nick through mucpresence plugin.")
+ return self
+
+ def setRoom(self, value):
+ logging.warning("Cannot set room through mucpresence plugin.")
+ return self
+
+ def delNick(self):
+ logging.warning("Cannot delete nick through mucpresence plugin.")
+ return self
+
+ def delRoom(self):
+ logging.warning("Cannot delete room through mucpresence plugin.")
+ return self
+
+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'
+ # load MUC support in presence stanzas
+ registerStanzaPlugin(Presence, MUCPresence)
+ self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
+ self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
+
+ def handle_groupchat_presence(self, pr):
+ """ Handle a presence in a muc.
+ """
+ got_offline = False
+ got_online = False
+ if pr['muc']['room'] not in self.rooms.keys():
+ return
+ entry = pr['muc'].getStanzaValues()
+ entry['show'] = pr['show']
+ entry['status'] = pr['status']
+ if pr['type'] == 'unavailable':
+ if entry['nick'] in self.rooms[entry['room']]:
+ del self.rooms[entry['room']][entry['nick']]
+ got_offline = True
+ else:
+ if entry['nick'] not in self.rooms[entry['room']]:
+ got_online = True
+ self.rooms[entry['room']][entry['nick']] = entry
+ logging.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry))
+ self.xmpp.event("groupchat_presence", pr)
+ self.xmpp.event("muc::%s::presence" % entry['room'], pr)
+ if got_offline:
+ self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
+ if got_online:
+ self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
+
+ def handle_groupchat_message(self, msg):
+ """ Handle a message event in a muc.
+ """
+ self.xmpp.event('groupchat_message', msg)
+ self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
+
+ def jidInRoom(self, room, jid):
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return True
+ return False
+
+ def getNick(self, room, jid):
+ for nick in self.rooms[room]:
+ entry = self.rooms[room][nick]
+ if entry is not None and entry['jid'].full == jid:
+ return nick
+
+ def getRoomForm(self, room, ifrom=None):
+ iq = self.xmpp.makeIqGet()
+ iq['to'] = room
+ if ifrom is not None:
+ iq['from'] = ifrom
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ iq.append(query)
+ result = iq.send()
+ if result['type'] == 'error':
+ return False
+ xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
+ if xform is None: return False
+ form = self.xmpp.plugin['old_0004'].buildForm(xform)
+ return form
+
+ def configureRoom(self, room, form=None, ifrom=None):
+ if form is None:
+ form = self.getRoomForm(room, ifrom=ifrom)
+ #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
+ #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
+ iq = self.xmpp.makeIqSet()
+ iq['to'] = room
+ if ifrom is not None:
+ iq['from'] = ifrom
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ form = form.getXML('submit')
+ query.append(form)
+ iq.append(query)
+ result = iq.send()
+ if result['type'] == 'error':
+ return False
+ return True
+
+ def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None):
+ """ Join the specified room, requesting 'maxhistory' lines of history.
+ """
+ stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow)
+ x = ET.Element('{http://jabber.org/protocol/muc}x')
+ if password:
+ passelement = ET.Element('password')
+ passelement.text = password
+ x.append(passelement)
+ if maxhistory:
+ history = ET.Element('history')
+ if maxhistory == "0":
+ history.attrib['maxchars'] = maxhistory
+ else:
+ 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("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
+ self.xmpp.send(stanza, expect)
+ self.rooms[room] = {}
+ self.ourNicks[room] = nick
+
+ def destroy(self, room, reason='', altroom = '', ifrom=None):
+ iq = self.xmpp.makeIqSet()
+ if ifrom is not None:
+ iq['from'] = ifrom
+ iq['to'] = room
+ query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
+ destroy = ET.Element('destroy')
+ if altroom:
+ destroy.attrib['jid'] = altroom
+ xreason = ET.Element('reason')
+ xreason.text = reason
+ destroy.append(xreason)
+ query.append(destroy)
+ iq.append(query)
+ r = iq.send()
+ if r is False or r['type'] == 'error':
+ return False
+ return True
+
+ def setAffiliation(self, room, jid=None, nick=None, 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')
+ if nick is not None:
+ item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
+ else:
+ item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
+ query.append(item)
+ iq = self.xmpp.makeIqSet(query)
+ iq['to'] = room
+ result = iq.send()
+ if result is False or result['type'] != 'result':
+ raise ValueError
+ return True
+
+ def invite(self, room, jid, reason=''):
+ """ Invite a jid to a room."""
+ msg = self.xmpp.makeMessage(room)
+ msg['from'] = self.xmpp.jid
+ x = ET.Element('{http://jabber.org/protocol/muc#user}x')
+ invite = ET.Element('{http://jabber.org/protocol/muc#user}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, msg=''):
+ """ Leave the specified room.
+ """
+ if msg:
+ self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg)
+ else:
+ 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['to'] = room
+ iq['from'] = self.xmpp.jid
+ result = iq.send()
+ if result is None or result['type'] != 'result':
+ raise ValueError
+ form = result.xml.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)
+ iq.send()
+
+ 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['to'] = room
+ iq['from'] = self.xmpp.jid
+ iq.send()
+
+ 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 room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
+ 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..5efb911
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0050.py
@@ -0,0 +1,133 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+from __future__ import with_statement
+from . import base
+import logging
+from xml.etree import cElementTree as ET
+import time
+
+class xep_0050(base.base_plugin):
+ """
+ XEP-0050 Ad-Hoc Commands
+ """
+
+ def plugin_init(self):
+ self.xep = '0050'
+ self.description = 'Ad-Hoc Commands'
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True)
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete')
+ self.commands = {}
+ self.sessions = {}
+ self.sd = self.xmpp.plugin['xep_0030']
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.sd.add_feature('http://jabber.org/protocol/commands')
+
+ def addCommand(self, node, name, form, pointer=None, multi=False):
+ self.sd.add_item(None, name, 'http://jabber.org/protocol/commands', node)
+ self.sd.add_identity('automation', 'command-node', name, node)
+ self.sd.add_feature('http://jabber.org/protocol/commands', node)
+ self.sd.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]['to'] = xml.get('to')
+ 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.plugin['old_0004'].makeForm('result')
+ results.fromXML(in_command.find('{jabber:x:data}x'))
+ 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[in_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.plugin['old_0004'].makeForm('result')
+ results.fromXML(in_command.find('{jabber:x:data}x'))
+ form, npointer, next = 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()
+ else:
+ iq.attrib['from'] = self.sessions[sessionid]['to']
+ 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..0b056f0
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0060.py
@@ -0,0 +1,309 @@
+from __future__ import with_statement
+from . import base
+import logging
+#from xml.etree import cElementTree as ET
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET
+from . import stanza_pubsub
+from . xep_0004 import Form
+
+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, ntype=None):
+ 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 collection:
+ ntype = 'collection'
+ #if config is None:
+ # submitform = self.xmpp.plugin['xep_0004'].makeForm('submit')
+ #else:
+ if config is not None:
+ 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 ntype:
+ if 'pubsub#node_type' in submitform.field:
+ submitform.field['pubsub#node_type'].setValue(ntype)
+ else:
+ submitform.addField('pubsub#node_type', value=ntype)
+ else:
+ if 'pubsub#node_type' in submitform.field:
+ submitform.field['pubsub#node_type'].setValue('leaf')
+ else:
+ submitform.addField('pubsub#node_type', value='leaf')
+ submitform['type'] = 'submit'
+ configure.append(submitform.xml)
+ pubsub.append(configure)
+ iq = self.xmpp.makeIqSet(pubsub)
+ iq.attrib['to'] = jid
+ iq.attrib['from'] = self.xmpp.fulljid
+ id = iq['id']
+ result = iq.send()
+ if result is False or result is None or result['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['id']
+ result = iq.send()
+ if result is False or result is None or result['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['id']
+ result = iq.send()
+ if result is False or result is None or result['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['id']
+ #self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
+ result = iq.send()
+ if result is None or result == False or result['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 Form(xml=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['id']
+ result = iq.send()
+ if result is None or result == False or result['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['id']
+ result = iq.send()
+ if result is None or result == False or result['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
+ result = iq.send()
+ if result is not None and result is not False and result['type'] != '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['id']
+ result = iq.send()
+ if result is None or result['type'] == 'error':
+ 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['id']
+ result = iq.send()
+ if result is None or result is False or result['type'] == 'error': return False
+ return True
+
+ def addItem(self, jid, node, items=[]):
+ return self.setItem(jid, node, items)
+
+ 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['id']
+ result = iq.send()
+ if result is None or result is False or result['type'] == 'error': return False
+ return True
+
+ 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['id']
+ result = iq.send()
+ if result is None or result is False or result['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..4b3ab82
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0078.py
@@ -0,0 +1,69 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+from __future__ import with_statement
+from xml.etree import cElementTree as ET
+import logging
+import hashlib
+from . 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_event_handler("session_start", self.check_stream)
+ #disabling until I fix conflict with PLAIN
+ #self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth)
+ self.streamid = ''
+
+ def check_stream(self, xml):
+ self.streamid = xml.attrib['id']
+ if xml.get('version', '0') != '1.0':
+ self.auth()
+
+ def auth(self, xml=None):
+ logging.debug("Starting jabber:iq:auth Authentication")
+ auth_request = self.xmpp.makeIqGet()
+ auth_request_query = ET.Element('{jabber:iq:auth}query')
+ auth_request.attrib['to'] = self.xmpp.server
+ username = ET.Element('username')
+ username.text = self.xmpp.username
+ auth_request_query.append(username)
+ auth_request.append(auth_request_query)
+ result = auth_request.send()
+ 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 = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest()
+ query.append(digest)
+ attempt.append(query)
+ result = attempt.send()
+ 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_0085.py b/sleekxmpp/plugins/xep_0085.py
new file mode 100644
index 0000000..b7b5d6d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0085.py
@@ -0,0 +1,101 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permissio
+"""
+
+import logging
+from . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.message import Message
+
+
+class ChatState(ElementBase):
+ namespace = 'http://jabber.org/protocol/chatstates'
+ plugin_attrib = 'chat_state'
+ interface = set(('state',))
+ states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
+
+ def active(self):
+ self.setState('active')
+
+ def composing(self):
+ self.setState('composing')
+
+ def gone(self):
+ self.setState('gone')
+
+ def inactive(self):
+ self.setState('inactive')
+
+ def paused(self):
+ self.setState('paused')
+
+ def setState(self, state):
+ if state in self.states:
+ self.name = state
+ self.xml.tag = '{%s}%s' % (self.namespace, state)
+ else:
+ raise ValueError('Invalid chat state')
+
+ def getState(self):
+ return self.name
+
+# In order to match the various chat state elements,
+# we need one stanza object per state, even though
+# they are all the same except for the initial name
+# value. Do not depend on the type of the chat state
+# stanza object for the actual state.
+
+class Active(ChatState):
+ name = 'active'
+class Composing(ChatState):
+ name = 'composing'
+class Gone(ChatState):
+ name = 'gone'
+class Inactive(ChatState):
+ name = 'inactive'
+class Paused(ChatState):
+ name = 'paused'
+
+
+class xep_0085(base.base_plugin):
+ """
+ XEP-0085 Chat State Notifications
+ """
+
+ def plugin_init(self):
+ self.xep = '0085'
+ self.description = 'Chat State Notifications'
+
+ handlers = [('Active Chat State', 'active'),
+ ('Composing Chat State', 'composing'),
+ ('Gone Chat State', 'gone'),
+ ('Inactive Chat State', 'inactive'),
+ ('Paused Chat State', 'paused')]
+ for handler in handlers:
+ self.xmpp.registerHandler(
+ Callback(handler[0],
+ MatchXPath("{%s}message/{%s}%s" % (self.xmpp.default_ns,
+ ChatState.namespace,
+ handler[1])),
+ self._handleChatState))
+
+ registerStanzaPlugin(Message, Active)
+ registerStanzaPlugin(Message, Composing)
+ registerStanzaPlugin(Message, Gone)
+ registerStanzaPlugin(Message, Inactive)
+ registerStanzaPlugin(Message, Paused)
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/chatstates')
+
+ def _handleChatState(self, msg):
+ state = msg['chat_state'].name
+ logging.debug("Chat State: %s, %s" % (state, msg['from'].jid))
+ self.xmpp.event('chatstate_%s' % state, msg)
diff --git a/sleekxmpp/plugins/xep_0086.py b/sleekxmpp/plugins/xep_0086.py
new file mode 100644
index 0000000..e6c18c7
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0086.py
@@ -0,0 +1,49 @@
+
+from __future__ import with_statement
+from . 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..ca02c4a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0092.py
@@ -0,0 +1,56 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+from xml.etree import cElementTree as ET
+from . import base
+from .. xmlstream.handler.xmlwaiter import XMLWaiter
+
+class xep_0092(base.base_plugin):
+ """
+ XEP-0092 Software Version
+ """
+ def plugin_init(self):
+ self.description = "Software Version"
+ self.xep = "0092"
+ self.name = self.config.get('name', 'SleekXMPP')
+ self.version = self.config.get('version', '0.1-dev')
+ self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version, name='Sofware Version')
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['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 = iq.send()
+ 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_0128.py b/sleekxmpp/plugins/xep_0128.py
new file mode 100644
index 0000000..824977b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0128.py
@@ -0,0 +1,51 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+from . import base
+from .. xmlstream.handler.callback import Callback
+from .. xmlstream.matcher.xpath import MatchXPath
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
+from .. stanza.iq import Iq
+from . xep_0030 import DiscoInfo, DiscoItems
+from . xep_0004 import Form
+
+
+class xep_0128(base.base_plugin):
+ """
+ XEP-0128 Service Discovery Extensions
+ """
+
+ def plugin_init(self):
+ self.xep = '0128'
+ self.description = 'Service Discovery Extensions'
+
+ registerStanzaPlugin(DiscoInfo, Form)
+ registerStanzaPlugin(DiscoItems, Form)
+
+ def extend_info(self, node, data=None):
+ if data is None:
+ data = {}
+ node = self.xmpp['xep_0030'].nodes.get(node, None)
+ if node is None:
+ self.xmpp['xep_0030'].add_node(node)
+
+ info = node.info
+ info['form']['type'] = 'result'
+ info['form'].setFields(data, default=None)
+
+ def extend_items(self, node, data=None):
+ if data is None:
+ data = {}
+ node = self.xmpp['xep_0030'].nodes.get(node, None)
+ if node is None:
+ self.xmpp['xep_0030'].add_node(node)
+
+ items = node.items
+ items['form']['type'] = 'result'
+ items['form'].setFields(data, default=None)
diff --git a/sleekxmpp/plugins/xep_0199.py b/sleekxmpp/plugins/xep_0199.py
new file mode 100644
index 0000000..3fc62f5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0199.py
@@ -0,0 +1,59 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+from xml.etree import cElementTree as ET
+from . import base
+import time
+import logging
+
+class xep_0199(base.base_plugin):
+ """XEP-0199 XMPP Ping"""
+
+ def plugin_init(self):
+ self.description = "XMPP Ping"
+ self.xep = "0199"
+ self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='http://www.xmpp.org/extensions/xep-0199.html#ns'/></iq>" % self.xmpp.default_ns, self.handler_ping, name='XMPP 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):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['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)
+ pingresult = iq.send()
+ 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..8302c43
--- /dev/null
+++ b/sleekxmpp/stanza/__init__.py
@@ -0,0 +1,13 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from sleekxmpp.stanza.error import Error
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.stanza.message import Message
+from sleekxmpp.stanza.presence import Presence
diff --git a/sleekxmpp/stanza/atom.py b/sleekxmpp/stanza/atom.py
new file mode 100644
index 0000000..244ef31
--- /dev/null
+++ b/sleekxmpp/stanza/atom.py
@@ -0,0 +1,26 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class AtomEntry(ElementBase):
+
+ """
+ A simple Atom feed entry.
+
+ Stanza Interface:
+ title -- The title of the Atom feed entry.
+ summary -- The summary of the Atom feed entry.
+ """
+
+ namespace = 'http://www.w3.org/2005/Atom'
+ name = 'entry'
+ plugin_attrib = 'entry'
+ interfaces = set(('title', 'summary'))
+ sub_interfaces = set(('title', 'summary'))
diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py
new file mode 100644
index 0000000..09229bc
--- /dev/null
+++ b/sleekxmpp/stanza/error.py
@@ -0,0 +1,141 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Error(ElementBase):
+
+ """
+ XMPP stanzas of type 'error' should include an <error> stanza that
+ describes the nature of the error and how it should be handled.
+
+ Use the 'XEP-0086: Error Condition Mappings' plugin to include error
+ codes used in older XMPP versions.
+
+ Example error stanza:
+ <error type="cancel" code="404">
+ <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ The item was not found.
+ </text>
+ </error>
+
+ Stanza Interface:
+ code -- The error code used in older XMPP versions.
+ condition -- The name of the condition element.
+ text -- Human readable description of the error.
+ type -- Error type indicating how the error should be handled.
+
+ Attributes:
+ conditions -- The set of allowable error condition elements.
+ condition_ns -- The namespace for the condition element.
+ types -- A set of values indicating how the error
+ should be treated.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_condition -- Retrieve the name of the condition element.
+ set_condition -- Add a condition element.
+ del_condition -- Remove the condition element.
+ get_text -- Retrieve the contents of the <text> element.
+ set_text -- Set the contents of the <text> element.
+ del_text -- Remove the <text> element.
+ """
+
+ namespace = 'jabber:client'
+ name = 'error'
+ plugin_attrib = 'error'
+ interfaces = set(('code', 'condition', 'text', 'type'))
+ sub_interfaces = set(('text',))
+ conditions = set(('bad-request', 'conflict', 'feature-not-implemented',
+ 'forbidden', 'gone', 'internal-server-error',
+ 'item-not-found', 'jid-malformed', 'not-acceptable',
+ 'not-allowed', 'not-authorized', 'payment-required',
+ 'recipient-unavailable', 'redirect',
+ 'registration-required', 'remote-server-not-found',
+ 'remote-server-timeout', 'resource-constraint',
+ 'service-unavailable', 'subscription-required',
+ 'undefined-condition', 'unexpected-request'))
+ condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
+ types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup.
+
+ Sets a default error type and condition, and changes the
+ parent stanza's type to 'error'.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.getCondition = self.get_condition
+ self.setCondition = self.set_condition
+ self.delCondition = self.del_condition
+ self.getText = self.get_text
+ self.setText = self.set_text
+ self.delText = self.del_text
+
+ if ElementBase.setup(self, xml):
+ #If we had to generate XML then set default values.
+ self['type'] = 'cancel'
+ self['condition'] = 'feature-not-implemented'
+ if self.parent is not None:
+ self.parent()['type'] = 'error'
+
+ def get_condition(self):
+ """Return the condition element's name."""
+ for child in self.xml.getchildren():
+ if "{%s}" % self.condition_ns in child.tag:
+ return child.tag.split('}', 1)[-1]
+ return ''
+
+ def set_condition(self, value):
+ """
+ Set the tag name of the condition element.
+
+ Arguments:
+ value -- The tag name of the condition element.
+ """
+ if value in self.conditions:
+ del self['condition']
+ self.xml.append(ET.Element("{%s}%s" % (self.condition_ns, value)))
+ return self
+
+ def del_condition(self):
+ """Remove the condition element."""
+ for child in self.xml.getchildren():
+ if "{%s}" % self.condition_ns in child.tag:
+ tag = child.tag.split('}', 1)[-1]
+ if tag in self.conditions:
+ self.xml.remove(child)
+ return self
+
+ def get_text(self):
+ """Retrieve the contents of the <text> element."""
+ return self._get_sub_text('{%s}text' % self.condition_ns)
+
+ def set_text(self, value):
+ """
+ Set the contents of the <text> element.
+
+ Arguments:
+ value -- The new contents for the <text> element.
+ """
+ self._set_sub_text('{%s}text' % self.condition_ns, text=value)
+ return self
+
+ def del_text(self):
+ """Remove the <text> element."""
+ self._del_sub('{%s}text' % self.condition_ns)
+ return self
diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py
new file mode 100644
index 0000000..4586828
--- /dev/null
+++ b/sleekxmpp/stanza/htmlim.py
@@ -0,0 +1,97 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Message
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class HTMLIM(ElementBase):
+
+ """
+ XEP-0071: XHTML-IM defines a method for embedding XHTML content
+ within a <message> stanza so that lightweight markup can be used
+ to format the message contents and to create links.
+
+ Only a subset of XHTML is recommended for use with XHTML-IM.
+ See the full spec at 'http://xmpp.org/extensions/xep-0071.html'
+ for more information.
+
+ Example stanza:
+ <message to="user@example.com">
+ <body>Non-html message content.</body>
+ <html xmlns="http://jabber.org/protocol/xhtml-im">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p><b>HTML!</b></p>
+ </body>
+ </html>
+ </message>
+
+ Stanza Interface:
+ body -- The contents of the HTML body tag.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_body -- Return the HTML body contents.
+ set_body -- Set the HTML body contents.
+ del_body -- Remove the HTML body contents.
+ """
+
+ namespace = 'http://jabber.org/protocol/xhtml-im'
+ name = 'html'
+ interfaces = set(('body',))
+ plugin_attrib = name
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides StanzaBase.setup.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.setBody = self.set_body
+ self.getBody = self.get_body
+ self.delBody = self.del_body
+
+ return ElementBase.setup(self, xml)
+
+ def set_body(self, html):
+ """
+ Set the contents of the HTML body.
+
+ Arguments:
+ html -- Either a string or XML object. If the top level
+ element is not <body> with a namespace of
+ 'http://www.w3.org/1999/xhtml', it will be wrapped.
+ """
+ if isinstance(html, str):
+ html = ET.XML(html)
+ if html.tag != '{http://www.w3.org/1999/xhtml}body':
+ body = ET.Element('{http://www.w3.org/1999/xhtml}body')
+ body.append(html)
+ self.xml.append(body)
+ else:
+ self.xml.append(html)
+
+ def get_body(self):
+ """Return the contents of the HTML body."""
+ html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
+ if html is None:
+ return ''
+ return html
+
+ def del_body(self):
+ """Remove the HTML body contents."""
+ if self.parent is not None:
+ self.parent().xml.remove(self.xml)
+
+
+register_stanza_plugin(Message, HTMLIM)
diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py
new file mode 100644
index 0000000..614d14f
--- /dev/null
+++ b/sleekxmpp/stanza/iq.py
@@ -0,0 +1,183 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream import RESPONSE_TIMEOUT, StanzaBase, ET
+from sleekxmpp.xmlstream.handler import Waiter
+from sleekxmpp.xmlstream.matcher import MatcherId
+
+
+class Iq(RootStanza):
+
+ """
+ XMPP <iq> stanzas, or info/query stanzas, are XMPP's method of
+ requesting and modifying information, similar to HTTP's GET and
+ POST methods.
+
+ Each <iq> stanza must have an 'id' value which associates the
+ stanza with the response stanza. XMPP entities must always
+ be given a response <iq> stanza with a type of 'result' after
+ sending a stanza of type 'get' or 'set'.
+
+ Most uses cases for <iq> stanzas will involve adding a <query>
+ element whose namespace indicates the type of information
+ desired. However, some custom XMPP applications use <iq> stanzas
+ as a carrier stanza for an application-specific protocol instead.
+
+ Example <iq> Stanzas:
+ <iq to="user@example.com" type="get" id="314">
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+
+ <iq to="user@localhost" type="result" id="17">
+ <query xmlns='jabber:iq:roster'>
+ <item jid='otheruser@example.net'
+ name='John Doe'
+ subscription='both'>
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+
+ Stanza Interface:
+ query -- The namespace of the <query> element if one exists.
+
+ Attributes:
+ types -- May be one of: get, set, result, or error.
+
+ Methods:
+ __init__ -- Overrides StanzaBase.__init__.
+ unhandled -- Send error if there are no handlers.
+ set_payload -- Overrides StanzaBase.set_payload.
+ set_query -- Add or modify a <query> element.
+ get_query -- Return the namespace of the <query> element.
+ del_query -- Remove the <query> element.
+ reply -- Overrides StanzaBase.reply
+ send -- Overrides StanzaBase.send
+ """
+
+ namespace = 'jabber:client'
+ name = 'iq'
+ interfaces = set(('type', 'to', 'from', 'id', 'query'))
+ types = set(('get', 'result', 'set', 'error'))
+ plugin_attrib = name
+
+ def __init__(self, *args, **kwargs):
+ """
+ Initialize a new <iq> stanza with an 'id' value.
+
+ Overrides StanzaBase.__init__.
+ """
+ StanzaBase.__init__(self, *args, **kwargs)
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.setPayload = self.set_payload
+ self.getQuery = self.get_query
+ self.setQuery = self.set_query
+ self.delQuery = self.del_query
+
+ if self['id'] == '':
+ if self.stream is not None:
+ self['id'] = self.stream.getNewId()
+ else:
+ self['id'] = '0'
+
+ def unhandled(self):
+ """
+ Send a feature-not-implemented error if the stanza is not handled.
+
+ Overrides StanzaBase.unhandled.
+ """
+ if self['type'] in ('get', 'set'):
+ self.reply()
+ self['error']['condition'] = 'feature-not-implemented'
+ self['error']['text'] = 'No handlers registered for this request.'
+ self.send()
+
+ def set_payload(self, value):
+ """
+ Set the XML contents of the <iq> stanza.
+
+ Arguments:
+ value -- An XML object to use as the <iq> stanza's contents
+ """
+ self.clear()
+ StanzaBase.set_payload(self, value)
+ return self
+
+ def set_query(self, value):
+ """
+ Add or modify a <query> element.
+
+ Query elements are differentiated by their namespace.
+
+ Arguments:
+ value -- The namespace of the <query> element.
+ """
+ query = self.xml.find("{%s}query" % value)
+ if query is None and value:
+ self.clear()
+ query = ET.Element("{%s}query" % value)
+ self.xml.append(query)
+ return self
+
+ def get_query(self):
+ """Return the namespace of the <query> element."""
+ for child in self.xml.getchildren():
+ if child.tag.endswith('query'):
+ ns = child.tag.split('}')[0]
+ if '{' in ns:
+ ns = ns[1:]
+ return ns
+ return ''
+
+ def del_query(self):
+ """Remove the <query> element."""
+ for child in self.xml.getchildren():
+ if child.tag.endswith('query'):
+ self.xml.remove(child)
+ return self
+
+ def reply(self):
+ """
+ Send a reply <iq> stanza.
+
+ Overrides StanzaBase.reply
+
+ Sets the 'type' to 'result' in addition to the default
+ StanzaBase.reply behavior.
+ """
+ self['type'] = 'result'
+ StanzaBase.reply(self)
+ return self
+
+ def send(self, block=True, timeout=RESPONSE_TIMEOUT):
+ """
+ Send an <iq> stanza over the XML stream.
+
+ The send call can optionally block until a response is received or
+ a timeout occurs. Be aware that using blocking in non-threaded event
+ handlers can drastically impact performance.
+
+ Overrides StanzaBase.send
+
+ Arguments:
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ """
+ if block and self['type'] in ('get', 'set'):
+ waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
+ self.stream.registerHandler(waitfor)
+ StanzaBase.send(self)
+ return waitfor.wait(timeout)
+ else:
+ return StanzaBase.send(self)
diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py
new file mode 100644
index 0000000..66c74d8
--- /dev/null
+++ b/sleekxmpp/stanza/message.py
@@ -0,0 +1,165 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream import StanzaBase, ET
+
+
+class Message(RootStanza):
+
+ """
+ XMPP's <message> stanzas are a "push" mechanism to send information
+ to other XMPP entities without requiring a response.
+
+ Chat clients will typically use <message> stanzas that have a type
+ of either "chat" or "groupchat".
+
+ When handling a message event, be sure to check if the message is
+ an error response.
+
+ Example <message> stanzas:
+ <message to="user1@example.com" from="user2@example.com">
+ <body>Hi!</body>
+ </message>
+
+ <message type="groupchat" to="room@conference.example.com">
+ <body>Hi everyone!</body>
+ </message>
+
+ Stanza Interface:
+ body -- The main contents of the message.
+ subject -- An optional description of the message's contents.
+ mucroom -- (Read-only) The name of the MUC room that sent the message.
+ mucnick -- (Read-only) The MUC nickname of message's sender.
+
+ Attributes:
+ types -- May be one of: normal, chat, headline, groupchat, or error.
+
+ Methods:
+ setup -- Overrides StanzaBase.setup.
+ chat -- Set the message type to 'chat'.
+ normal -- Set the message type to 'normal'.
+ reply -- Overrides StanzaBase.reply
+ get_type -- Overrides StanzaBase interface
+ get_mucroom -- Return the name of the MUC room of the message.
+ set_mucroom -- Dummy method to prevent assignment.
+ del_mucroom -- Dummy method to prevent deletion.
+ get_mucnick -- Return the MUC nickname of the message's sender.
+ set_mucnick -- Dummy method to prevent assignment.
+ del_mucnick -- Dummy method to prevent deletion.
+ """
+
+ namespace = 'jabber:client'
+ name = 'message'
+ interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject',
+ 'mucroom', 'mucnick'))
+ sub_interfaces = set(('body', 'subject'))
+ plugin_attrib = name
+ types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides StanzaBase.setup.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.getType = self.get_type
+ self.getMucroom = self.get_mucroom
+ self.setMucroom = self.set_mucroom
+ self.delMucroom = self.del_mucroom
+ self.getMucnick = self.get_mucnick
+ self.setMucnick = self.set_mucnick
+ self.delMucnick = self.del_mucnick
+
+ return StanzaBase.setup(self, xml)
+
+ def get_type(self):
+ """
+ Return the message type.
+
+ Overrides default stanza interface behavior.
+
+ Returns 'normal' if no type attribute is present.
+ """
+ return self._get_attr('type', 'normal')
+
+ def chat(self):
+ """Set the message type to 'chat'."""
+ self['type'] = 'chat'
+ return self
+
+ def normal(self):
+ """Set the message type to 'chat'."""
+ self['type'] = 'normal'
+ return self
+
+ def reply(self, body=None):
+ """
+ Create a message reply.
+
+ Overrides StanzaBase.reply.
+
+ Sets proper 'to' attribute if the message is from a MUC, and
+ adds a message body if one is given.
+
+ Arguments:
+ body -- Optional text content for the message.
+ """
+ StanzaBase.reply(self)
+ if self['type'] == 'groupchat':
+ self['to'] = self['to'].bare
+
+ del self['id']
+
+ if body is not None:
+ self['body'] = body
+ return self
+
+ def get_mucroom(self):
+ """
+ Return the name of the MUC room where the message originated.
+
+ Read-only stanza interface.
+ """
+ if self['type'] == 'groupchat':
+ return self['from'].bare
+ else:
+ return ''
+
+ def get_mucnick(self):
+ """
+ Return the nickname of the MUC user that sent the message.
+
+ Read-only stanza interface.
+ """
+ if self['type'] == 'groupchat':
+ return self['from'].resource
+ else:
+ return ''
+
+ def set_mucroom(self, value):
+ """Dummy method to prevent modification."""
+ pass
+
+ def del_mucroom(self):
+ """Dummy method to prevent deletion."""
+ pass
+
+ def set_mucnick(self, value):
+ """Dummy method to prevent modification."""
+ pass
+
+ def del_mucnick(self):
+ """Dummy method to prevent deletion."""
+ pass
diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py
new file mode 100644
index 0000000..a9243d1
--- /dev/null
+++ b/sleekxmpp/stanza/nick.py
@@ -0,0 +1,89 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Message, Presence
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
+
+
+class Nick(ElementBase):
+
+ """
+ XEP-0172: User Nickname allows the addition of a <nick> element
+ in several stanza types, including <message> and <presence> stanzas.
+
+ The nickname contained in a <nick> should be the global, friendly or
+ informal name chosen by the owner of a bare JID. The <nick> element
+ may be included when establishing communications with new entities,
+ such as normal XMPP users or MUC services.
+
+ The nickname contained in a <nick> element will not necessarily be
+ the same as the nickname used in a MUC.
+
+ Example stanzas:
+ <message to="user@example.com">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ <body>...</body>
+ </message>
+
+ <presence to="otheruser@example.com" type="subscribe">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ </presence>
+
+ Stanza Interface:
+ nick -- A global, friendly or informal name chosen by a user.
+
+ Methods:
+ setup -- Overrides ElementBase.setup.
+ get_nick -- Return the nickname in the <nick> element.
+ set_nick -- Add a <nick> element with the given nickname.
+ del_nick -- Remove the <nick> element.
+ """
+
+ namespace = 'http://jabber.org/nick/nick'
+ name = 'nick'
+ plugin_attrib = name
+ interfaces = set(('nick',))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides StanzaBase.setup.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.setNick = self.set_nick
+ self.getNick = self.get_nick
+ self.delNick = self.del_nick
+
+ return ElementBase.setup(self, xml)
+
+ def set_nick(self, nick):
+ """
+ Add a <nick> element with the given nickname.
+
+ Arguments:
+ nick -- A human readable, informal name.
+ """
+ self.xml.text = nick
+
+ def get_nick(self):
+ """Return the nickname in the <nick> element."""
+ return self.xml.text
+
+ def del_nick(self):
+ """Remove the <nick> element."""
+ if self.parent is not None:
+ self.parent().xml.remove(self.xml)
+
+
+register_stanza_plugin(Message, Nick)
+register_stanza_plugin(Presence, Nick)
diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py
new file mode 100644
index 0000000..7dcd8f9
--- /dev/null
+++ b/sleekxmpp/stanza/presence.py
@@ -0,0 +1,186 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream import StanzaBase, ET
+
+
+class Presence(RootStanza):
+
+ """
+ XMPP's <presence> stanza allows entities to know the status of other
+ clients and components. Since it is currently the only multi-cast
+ stanza in XMPP, many extensions add more information to <presence>
+ stanzas to broadcast to every entry in the roster, such as
+ capabilities, music choices, or locations (XEP-0115: Entity Capabilities
+ and XEP-0163: Personal Eventing Protocol).
+
+ Since <presence> stanzas are broadcast when an XMPP entity changes
+ its status, the bulk of the traffic in an XMPP network will be from
+ <presence> stanzas. Therefore, do not include more information than
+ necessary in a status message or within a <presence> stanza in order
+ to help keep the network running smoothly.
+
+ Example <presence> stanzas:
+ <presence />
+
+ <presence from="user@example.com">
+ <show>away</show>
+ <status>Getting lunch.</status>
+ <priority>5</priority>
+ </presence>
+
+ <presence type="unavailable" />
+
+ <presence to="user@otherhost.com" type="subscribe" />
+
+ Stanza Interface:
+ priority -- A value used by servers to determine message routing.
+ show -- The type of status, such as away or available for chat.
+ status -- Custom, human readable status message.
+
+ Attributes:
+ types -- One of: available, unavailable, error, probe,
+ subscribe, subscribed, unsubscribe,
+ and unsubscribed.
+ showtypes -- One of: away, chat, dnd, and xa.
+
+ Methods:
+ setup -- Overrides StanzaBase.setup
+ reply -- Overrides StanzaBase.reply
+ set_show -- Set the value of the <show> element.
+ get_type -- Get the value of the type attribute or <show> element.
+ set_type -- Set the value of the type attribute or <show> element.
+ get_priority -- Get the value of the <priority> element.
+ set_priority -- Set the value of the <priority> element.
+ """
+
+ namespace = 'jabber:client'
+ name = 'presence'
+ interfaces = set(('type', 'to', 'from', 'id', 'show',
+ 'status', 'priority'))
+ sub_interfaces = set(('show', 'status', 'priority'))
+ plugin_attrib = name
+
+ types = set(('available', 'unavailable', 'error', 'probe', 'subscribe',
+ 'subscribed', 'unsubscribe', 'unsubscribed'))
+ showtypes = set(('dnd', 'chat', 'xa', 'away'))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.setShow = self.set_show
+ self.getType = self.get_type
+ self.setType = self.set_type
+ self.delType = self.get_type
+ self.getPriority = self.get_priority
+ self.setPriority = self.set_priority
+
+ return StanzaBase.setup(self, xml)
+
+ def exception(self, e):
+ """
+ Override exception passback for presence.
+ """
+ pass
+
+ def set_show(self, show):
+ """
+ Set the value of the <show> element.
+
+ Arguments:
+ show -- Must be one of: away, chat, dnd, or xa.
+ """
+ if show is None:
+ self._del_sub('show')
+ elif show in self.showtypes:
+ self._set_sub_text('show', text=show)
+ return self
+
+ def get_type(self):
+ """
+ Return the value of the <presence> stanza's type attribute, or
+ the value of the <show> element.
+ """
+ out = self._get_attr('type')
+ if not out:
+ out = self['show']
+ if not out or out is None:
+ out = 'available'
+ return out
+
+ def set_type(self, value):
+ """
+ Set the type attribute's value, and the <show> element
+ if applicable.
+
+ Arguments:
+ value -- Must be in either self.types or self.showtypes.
+ """
+ if value in self.types:
+ self['show'] = None
+ if value == 'available':
+ value = ''
+ self._set_attr('type', value)
+ elif value in self.showtypes:
+ self['show'] = value
+ return self
+
+ def del_type(self):
+ """
+ Remove both the type attribute and the <show> element.
+ """
+ self._del_attr('type')
+ self._del_sub('show')
+
+ def set_priority(self, value):
+ """
+ Set the entity's priority value. Some server use priority to
+ determine message routing behavior.
+
+ Bot clients should typically use a priority of 0 if the same
+ JID is used elsewhere by a human-interacting client.
+
+ Arguments:
+ value -- An integer value greater than or equal to 0.
+ """
+ self._set_sub_text('priority', text=str(value))
+
+ def get_priority(self):
+ """
+ Return the value of the <presence> element as an integer.
+ """
+ p = self._get_sub_text('priority')
+ if not p:
+ p = 0
+ try:
+ return int(p)
+ except ValueError:
+ # The priority is not a number: we consider it 0 as a default
+ return 0
+
+ def reply(self):
+ """
+ Set the appropriate presence reply type.
+
+ Overrides StanzaBase.reply.
+ """
+ if self['type'] == 'unsubscribe':
+ self['type'] = 'unsubscribed'
+ elif self['type'] == 'subscribe':
+ self['type'] = 'subscribed'
+ return StanzaBase.reply(self)
diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py
new file mode 100644
index 0000000..2677ea9
--- /dev/null
+++ b/sleekxmpp/stanza/rootstanza.py
@@ -0,0 +1,66 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import traceback
+import sys
+
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.stanza import Error
+from sleekxmpp.xmlstream import ET, StanzaBase, register_stanza_plugin
+
+
+class RootStanza(StanzaBase):
+
+ """
+ A top-level XMPP stanza in an XMLStream.
+
+ The RootStanza class provides a more XMPP specific exception
+ handler than provided by the generic StanzaBase class.
+
+ Methods:
+ exception -- Overrides StanzaBase.exception
+ """
+
+ def exception(self, e):
+ """
+ Create and send an error reply.
+
+ Typically called when an event handler raises an exception.
+ The error's type and text content are based on the exception
+ object's type and content.
+
+ Overrides StanzaBase.exception.
+
+ Arguments:
+ e -- Exception object
+ """
+ self.reply()
+ if isinstance(e, XMPPError):
+ # We raised this deliberately
+ self['error']['condition'] = e.condition
+ self['error']['text'] = e.text
+ if e.extension is not None:
+ # Extended error tag
+ extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension),
+ e.extension_args)
+ self['error'].append(extxml)
+ self['error']['type'] = e.etype
+ else:
+ # We probably didn't raise this on purpose, so send a traceback
+ self['error']['condition'] = 'undefined-condition'
+ if sys.version_info < (3, 0):
+ self['error']['text'] = "SleekXMPP got into trouble."
+ else:
+ self['error']['text'] = traceback.format_tb(e.__traceback__)
+ logging.exception('Error handling {%s}%s stanza' %
+ (self.namespace, self.name))
+ self.send()
+
+
+register_stanza_plugin(RootStanza, Error)
diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py
new file mode 100644
index 0000000..8f154a2
--- /dev/null
+++ b/sleekxmpp/stanza/roster.py
@@ -0,0 +1,125 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Iq
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Roster(ElementBase):
+
+ """
+ Example roster stanzas:
+ <iq type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" subscription="both" name="User">
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+
+ Stanza Inteface:
+ items -- A dictionary of roster entries contained
+ in the stanza.
+
+ Methods:
+ get_items -- Return a dictionary of roster entries.
+ set_items -- Add <item> elements.
+ del_items -- Remove all <item> elements.
+ """
+
+ namespace = 'jabber:iq:roster'
+ name = 'query'
+ plugin_attrib = 'roster'
+ interfaces = set(('items',))
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides StanzaBase.setup.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.setItems = self.set_items
+ self.getItems = self.get_items
+ self.delItems = self.del_items
+
+ return ElementBase.setup(self, xml)
+
+ def set_items(self, items):
+ """
+ Set the roster entries in the <roster> stanza.
+
+ Uses a dictionary using JIDs as keys, where each entry is itself
+ a dictionary that contains:
+ name -- An alias or nickname for the JID.
+ subscription -- The subscription type. Can be one of 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names to which the JID
+ has been assigned.
+
+ Arguments:
+ items -- A dictionary of roster entries.
+ """
+ self.del_items()
+ for jid in items:
+ ijid = str(jid)
+ item = ET.Element('{jabber:iq:roster}item', {'jid': ijid})
+ if 'subscription' in items[jid]:
+ item.attrib['subscription'] = items[jid]['subscription']
+ if 'name' in items[jid]:
+ name = items[jid]['name']
+ if name is not None:
+ item.attrib['name'] = name
+ if 'groups' in items[jid]:
+ for group in items[jid]['groups']:
+ groupxml = ET.Element('{jabber:iq:roster}group')
+ groupxml.text = group
+ item.append(groupxml)
+ self.xml.append(item)
+ return self
+
+ def get_items(self):
+ """
+ Return a dictionary of roster entries.
+
+ Each item is keyed using its JID, and contains:
+ name -- An assigned alias or nickname for the JID.
+ subscription -- The subscription type. Can be one of 'to',
+ 'from', 'both', 'none', or 'remove'.
+ groups -- A list of group names to which the JID has
+ been assigned.
+ """
+ items = {}
+ itemsxml = self.xml.findall('{jabber:iq:roster}item')
+ if itemsxml is not None:
+ for itemxml in itemsxml:
+ item = {}
+ item['name'] = itemxml.get('name', '')
+ item['subscription'] = itemxml.get('subscription', '')
+ item['groups'] = []
+ groupsxml = itemxml.findall('{jabber:iq:roster}group')
+ if groupsxml is not None:
+ for groupxml in groupsxml:
+ item['groups'].append(groupxml.text)
+ items[itemxml.get('jid')] = item
+ return items
+
+ def del_items(self):
+ """
+ Remove all <item> elements from the roster stanza.
+ """
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+
+register_stanza_plugin(Iq, Roster)
diff --git a/sleekxmpp/test/__init__.py b/sleekxmpp/test/__init__.py
new file mode 100644
index 0000000..54d4dc5
--- /dev/null
+++ b/sleekxmpp/test/__init__.py
@@ -0,0 +1,11 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.test.mocksocket import TestSocket
+from sleekxmpp.test.livesocket import TestLiveSocket
+from sleekxmpp.test.sleektest import *
diff --git a/sleekxmpp/test/livesocket.py b/sleekxmpp/test/livesocket.py
new file mode 100644
index 0000000..5e8c547
--- /dev/null
+++ b/sleekxmpp/test/livesocket.py
@@ -0,0 +1,145 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import socket
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+
+class TestLiveSocket(object):
+
+ """
+ A live test socket that reads and writes to queues in
+ addition to an actual networking socket.
+
+ Methods:
+ next_sent -- Return the next sent stanza.
+ next_recv -- Return the next received stanza.
+ recv_data -- Dummy method to have same interface as TestSocket.
+ recv -- Read the next stanza from the socket.
+ send -- Write a stanza to the socket.
+ makefile -- Dummy call, returns self.
+ read -- Read the next stanza from the socket.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ Create a new, live test socket.
+
+ Arguments:
+ Same as arguments for socket.socket
+ """
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.recv_buffer = []
+ self.recv_queue = queue.Queue()
+ self.send_queue = queue.Queue()
+ self.is_live = True
+
+ def __getattr__(self, name):
+ """
+ Return attribute values of internal, live socket.
+
+ Arguments:
+ name -- Name of the attribute requested.
+ """
+
+ return getattr(self.socket, name)
+
+ # ------------------------------------------------------------------
+ # Testing Interface
+
+ def next_sent(self, timeout=None):
+ """
+ Get the next stanza that has been sent.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ return self.send_queue.get(**args)
+ except:
+ return None
+
+ def next_recv(self, timeout=None):
+ """
+ Get the next stanza that has been received.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ if self.recv_buffer:
+ return self.recv_buffer.pop(0)
+ else:
+ return self.recv_queue.get(**args)
+ except:
+ return None
+
+ def recv_data(self, data):
+ """
+ Add data to a receive buffer for cases when more than a single stanza
+ was received.
+ """
+ self.recv_buffer.append(data)
+
+ # ------------------------------------------------------------------
+ # Socket Interface
+
+ def recv(self, *args, **kwargs):
+ """
+ Read data from the socket.
+
+ Store a copy in the receive queue.
+
+ Arguments:
+ Placeholders. Same as for socket.recv.
+ """
+ data = self.socket.recv(*args, **kwargs)
+ self.recv_queue.put(data)
+ return data
+
+ def send(self, data):
+ """
+ Send data on the socket.
+
+ Store a copy in the send queue.
+
+ Arguments:
+ data -- String value to write.
+ """
+ self.send_queue.put(data)
+ self.socket.send(data)
+
+ # ------------------------------------------------------------------
+ # File Socket
+
+ def makefile(self, *args, **kwargs):
+ """
+ File socket version to use with ElementTree.
+
+ Arguments:
+ Placeholders, same as socket.makefile()
+ """
+ return self
+
+ def read(self, *args, **kwargs):
+ """
+ Implement the file socket read interface.
+
+ Arguments:
+ Placeholders, same as socket.recv()
+ """
+ return self.recv(*args, **kwargs)
diff --git a/sleekxmpp/test/mocksocket.py b/sleekxmpp/test/mocksocket.py
new file mode 100644
index 0000000..e3ddd70
--- /dev/null
+++ b/sleekxmpp/test/mocksocket.py
@@ -0,0 +1,140 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import socket
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+
+class TestSocket(object):
+
+ """
+ A dummy socket that reads and writes to queues instead
+ of an actual networking socket.
+
+ Methods:
+ next_sent -- Return the next sent stanza.
+ recv_data -- Make a stanza available to read next.
+ recv -- Read the next stanza from the socket.
+ send -- Write a stanza to the socket.
+ makefile -- Dummy call, returns self.
+ read -- Read the next stanza from the socket.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """
+ Create a new test socket.
+
+ Arguments:
+ Same as arguments for socket.socket
+ """
+ self.socket = socket.socket(*args, **kwargs)
+ self.recv_queue = queue.Queue()
+ self.send_queue = queue.Queue()
+ self.is_live = False
+
+ def __getattr__(self, name):
+ """
+ Return attribute values of internal, dummy socket.
+
+ Some attributes and methods are disabled to prevent the
+ socket from connecting to the network.
+
+ Arguments:
+ name -- Name of the attribute requested.
+ """
+
+ def dummy(*args):
+ """Method to do nothing and prevent actual socket connections."""
+ return None
+
+ overrides = {'connect': dummy,
+ 'close': dummy,
+ 'shutdown': dummy}
+
+ return overrides.get(name, getattr(self.socket, name))
+
+ # ------------------------------------------------------------------
+ # Testing Interface
+
+ def next_sent(self, timeout=None):
+ """
+ Get the next stanza that has been 'sent'.
+
+ Arguments:
+ timeout -- Optional timeout for waiting for a new value.
+ """
+ args = {'block': False}
+ if timeout is not None:
+ args = {'block': True, 'timeout': timeout}
+ try:
+ return self.send_queue.get(**args)
+ except:
+ return None
+
+ def recv_data(self, data):
+ """
+ Add data to the receiving queue.
+
+ Arguments:
+ data -- String data to 'write' to the socket to be received
+ by the XMPP client.
+ """
+ self.recv_queue.put(data)
+
+ # ------------------------------------------------------------------
+ # Socket Interface
+
+ def recv(self, *args, **kwargs):
+ """
+ Read a value from the received queue.
+
+ Arguments:
+ Placeholders. Same as for socket.Socket.recv.
+ """
+ return self.read(block=True)
+
+ def send(self, data):
+ """
+ Send data by placing it in the send queue.
+
+ Arguments:
+ data -- String value to write.
+ """
+ self.send_queue.put(data)
+
+ # ------------------------------------------------------------------
+ # File Socket
+
+ def makefile(self, *args, **kwargs):
+ """
+ File socket version to use with ElementTree.
+
+ Arguments:
+ Placeholders, same as socket.Socket.makefile()
+ """
+ return self
+
+ def read(self, block=True, timeout=None, **kwargs):
+ """
+ Implement the file socket interface.
+
+ Arguments:
+ block -- Indicate if the read should block until a
+ value is ready.
+ timeout -- Time in seconds a block should last before
+ returning None.
+ """
+ if timeout is not None:
+ block = True
+ try:
+ return self.recv_queue.get(block, timeout)
+ except:
+ return None
diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py
new file mode 100644
index 0000000..2901e59
--- /dev/null
+++ b/sleekxmpp/test/sleektest.py
@@ -0,0 +1,763 @@
+"""
+
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import unittest
+
+import sleekxmpp
+from sleekxmpp import ClientXMPP, ComponentXMPP
+from sleekxmpp.stanza import Message, Iq, Presence
+from sleekxmpp.test import TestSocket, TestLiveSocket
+from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin
+from sleekxmpp.xmlstream.tostring import tostring
+
+
+class SleekTest(unittest.TestCase):
+
+ """
+ A SleekXMPP specific TestCase class that provides
+ methods for comparing message, iq, and presence stanzas.
+
+ Methods:
+ Message -- Create a Message stanza object.
+ Iq -- Create an Iq stanza object.
+ Presence -- Create a Presence stanza object.
+ check_stanza -- Compare a generic stanza against an XML string.
+ check_message -- Compare a Message stanza against an XML string.
+ check_iq -- Compare an Iq stanza against an XML string.
+ check_presence -- Compare a Presence stanza against an XML string.
+ stream_start -- Initialize a dummy XMPP client.
+ stream_recv -- Queue data for XMPP client to receive.
+ stream_make_header -- Create a stream header.
+ stream_send_header -- Check that the given header has been sent.
+ stream_send_message -- Check that the XMPP client sent the given
+ Message stanza.
+ stream_send_iq -- Check that the XMPP client sent the given
+ Iq stanza.
+ stream_send_presence -- Check thatt the XMPP client sent the given
+ Presence stanza.
+ stream_send_stanza -- Check that the XMPP client sent the given
+ generic stanza.
+ stream_close -- Disconnect the XMPP client.
+ fix_namespaces -- Add top-level namespace to an XML object.
+ compare -- Compare XML objects against each other.
+ """
+
+ def parse_xml(self, xml_string):
+ try:
+ xml = ET.fromstring(xml_string)
+ return xml
+ except SyntaxError as e:
+ if 'unbound' in e.msg:
+ known_prefixes = {
+ 'stream': 'http://etherx.jabber.org/streams'}
+
+ prefix = xml_string.split('<')[1].split(':')[0]
+ if prefix in known_prefixes:
+ xml_string = '<fixns xmlns:%s="%s">%s</fixns>' % (
+ prefix,
+ known_prefixes[prefix],
+ xml_string)
+ xml = self.parse_xml(xml_string)
+ xml = xml.getchildren()[0]
+ return xml
+
+ # ------------------------------------------------------------------
+ # Shortcut methods for creating stanza objects
+
+ def Message(self, *args, **kwargs):
+ """
+ Create a Message stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Message's values.
+ """
+ return Message(None, *args, **kwargs)
+
+ def Iq(self, *args, **kwargs):
+ """
+ Create an Iq stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Iq's values.
+ """
+ return Iq(None, *args, **kwargs)
+
+ def Presence(self, *args, **kwargs):
+ """
+ Create a Presence stanza.
+
+ Uses same arguments as StanzaBase.__init__
+
+ Arguments:
+ xml -- An XML object to use for the Iq's values.
+ """
+ return Presence(None, *args, **kwargs)
+
+
+
+ def check_JID(self, jid, user=None, domain=None, resource=None,
+ bare=None, full=None, string=None):
+ """
+ Verify the components of a JID.
+
+ Arguments:
+ jid -- The JID object to test.
+ user -- Optional. The user name portion of the JID.
+ domain -- Optional. The domain name portion of the JID.
+ resource -- Optional. The resource portion of the JID.
+ bare -- Optional. The bare JID.
+ full -- Optional. The full JID.
+ string -- Optional. The string version of the JID.
+ """
+ if user is not None:
+ self.assertEqual(jid.user, user,
+ "User does not match: %s" % jid.user)
+ if domain is not None:
+ self.assertEqual(jid.domain, domain,
+ "Domain does not match: %s" % jid.domain)
+ if resource is not None:
+ self.assertEqual(jid.resource, resource,
+ "Resource does not match: %s" % jid.resource)
+ if bare is not None:
+ self.assertEqual(jid.bare, bare,
+ "Bare JID does not match: %s" % jid.bare)
+ if full is not None:
+ self.assertEqual(jid.full, full,
+ "Full JID does not match: %s" % jid.full)
+ if string is not None:
+ self.assertEqual(str(jid), string,
+ "String does not match: %s" % str(jid))
+
+ # ------------------------------------------------------------------
+ # Methods for comparing stanza objects to XML strings
+
+ def check_stanza(self, stanza_class, stanza, xml_string,
+ defaults=None, use_values=True):
+ """
+ Create and compare several stanza objects to a correct XML string.
+
+ If use_values is False, test using getStanzaValues() and
+ setStanzaValues() will not be used.
+
+ Some stanzas provide default values for some interfaces, but
+ these defaults can be problematic for testing since they can easily
+ be forgotten when supplying the XML string. A list of interfaces that
+ use defaults may be provided and the generated stanzas will use the
+ default values for those interfaces if needed.
+
+ However, correcting the supplied XML is not possible for interfaces
+ that add or remove XML elements. Only interfaces that map to XML
+ attributes may be set using the defaults parameter. The supplied XML
+ must take into account any extra elements that are included by default.
+
+ Arguments:
+ stanza_class -- The class of the stanza being tested.
+ stanza -- The stanza object to test.
+ xml_string -- A string version of the correct XML expected.
+ defaults -- A list of stanza interfaces that have default
+ values. These interfaces will be set to their
+ defaults for the given and generated stanzas to
+ prevent unexpected test failures.
+ use_values -- Indicates if testing using getStanzaValues() and
+ setStanzaValues() should be used. Defaults to
+ True.
+ """
+ xml = self.parse_xml(xml_string)
+
+ # Ensure that top level namespaces are used, even if they
+ # were not provided.
+ self.fix_namespaces(stanza.xml, 'jabber:client')
+ self.fix_namespaces(xml, 'jabber:client')
+
+ stanza2 = stanza_class(xml=xml)
+
+ if use_values:
+ # Using getStanzaValues() and setStanzaValues() will add
+ # XML for any interface that has a default value. We need
+ # to set those defaults on the existing stanzas and XML
+ # so that they will compare correctly.
+ default_stanza = stanza_class()
+ if defaults is None:
+ defaults = []
+ for interface in defaults:
+ stanza[interface] = stanza[interface]
+ stanza2[interface] = stanza2[interface]
+ # Can really only automatically add defaults for top
+ # level attribute values. Anything else must be accounted
+ # for in the provided XML string.
+ if interface not in xml.attrib:
+ if interface in default_stanza.xml.attrib:
+ value = default_stanza.xml.attrib[interface]
+ xml.attrib[interface] = value
+
+ values = stanza2.getStanzaValues()
+ stanza3 = stanza_class()
+ stanza3.setStanzaValues(values)
+
+ debug = "Three methods for creating stanzas do not match.\n"
+ debug += "Given XML:\n%s\n" % tostring(xml)
+ debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
+ debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
+ debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml)
+ result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml)
+ else:
+ debug = "Two methods for creating stanzas do not match.\n"
+ debug += "Given XML:\n%s\n" % tostring(xml)
+ debug += "Given stanza:\n%s\n" % tostring(stanza.xml)
+ debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml)
+ result = self.compare(xml, stanza.xml, stanza2.xml)
+
+ self.failUnless(result, debug)
+
+ def check_message(self, msg, xml_string, use_values=True):
+ """
+ Create and compare several message stanza objects to a
+ correct XML string.
+
+ If use_values is False, the test using getStanzaValues() and
+ setStanzaValues() will not be used.
+
+ Arguments:
+ msg -- The Message stanza object to check.
+ xml_string -- The XML contents to compare against.
+ use_values -- Indicates if the test using getStanzaValues
+ and setStanzaValues should be used. Defaults
+ to True.
+ """
+
+ return self.check_stanza(Message, msg, xml_string,
+ defaults=['type'],
+ use_values=use_values)
+
+ def check_iq(self, iq, xml_string, use_values=True):
+ """
+ Create and compare several iq stanza objects to a
+ correct XML string.
+
+ If use_values is False, the test using getStanzaValues() and
+ setStanzaValues() will not be used.
+
+ Arguments:
+ iq -- The Iq stanza object to check.
+ xml_string -- The XML contents to compare against.
+ use_values -- Indicates if the test using getStanzaValues
+ and setStanzaValues should be used. Defaults
+ to True.
+ """
+ return self.check_stanza(Iq, iq, xml_string, use_values=use_values)
+
+ def check_presence(self, pres, xml_string, use_values=True):
+ """
+ Create and compare several presence stanza objects to a
+ correct XML string.
+
+ If use_values is False, the test using getStanzaValues() and
+ setStanzaValues() will not be used.
+
+ Arguments:
+ iq -- The Iq stanza object to check.
+ xml_string -- The XML contents to compare against.
+ use_values -- Indicates if the test using getStanzaValues
+ and setStanzaValues should be used. Defaults
+ to True.
+ """
+ return self.check_stanza(Presence, pres, xml_string,
+ defaults=['priority'],
+ use_values=use_values)
+
+ # ------------------------------------------------------------------
+ # Methods for simulating stanza streams.
+
+ def stream_start(self, mode='client', skip=True, header=None,
+ socket='mock', jid='tester@localhost',
+ password='test', server='localhost',
+ port=5222):
+ """
+ Initialize an XMPP client or component using a dummy XML stream.
+
+ Arguments:
+ mode -- Either 'client' or 'component'. Defaults to 'client'.
+ skip -- Indicates if the first item in the sent queue (the
+ stream header) should be removed. Tests that wish
+ to test initializing the stream should set this to
+ False. Otherwise, the default of True should be used.
+ socket -- Either 'mock' or 'live' to indicate if the socket
+ should be a dummy, mock socket or a live, functioning
+ socket. Defaults to 'mock'.
+ jid -- The JID to use for the connection.
+ Defaults to 'tester@localhost'.
+ password -- The password to use for the connection.
+ Defaults to 'test'.
+ server -- The name of the XMPP server. Defaults to 'localhost'.
+ port -- The port to use when connecting to the server.
+ Defaults to 5222.
+ """
+
+ if mode == 'client':
+ self.xmpp = ClientXMPP(jid, password)
+ elif mode == 'component':
+ self.xmpp = ComponentXMPP(jid, password,
+ server, port)
+ else:
+ raise ValueError("Unknown XMPP connection mode.")
+
+ if socket == 'mock':
+ self.xmpp.set_socket(TestSocket())
+
+ # Simulate connecting for mock sockets.
+ self.xmpp.auto_reconnect = False
+ self.xmpp.is_client = True
+ self.xmpp.state._set_state('connected')
+
+ # Must have the stream header ready for xmpp.process() to work.
+ if not header:
+ header = self.xmpp.stream_header
+ self.xmpp.socket.recv_data(header)
+ elif socket == 'live':
+ self.xmpp.socket_class = TestLiveSocket
+ self.xmpp.connect()
+ else:
+ raise ValueError("Unknown socket type.")
+
+ self.xmpp.register_plugins()
+ self.xmpp.process(threaded=True)
+ if skip:
+ # Clear startup stanzas
+ self.xmpp.socket.next_sent(timeout=1)
+ if mode == 'component':
+ self.xmpp.socket.next_sent(timeout=1)
+
+ def stream_make_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ version="1.0",
+ xml_header=True):
+ """
+ Create a stream header to be received by the test XMPP agent.
+
+ The header must be saved and passed to stream_start.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ """
+ header = '<stream:stream %s>'
+ parts = []
+ if xml_header:
+ header = '<?xml version="1.0"?>' + header
+ if sto:
+ parts.append('to="%s"' % sto)
+ if sfrom:
+ parts.append('from="%s"' % sfrom)
+ if sid:
+ parts.append('id="%s"' % sid)
+ parts.append('version="%s"' % version)
+ parts.append('xmlns:stream="%s"' % stream_ns)
+ parts.append('xmlns="%s"' % default_ns)
+ return header % ' '.join(parts)
+
+ def stream_recv(self, data, stanza_class=StanzaBase, defaults=[],
+ use_values=True, timeout=1):
+ """
+ Pass data to the dummy XMPP client as if it came from an XMPP server.
+
+ If using a live connection, verify what the server has sent.
+
+ Arguments:
+ data -- String stanza XML to be received and processed by
+ the XMPP client or component.
+ stanza_class -- The stanza object class for verifying data received
+ by a live connection. Defaults to StanzaBase.
+ defaults -- A list of stanza interfaces with default values that
+ may interfere with comparisons.
+ use_values -- Indicates if stanza comparisons should test using
+ getStanzaValues() and setStanzaValues().
+ Defaults to True.
+ timeout -- Time to wait in seconds for data to be received by
+ a live connection.
+ """
+ if self.xmpp.socket.is_live:
+ # we are working with a live connection, so we should
+ # verify what has been received instead of simulating
+ # receiving data.
+ recv_data = self.xmpp.socket.next_recv(timeout)
+ if recv_data is None:
+ return False
+ stanza = stanza_class(xml=self.parse_xml(recv_data))
+ return self.check_stanza(stanza_class, stanza, data,
+ defaults=defaults,
+ use_values=use_values)
+ else:
+ # place the data in the dummy socket receiving queue.
+ data = str(data)
+ self.xmpp.socket.recv_data(data)
+
+ def stream_recv_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ version="1.0",
+ xml_header=False,
+ timeout=1):
+ """
+ Check that a given stream header was received.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id. Set to None to ignore.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ timeout -- Length of time to wait in seconds for a
+ response.
+ """
+ header = self.stream_make_header(sto, sfrom, sid,
+ stream_ns=stream_ns,
+ default_ns=default_ns,
+ version=version,
+ xml_header=xml_header)
+ recv_header = self.xmpp.socket.next_recv(timeout)
+ if recv_header is None:
+ raise ValueError("Socket did not return data.")
+
+ # Apply closing elements so that we can construct
+ # XML objects for comparison.
+ header2 = header + '</stream:stream>'
+ recv_header2 = recv_header + '</stream:stream>'
+
+ xml = self.parse_xml(header2)
+ recv_xml = self.parse_xml(recv_header2)
+
+ if sid is None:
+ # Ignore the id sent by the server since
+ # we can't know in advance what it will be.
+ if 'id' in recv_xml.attrib:
+ del recv_xml.attrib['id']
+
+ # Ignore the xml:lang attribute for now.
+ if 'xml:lang' in recv_xml.attrib:
+ del recv_xml.attrib['xml:lang']
+ xml_ns = 'http://www.w3.org/XML/1998/namespace'
+ if '{%s}lang' % xml_ns in recv_xml.attrib:
+ del recv_xml.attrib['{%s}lang' % xml_ns]
+
+ if recv_xml.getchildren:
+ # We received more than just the header
+ for xml in recv_xml.getchildren():
+ self.xmpp.socket.recv_data(tostring(xml))
+
+ attrib = recv_xml.attrib
+ recv_xml.clear()
+ recv_xml.attrib = attrib
+
+ self.failUnless(
+ self.compare(xml, recv_xml),
+ "Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % (
+ '%s %s' % (xml.tag, xml.attrib),
+ '%s %s' % (recv_xml.tag, recv_xml.attrib)))
+ #tostring(xml), tostring(recv_xml)))#recv_header))
+
+ def stream_recv_feature(self, data, use_values=True, timeout=1):
+ """
+ """
+ if self.xmpp.socket.is_live:
+ # we are working with a live connection, so we should
+ # verify what has been received instead of simulating
+ # receiving data.
+ recv_data = self.xmpp.socket.next_recv(timeout)
+ if recv_data is None:
+ return False
+ xml = self.parse_xml(data)
+ recv_xml = self.parse_xml(recv_data)
+ self.failUnless(self.compare(xml, recv_xml),
+ "Features do not match.\nDesired:\n%s\nReceived:\n%s" % (
+ tostring(xml), tostring(recv_xml)))
+ else:
+ # place the data in the dummy socket receiving queue.
+ data = str(data)
+ self.xmpp.socket.recv_data(data)
+
+
+
+ def stream_recv_message(self, data, use_values=True, timeout=1):
+ """
+ """
+ return self.stream_recv(data, stanza_class=Message,
+ defaults=['type'],
+ use_values=use_values,
+ timeout=timeout)
+
+ def stream_recv_iq(self, data, use_values=True, timeout=1):
+ """
+ """
+ return self.stream_recv(data, stanza_class=Iq,
+ use_values=use_values,
+ timeout=timeout)
+
+ def stream_recv_presence(self, data, use_values=True, timeout=1):
+ """
+ """
+ return self.stream_recv(data, stanza_class=Presence,
+ defaults=['priority'],
+ use_values=use_values,
+ timeout=timeout)
+
+ def stream_send_header(self, sto='',
+ sfrom='',
+ sid='',
+ stream_ns="http://etherx.jabber.org/streams",
+ default_ns="jabber:client",
+ version="1.0",
+ xml_header=False,
+ timeout=1):
+ """
+ Check that a given stream header was sent.
+
+ Arguments:
+ sto -- The recipient of the stream header.
+ sfrom -- The agent sending the stream header.
+ sid -- The stream's id.
+ stream_ns -- The namespace of the stream's root element.
+ default_ns -- The default stanza namespace.
+ version -- The stream version.
+ xml_header -- Indicates if the XML version header should be
+ appended before the stream header.
+ timeout -- Length of time to wait in seconds for a
+ response.
+ """
+ header = self.stream_make_header(sto, sfrom, sid,
+ stream_ns=stream_ns,
+ default_ns=default_ns,
+ version=version,
+ xml_header=xml_header)
+ sent_header = self.xmpp.socket.next_sent(timeout)
+ if sent_header is None:
+ raise ValueError("Socket did not return data.")
+
+ # Apply closing elements so that we can construct
+ # XML objects for comparison.
+ header2 = header + '</stream:stream>'
+ sent_header2 = sent_header + b'</stream:stream>'
+
+ xml = self.parse_xml(header2)
+ sent_xml = self.parse_xml(sent_header2)
+
+ self.failUnless(
+ self.compare(xml, sent_xml),
+ "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % (
+ header, sent_header))
+
+ def stream_send_feature(self, data, use_values=True, timeout=1):
+ """
+ """
+ sent_data = self.xmpp.socket.next_sent(timeout)
+ if sent_data is None:
+ return False
+ xml = self.parse_xml(data)
+ sent_xml = self.parse_xml(sent_data)
+ self.failUnless(self.compare(xml, sent_xml),
+ "Features do not match.\nDesired:\n%s\nSent:\n%s" % (
+ tostring(xml), tostring(sent_xml)))
+
+ def stream_send_stanza(self, stanza_class, data, defaults=None,
+ use_values=True, timeout=.1):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using check_stanza.
+
+ Arguments:
+ stanza_class -- The class of the sent stanza object.
+ data -- The XML string of the expected Message stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by check_message.
+ defaults -- A list of stanza interfaces that have defaults
+ values which may interfere with comparisons.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ """
+ if isintance(data, str):
+ data = stanza_class(xml=self.parse_xml(data))
+ sent = self.xmpp.socket.next_sent(timeout)
+ self.check_stanza(stanza_class, data, sent,
+ defaults=defaults,
+ use_values=use_values)
+
+ def stream_send_message(self, data, use_values=True, timeout=.1):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using check_message.
+
+ Arguments:
+ data -- The XML string of the expected Message stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by check_message.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ """
+ if isinstance(data, str):
+ data = self.Message(xml=self.parse_xml(data))
+ sent = self.xmpp.socket.next_sent(timeout)
+ self.check_message(data, sent, use_values)
+
+ def stream_send_iq(self, data, use_values=True, timeout=.1):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using check_iq.
+
+ Arguments:
+ data -- The XML string of the expected Iq stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by check_iq.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ """
+ if isinstance(data, str):
+ data = self.Iq(xml=self.parse_xml(data))
+ sent = self.xmpp.socket.next_sent(timeout)
+ self.check_iq(data, sent, use_values)
+
+ def stream_send_presence(self, data, use_values=True, timeout=.1):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using check_presence.
+
+ Arguments:
+ data -- The XML string of the expected Presence stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by check_presence.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ """
+ if isinstance(data, str):
+ data = self.Presence(xml=self.parse_xml(data))
+ sent = self.xmpp.socket.next_sent(timeout)
+ self.check_presence(data, sent, use_values)
+
+ def stream_close(self):
+ """
+ Disconnect the dummy XMPP client.
+
+ Can be safely called even if stream_start has not been called.
+
+ Must be placed in the tearDown method of a test class to ensure
+ that the XMPP client is disconnected after an error.
+ """
+ if hasattr(self, 'xmpp') and self.xmpp is not None:
+ self.xmpp.socket.recv_data(self.xmpp.stream_footer)
+ self.xmpp.disconnect()
+
+ # ------------------------------------------------------------------
+ # XML Comparison and Cleanup
+
+ def fix_namespaces(self, xml, ns):
+ """
+ Assign a namespace to an element and any children that
+ don't have a namespace.
+
+ Arguments:
+ xml -- The XML object to fix.
+ ns -- The namespace to add to the XML object.
+ """
+ if xml.tag.startswith('{'):
+ return
+ xml.tag = '{%s}%s' % (ns, xml.tag)
+ for child in xml.getchildren():
+ self.fix_namespaces(child, ns)
+
+ def compare(self, xml, *other):
+ """
+ Compare XML objects.
+
+ Arguments:
+ xml -- The XML object to compare against.
+ *other -- The list of XML objects to compare.
+ """
+ if not other:
+ return False
+
+ # Compare multiple objects
+ if len(other) > 1:
+ for xml2 in other:
+ if not self.compare(xml, xml2):
+ return False
+ return True
+
+ other = other[0]
+
+ # Step 1: Check tags
+ if xml.tag != other.tag:
+ return False
+
+ # Step 2: Check attributes
+ if xml.attrib != other.attrib:
+ return False
+
+ # Step 3: Check text
+ if xml.text is None:
+ xml.text = ""
+ if other.text is None:
+ other.text = ""
+ xml.text = xml.text.strip()
+ other.text = other.text.strip()
+
+ if xml.text != other.text:
+ return False
+
+ # Step 4: Check children count
+ if len(xml.getchildren()) != len(other.getchildren()):
+ return False
+
+ # Step 5: Recursively check children
+ for child in xml:
+ child2s = other.findall("%s" % child.tag)
+ if child2s is None:
+ return False
+ for child2 in child2s:
+ if self.compare(child, child2):
+ break
+ else:
+ return False
+
+ # Step 6: Recursively check children the other way.
+ for child in other:
+ child2s = xml.findall("%s" % child.tag)
+ if child2s is None:
+ return False
+ for child2 in child2s:
+ if self.compare(child, child2):
+ break
+ else:
+ return False
+
+ # Everything matches
+ return True
diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/sleekxmpp/thirdparty/__init__.py
diff --git a/sleekxmpp/thirdparty/statemachine.py b/sleekxmpp/thirdparty/statemachine.py
new file mode 100644
index 0000000..b176df0
--- /dev/null
+++ b/sleekxmpp/thirdparty/statemachine.py
@@ -0,0 +1,287 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+import threading
+import time
+import logging
+
+log = logging.getLogger(__name__)
+
+
+class StateMachine(object):
+
+ def __init__(self, states=[]):
+ self.lock = threading.Lock()
+ self.notifier = threading.Event()
+ self.__states = []
+ self.addStates(states)
+ self.__default_state = self.__states[0]
+ self.__current_state = self.__default_state
+
+ def addStates(self, states):
+ self.lock.acquire()
+ try:
+ for state in states:
+ if state in self.__states:
+ raise IndexError("The state '%s' is already in the StateMachine." % state)
+ self.__states.append(state)
+ finally: self.lock.release()
+
+
+ def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}):
+ '''
+ Transition from the given `from_state` to the given `to_state`.
+ This method will return `True` if the state machine is now in `to_state`. It
+ will return `False` if a timeout occurred the transition did not occur.
+ If `wait` is 0 (the default,) this method returns immediately if the state machine
+ is not in `from_state`.
+
+ If you want the thread to block and transition once the state machine to enters
+ `from_state`, set `wait` to a non-negative value. Note there is no 'block
+ indefinitely' flag since this leads to deadlock. If you want to wait indefinitely,
+ choose a reasonable value for `wait` (e.g. 20 seconds) and do so in a while loop like so:
+
+ ::
+
+ while not thread_should_exit and not state_machine.transition('disconnected', 'connecting', wait=20 ):
+ pass # timeout will occur every 20s unless transition occurs
+ if thread_should_exit: return
+ # perform actions here after successful transition
+
+ This allows the thread to be responsive by setting `thread_should_exit=True`.
+
+ The optional `func` argument allows the user to pass a callable operation which occurs
+ within the context of the state transition (e.g. while the state machine is locked.)
+ If `func` returns a True value, the transition will occur. If `func` returns a non-
+ True value or if an exception is thrown, the transition will not occur. Any thrown
+ exception is not caught by the state machine and is the caller's responsibility to handle.
+ If `func` completes normally, this method will return the value returned by `func.` If
+ values for `args` and `kwargs` are provided, they are expanded and passed like so:
+ `func( *args, **kwargs )`.
+ '''
+
+ return self.transition_any((from_state,), to_state, wait=wait,
+ func=func, args=args, kwargs=kwargs)
+
+
+ def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}):
+ '''
+ Transition from any of the given `from_states` to the given `to_state`.
+ '''
+
+ if not (isinstance(from_states,tuple) or isinstance(from_states,list)):
+ raise ValueError("from_states should be a list or tuple")
+
+ for state in from_states:
+ if not state in self.__states:
+ raise ValueError("StateMachine does not contain from_state %s." % state)
+ if not to_state in self.__states:
+ raise ValueError("StateMachine does not contain to_state %s." % to_state)
+
+ start = time.time()
+ while not self.lock.acquire(False):
+ time.sleep(.001)
+ if (start + wait - time.time()) <= 0.0:
+ logging.debug("Could not acquire lock")
+ return False
+
+ while not self.__current_state in from_states:
+ # detect timeout:
+ remainder = start + wait - time.time()
+ if remainder > 0:
+ self.notifier.wait(remainder)
+ else:
+ logging.debug("State was not ready")
+ self.lock.release()
+ return False
+
+ try: # lock is acquired; all other threads will return false or wait until notify/timeout
+ if self.__current_state in from_states: # should always be True due to lock
+
+ # Note that func might throw an exception, but that's OK, it aborts the transition
+ return_val = func(*args,**kwargs) if func is not None else True
+
+ # some 'false' value returned from func,
+ # indicating that transition should not occur:
+ if not return_val: return return_val
+
+ log.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state)
+ self._set_state(to_state)
+ return return_val # some 'true' value returned by func or True if func was None
+ else:
+ log.error("StateMachine bug!! The lock should ensure this doesn't happen!")
+ return False
+ finally:
+ self.notifier.set() # notify any waiting threads that the state has changed.
+ self.notifier.clear()
+ self.lock.release()
+
+
+ def transition_ctx(self, from_state, to_state, wait=0.0):
+ '''
+ Use the state machine as a context manager. The transition occurs on /exit/ from
+ the `with` context, so long as no exception is thrown. For example:
+
+ ::
+
+ with state_machine.transition_ctx('one','two', wait=5) as locked:
+ if locked:
+ # the state machine is currently locked in state 'one', and will
+ # transition to 'two' when the 'with' statement ends, so long as
+ # no exception is thrown.
+ print 'Currently locked in state one: %s' % state_machine['one']
+
+ else:
+ # The 'wait' timed out, and no lock has been acquired
+ print 'Timed out before entering state "one"'
+
+ print 'Since no exception was thrown, we are now in state "two": %s' % state_machine['two']
+
+
+ The other main difference between this method and `transition()` is that the
+ state machine is locked for the duration of the `with` statement. Normally,
+ after a `transition()` occurs, the state machine is immediately unlocked and
+ available to another thread to call `transition()` again.
+ '''
+
+ if not from_state in self.__states:
+ raise ValueError("StateMachine does not contain from_state %s." % from_state)
+ if not to_state in self.__states:
+ raise ValueError("StateMachine does not contain to_state %s." % to_state)
+
+ return _StateCtx(self, from_state, to_state, wait)
+
+
+ def ensure(self, state, wait=0.0, block_on_transition=False):
+ '''
+ Ensure the state machine is currently in `state`, or wait until it enters `state`.
+ '''
+ return self.ensure_any((state,), wait=wait, block_on_transition=block_on_transition)
+
+
+ def ensure_any(self, states, wait=0.0, block_on_transition=False):
+ '''
+ Ensure we are currently in one of the given `states` or wait until
+ we enter one of those states.
+
+ Note that due to the nature of the function, you cannot guarantee that
+ the entirety of some operation completes while you remain in a given
+ state. That would require acquiring and holding a lock, which
+ would mean no other threads could do the same. (You'd essentially
+ be serializing all of the threads that are 'ensuring' their tasks
+ occurred in some state.
+ '''
+ if not (isinstance(states,tuple) or isinstance(states,list)):
+ raise ValueError('states arg should be a tuple or list')
+
+ for state in states:
+ if not state in self.__states:
+ raise ValueError("StateMachine does not contain state '%s'" % state)
+
+ # if we're in the middle of a transition, determine whether we should
+ # 'fall back' to the 'current' state, or wait for the new state, in order to
+ # avoid an operation occurring in the wrong state.
+ # TODO another option would be an ensure_ctx that uses a semaphore to allow
+ # threads to indicate they want to remain in a particular state.
+
+ # will return immediately if no transition is in process.
+ if block_on_transition:
+ # we're not in the middle of a transition; don't hold the lock
+ if self.lock.acquire(False): self.lock.release()
+ # wait for the transition to complete
+ else: self.notifier.wait()
+
+ start = time.time()
+ while not self.__current_state in states:
+ # detect timeout:
+ remainder = start + wait - time.time()
+ if remainder > 0: self.notifier.wait(remainder)
+ else: return False
+ return True
+
+
+ def reset(self):
+ # TODO need to lock before calling this?
+ self.transition(self.__current_state, self.__default_state)
+
+
+ def _set_state(self, state): #unsynchronized, only call internally after lock is acquired
+ self.__current_state = state
+ return state
+
+
+ def current_state(self):
+ '''
+ Return the current state name.
+ '''
+ return self.__current_state
+
+
+ def __getitem__(self, state):
+ '''
+ Non-blocking, non-synchronized test to determine if we are in the given state.
+ Use `StateMachine.ensure(state)` to wait until the machine enters a certain state.
+ '''
+ return self.__current_state == state
+
+ def __str__(self):
+ return "".join(("StateMachine(", ','.join(self.__states), "): ", self.__current_state))
+
+
+
+class _StateCtx:
+
+ def __init__(self, state_machine, from_state, to_state, wait):
+ self.state_machine = state_machine
+ self.from_state = from_state
+ self.to_state = to_state
+ self.wait = wait
+ self._locked = False
+
+ def __enter__(self):
+ start = time.time()
+ while not self.state_machine[self.from_state] or not self.state_machine.lock.acquire(False):
+ # detect timeout:
+ remainder = start + self.wait - time.time()
+ if remainder > 0: self.state_machine.notifier.wait(remainder)
+ else:
+ log.debug('StateMachine timeout while waiting for state: %s', self.from_state)
+ return False
+
+ self._locked = True # lock has been acquired at this point
+ self.state_machine.notifier.clear()
+ log.debug('StateMachine entered context in state: %s',
+ self.state_machine.current_state())
+ return True
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if exc_val is not None:
+ log.exception("StateMachine exception in context, remaining in state: %s\n%s:%s",
+ self.state_machine.current_state(), exc_type.__name__, exc_val)
+
+ if self._locked:
+ if exc_val is None:
+ log.debug(' ==== TRANSITION %s -> %s',
+ self.state_machine.current_state(), self.to_state)
+ self.state_machine._set_state(self.to_state)
+
+ self.state_machine.notifier.set()
+ self.state_machine.lock.release()
+
+ return False # re-raise any exception
+
+if __name__ == '__main__':
+
+ def callback(s, s2):
+ print((1, s.transition('on', 'off', wait=0.0, func=callback, args=[s,s2])))
+ print((2, s2.transition('off', 'on', func=callback, args=[s,s2])))
+ return True
+
+ s = StateMachine(('off', 'on'))
+ s2 = StateMachine(('off', 'on'))
+ print((3, s.transition('off', 'on', wait=0.0, func=callback, args=[s,s2]),))
+ print((s.current_state(), s2.current_state()))
diff --git a/sleekxmpp/xmlstream/__init__.py b/sleekxmpp/xmlstream/__init__.py
new file mode 100644
index 0000000..67b20c5
--- /dev/null
+++ b/sleekxmpp/xmlstream/__init__.py
@@ -0,0 +1,19 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.jid import JID
+from sleekxmpp.xmlstream.scheduler import Scheduler
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase, ET
+from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
+from sleekxmpp.xmlstream.tostring import tostring
+from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT
+from sleekxmpp.xmlstream.xmlstream import RestartStream
+
+__all__ = ['JID', 'Scheduler', 'StanzaBase', 'ElementBase',
+ 'ET', 'StateMachine', 'tostring', 'XMLStream',
+ 'RESPONSE_TIMEOUT', 'RestartStream']
diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py
new file mode 100644
index 0000000..441ff87
--- /dev/null
+++ b/sleekxmpp/xmlstream/filesocket.py
@@ -0,0 +1,41 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from socket import _fileobject
+import socket
+
+
+class FileSocket(_fileobject):
+
+ """
+ Create a file object wrapper for a socket to work around
+ issues present in Python 2.6 when using sockets as file objects.
+
+ The parser for xml.etree.cElementTree requires a file, but we will
+ be reading from the XMPP connection socket instead.
+ """
+
+ def read(self, size=4096):
+ """Read data from the socket as if it were a file."""
+ data = self._sock.recv(size)
+ if data is not None:
+ return data
+
+
+class Socket26(socket._socketobject):
+
+ """
+ A custom socket implementation that uses our own FileSocket class
+ to work around issues in Python 2.6 when using sockets as files.
+ """
+
+ def makefile(self, mode='r', bufsize=-1):
+ """makefile([mode[, bufsize]]) -> file object
+ Return a regular file object corresponding to the socket. The mode
+ and bufsize arguments are as for the built-in open() function."""
+ return FileSocket(self._sock, mode, bufsize)
diff --git a/sleekxmpp/xmlstream/handler/__init__.py b/sleekxmpp/xmlstream/handler/__init__.py
new file mode 100644
index 0000000..7bcf0b7
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/__init__.py
@@ -0,0 +1,14 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.handler.waiter import Waiter
+from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback
+from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter
+
+__all__ = ['Callback', 'Waiter', 'XMLCallback', 'XMLWaiter']
diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py
new file mode 100644
index 0000000..9c704ec
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/base.py
@@ -0,0 +1,89 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+class BaseHandler(object):
+
+ """
+ Base class for stream handlers. Stream handlers are matched with
+ incoming stanzas so that the stanza may be processed in some way.
+ Stanzas may be matched with multiple handlers.
+
+ Handler execution may take place in two phases. The first is during
+ the stream processing itself. The second is after stream processing
+ and during SleekXMPP's main event loop. The prerun method is used
+ for execution during stream processing, and the run method is used
+ during the main event loop.
+
+ Attributes:
+ name -- The name of the handler.
+ stream -- The stream this handler is assigned to.
+
+ Methods:
+ match -- Compare a stanza with the handler's matcher.
+ prerun -- Handler execution during stream processing.
+ run -- Handler execution during the main event loop.
+ check_delete -- Indicate if the handler may be removed from use.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ """
+ Create a new stream handler.
+
+ Arguments:
+ name -- The name of the handler.
+ matcher -- A matcher object from xmlstream.matcher that will be
+ used to determine if a stanza should be accepted by
+ this handler.
+ stream -- The XMLStream instance the handler should monitor.
+ """
+ self.checkDelete = self.check_delete
+
+ self.name = name
+ self.stream = stream
+ self._destroy = False
+ self._payload = None
+ self._matcher = matcher
+ if stream is not None:
+ stream.registerHandler(self)
+
+ def match(self, xml):
+ """
+ Compare a stanza or XML object with the handler's matcher.
+
+ Arguments
+ xml -- An XML or stanza object.
+ """
+ return self._matcher.match(xml)
+
+ def prerun(self, payload):
+ """
+ Prepare the handler for execution while the XML stream is being
+ processed.
+
+ Arguments:
+ payload -- A stanza object.
+ """
+ self._payload = payload
+
+ def run(self, payload):
+ """
+ Execute the handler after XML stream processing and during the
+ main event loop.
+
+ Arguments:
+ payload -- A stanza object.
+ """
+ self._payload = payload
+
+ def check_delete(self):
+ """
+ Check if the handler should be removed from the list of stream
+ handlers.
+ """
+ return self._destroy
diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py
new file mode 100644
index 0000000..f0a7285
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/callback.py
@@ -0,0 +1,84 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.handler.base import BaseHandler
+
+
+class Callback(BaseHandler):
+
+ """
+ The Callback handler will execute a callback function with
+ matched stanzas.
+
+ The handler may execute the callback either during stream
+ processing or during the main event loop.
+
+ Callback functions are all executed in the same thread, so be
+ aware if you are executing functions that will block for extended
+ periods of time. Typically, you should signal your own events using the
+ SleekXMPP object's event() method to pass the stanza off to a threaded
+ event handler for further processing.
+
+ Methods:
+ prerun -- Overrides BaseHandler.prerun
+ run -- Overrides BaseHandler.run
+ """
+
+ def __init__(self, name, matcher, pointer, thread=False,
+ once=False, instream=False, stream=None):
+ """
+ Create a new callback handler.
+
+ Arguments:
+ name -- The name of the handler.
+ matcher -- A matcher object for matching stanza objects.
+ pointer -- The function to execute during callback.
+ thread -- DEPRECATED. Remains only for backwards compatibility.
+ once -- Indicates if the handler should be used only
+ once. Defaults to False.
+ instream -- Indicates if the callback should be executed
+ during stream processing instead of in the
+ main event loop.
+ stream -- The XMLStream instance this handler should monitor.
+ """
+ BaseHandler.__init__(self, name, matcher, stream)
+ self._pointer = pointer
+ self._once = once
+ self._instream = instream
+
+ def prerun(self, payload):
+ """
+ Execute the callback during stream processing, if
+ the callback was created with instream=True.
+
+ Overrides BaseHandler.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ BaseHandler.prerun(self, payload)
+ if self._instream:
+ self.run(payload, True)
+
+ def run(self, payload, instream=False):
+ """
+ Execute the callback function with the matched stanza payload.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ instream -- Force the handler to execute during
+ stream processing. Used only by prerun.
+ Defaults to False.
+ """
+ if not self._instream or instream:
+ BaseHandler.run(self, payload)
+ 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..8072022
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/waiter.py
@@ -0,0 +1,98 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+from sleekxmpp.xmlstream import StanzaBase, RESPONSE_TIMEOUT
+from sleekxmpp.xmlstream.handler.base import BaseHandler
+
+
+class Waiter(BaseHandler):
+
+ """
+ The Waiter handler allows an event handler to block
+ until a particular stanza has been received. The handler
+ will either be given the matched stanza, or False if the
+ waiter has timed out.
+
+ Methods:
+ check_delete -- Overrides BaseHandler.check_delete
+ prerun -- Overrides BaseHandler.prerun
+ run -- Overrides BaseHandler.run
+ wait -- Wait for a stanza to arrive and return it to
+ an event handler.
+ """
+
+ def __init__(self, name, matcher, stream=None):
+ """
+ Create a new Waiter.
+
+ Arguments:
+ name -- The name of the waiter.
+ matcher -- A matcher object to detect the desired stanza.
+ stream -- Optional XMLStream instance to monitor.
+ """
+ BaseHandler.__init__(self, name, matcher, stream=stream)
+ self._payload = queue.Queue()
+
+ def prerun(self, payload):
+ """
+ Store the matched stanza.
+
+ Overrides BaseHandler.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ self._payload.put(payload)
+
+ def run(self, payload):
+ """
+ Do not process this handler during the main event loop.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ pass
+
+ def wait(self, timeout=RESPONSE_TIMEOUT):
+ """
+ Block an event handler while waiting for a stanza to arrive.
+
+ Be aware that this will impact performance if called from a
+ non-threaded event handler.
+
+ Will return either the received stanza, or False if the waiter
+ timed out.
+
+ Arguments:
+ timeout -- The number of seconds to wait for the stanza to
+ arrive. Defaults to the global default timeout
+ value sleekxmpp.xmlstream.RESPONSE_TIMEOUT.
+ """
+ try:
+ stanza = self._payload.get(True, timeout)
+ except queue.Empty:
+ stanza = False
+ logging.warning("Timed out waiting for %s" % self.name)
+ self.stream.removeHandler(self.name)
+ return stanza
+
+ def check_delete(self):
+ """
+ Always remove waiters after use.
+
+ Overrides BaseHandler.check_delete
+ """
+ return True
diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py
new file mode 100644
index 0000000..11607ff
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/xmlcallback.py
@@ -0,0 +1,36 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.handler import Callback
+
+
+class XMLCallback(Callback):
+
+ """
+ The XMLCallback class is identical to the normal Callback class,
+ except that XML contents of matched stanzas will be processed instead
+ of the stanza objects themselves.
+
+ Methods:
+ run -- Overrides Callback.run
+ """
+
+ def run(self, payload, instream=False):
+ """
+ Execute the callback function with the matched stanza's
+ XML contents, instead of the stanza itself.
+
+ Overrides BaseHandler.run
+
+ Arguments:
+ payload -- The matched stanza object.
+ instream -- Force the handler to execute during
+ stream processing. Used only by prerun.
+ Defaults to False.
+ """
+ Callback.run(self, payload.xml, instream)
diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py
new file mode 100644
index 0000000..5201caf
--- /dev/null
+++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py
@@ -0,0 +1,33 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.handler import Waiter
+
+
+class XMLWaiter(Waiter):
+
+ """
+ The XMLWaiter class is identical to the normal Waiter class
+ except that it returns the XML contents of the stanza instead
+ of the full stanza object itself.
+
+ Methods:
+ prerun -- Overrides Waiter.prerun
+ """
+
+ def prerun(self, payload):
+ """
+ Store the XML contents of the stanza to return to the
+ waiting event handler.
+
+ Overrides Waiter.prerun
+
+ Arguments:
+ payload -- The matched stanza object.
+ """
+ Waiter.prerun(self, payload.xml)
diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py
new file mode 100644
index 0000000..33d845a
--- /dev/null
+++ b/sleekxmpp/xmlstream/jid.py
@@ -0,0 +1,123 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+class JID(object):
+ """
+ A representation of a Jabber ID, or JID.
+
+ Each JID may have three components: a user, a domain, and an optional
+ resource. For example: user@domain/resource
+
+ When a resource is not used, the JID is called a bare JID.
+ The JID is a full JID otherwise.
+
+ Attributes:
+ jid -- Alias for 'full'.
+ full -- The value of the full JID.
+ bare -- The value of the bare JID.
+ user -- The username portion of the JID.
+ domain -- The domain name portion of the JID.
+ server -- Alias for 'domain'.
+ resource -- The resource portion of the JID.
+
+ Methods:
+ reset -- Use a new JID value.
+ regenerate -- Recreate the JID from its components.
+ """
+
+ def __init__(self, jid):
+ """Initialize a new JID"""
+ self.reset(jid)
+
+ def reset(self, jid):
+ """
+ Start fresh from a new JID string.
+
+ Arguments:
+ jid - The new JID value.
+ """
+ self._full = self._jid = str(jid)
+ self._domain = None
+ self._resource = None
+ self._user = None
+ self._bare = None
+
+ def __getattr__(self, name):
+ """
+ Handle getting the JID values, using cache if available.
+
+ Arguments:
+ name -- One of: user, server, domain, resource,
+ full, or bare.
+ """
+ if name == 'resource':
+ if self._resource is None and '/' in self._jid:
+ self._resource = self._jid.split('/', 1)[-1]
+ return self._resource or ""
+ elif name == 'user':
+ if self._user is None:
+ if '@' in self._jid:
+ self._user = self._jid.split('@', 1)[0]
+ else:
+ self._user = self._user
+ return self._user or ""
+ elif name in ('server', 'domain', 'host'):
+ if self._domain is None:
+ self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0]
+ return self._domain or ""
+ elif name == 'full':
+ return self._jid or ""
+ elif name == 'bare':
+ if self._bare is None:
+ self._bare = self._jid.split('/', 1)[0]
+ return self._bare or ""
+
+ def __setattr__(self, name, value):
+ """
+ Edit a JID by updating it's individual values, resetting the
+ generated JID in the end.
+
+ Arguments:
+ name -- The name of the JID part. One of: user, domain,
+ server, resource, full, jid, or bare.
+ value -- The new value for the JID part.
+ """
+ if name in ('resource', 'user', 'domain'):
+ object.__setattr__(self, "_%s" % name, value)
+ self.regenerate()
+ elif name in ('server', 'domain', 'host'):
+ self.domain = value
+ elif name in ('full', 'jid'):
+ self.reset(value)
+ self.regenerate()
+ elif name == 'bare':
+ if '@' in value:
+ u, d = value.split('@', 1)
+ object.__setattr__(self, "_user", u)
+ object.__setattr__(self, "_domain", d)
+ else:
+ object.__setattr__(self, "_user", '')
+ object.__setattr__(self, "_domain", value)
+ self.regenerate()
+ else:
+ object.__setattr__(self, name, value)
+
+ def regenerate(self):
+ """Generate a new JID based on current values, useful after editing."""
+ jid = ""
+ if self.user:
+ jid = "%s@" % self.user
+ jid += self.domain
+ if self.resource:
+ jid += "/%s" % self.resource
+ self.reset(jid)
+
+ def __str__(self):
+ """Use the full JID as the string value."""
+ return self.full
diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py
new file mode 100644
index 0000000..1038d1b
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.matcher.id import MatcherId
+from sleekxmpp.xmlstream.matcher.many import MatchMany
+from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
+from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+
+__all__ = ['MatcherId', 'MatchMany', 'StanzaPath',
+ 'MatchXMLMask', 'MatchXPath']
diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py
new file mode 100644
index 0000000..701ab32
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/base.py
@@ -0,0 +1,34 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+class MatcherBase(object):
+
+ """
+ Base class for stanza matchers. Stanza matchers are used to pick
+ stanzas out of the XML stream and pass them to the appropriate
+ stream handlers.
+ """
+
+ def __init__(self, criteria):
+ """
+ Create a new stanza matcher.
+
+ Arguments:
+ criteria -- Object to compare some aspect of a stanza
+ against.
+ """
+ self._criteria = criteria
+
+ def match(self, xml):
+ """
+ Check if a stanza matches the stored criteria.
+
+ Meant to be overridden.
+ """
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py
new file mode 100644
index 0000000..0c8ce2d
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/id.py
@@ -0,0 +1,32 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatcherId(MatcherBase):
+
+ """
+ The ID matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Compare the given stanza's 'id' attribute to the stored
+ id value.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza to compare against.
+ """
+ return xml['id'] == self._criteria
diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py
new file mode 100644
index 0000000..f470ec9
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/many.py
@@ -0,0 +1,40 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatchMany(MatcherBase):
+
+ """
+ The MatchMany matcher may compare a stanza against multiple
+ criteria. It is essentially an OR relation combining multiple
+ matchers.
+
+ Each of the criteria must implement a match() method.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Match a stanza against multiple criteria. The match is successful
+ if one of the criteria matches.
+
+ Each of the criteria must implement a match() method.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ for m in self._criteria:
+ if m.match(xml):
+ return True
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py
new file mode 100644
index 0000000..f8ff283
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/stanzapath.py
@@ -0,0 +1,38 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class StanzaPath(MatcherBase):
+
+ """
+ The StanzaPath matcher selects stanzas that match a given "stanza path",
+ which is similar to a normal XPath except that it uses the interfaces and
+ plugins of the stanza instead of the actual, underlying XML.
+
+ In most cases, the stanza path and XPath should be identical, but be
+ aware that differences may occur.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, stanza):
+ """
+ Compare a stanza against a "stanza path". A stanza path is similar to
+ an XPath expression, but uses the stanza's interfaces and plugins
+ instead of the underlying XML. For most cases, the stanza path and
+ XPath should be identical, but be aware that differences may occur.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ stanza -- The stanza object to compare against.
+ """
+ return stanza.match(self._criteria)
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
new file mode 100644
index 0000000..2967a2a
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -0,0 +1,155 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from xml.parsers.expat import ExpatError
+
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XMLMask matchers.
+IGNORE_NS = False
+
+
+class MatchXMLMask(MatcherBase):
+
+ """
+ The XMLMask matcher selects stanzas whose XML matches a given
+ XML pattern, or mask. For example, message stanzas with body elements
+ could be matched using the mask:
+
+ <message xmlns="jabber:client"><body /></message>
+
+ Use of XMLMask is discouraged, and XPath or StanzaPath should be used
+ instead.
+
+ The use of namespaces in the mask comparison is controlled by
+ IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching
+ for ALL XMLMask matchers.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ setDefaultNS -- Set the default namespace for the mask.
+ """
+
+ def __init__(self, criteria):
+ """
+ Create a new XMLMask matcher.
+
+ Arguments:
+ criteria -- Either an XML object or XML string to use as a mask.
+ """
+ MatcherBase.__init__(self, criteria)
+ if isinstance(criteria, str):
+ self._criteria = ET.fromstring(self._criteria)
+ self.default_ns = 'jabber:client'
+
+ def setDefaultNS(self, ns):
+ """
+ Set the default namespace to use during comparisons.
+
+ Arguments:
+ ns -- The new namespace to use as the default.
+ """
+ self.default_ns = ns
+
+ def match(self, xml):
+ """
+ Compare a stanza object or XML object against the stored XML mask.
+
+ Overrides MatcherBase.match.
+
+ Arguments:
+ xml -- The stanza object or XML object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ return self._mask_cmp(xml, self._criteria, True)
+
+ def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'):
+ """
+ Compare an XML object against an XML mask.
+
+ Arguments:
+ source -- The XML object to compare against the mask.
+ mask -- The XML object serving as the mask.
+ use_ns -- Indicates if namespaces should be respected during
+ the comparison.
+ default_ns -- The default namespace to apply to elements that
+ do not have a specified namespace.
+ Defaults to "__no_ns__".
+ """
+ use_ns = not IGNORE_NS
+
+ if source is None:
+ # If the element was not found. May happend during recursive calls.
+ return False
+
+ # Convert the mask to an XML object if it is a string.
+ if not hasattr(mask, 'attrib'):
+ try:
+ mask = ET.fromstring(mask)
+ except ExpatError:
+ logging.log(logging.WARNING,
+ "Expat error: %s\nIn parsing: %s" % ('', mask))
+
+ if not use_ns:
+ # Compare the element without using namespaces.
+ source_tag = source.tag.split('}', 1)[-1]
+ mask_tag = mask.tag.split('}', 1)[-1]
+ if source_tag != mask_tag:
+ return False
+ else:
+ # Compare the element using namespaces
+ mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag)
+ if source.tag not in [mask.tag, mask_ns_tag]:
+ return False
+
+ # If the mask includes text, compare it.
+ if mask.text and source.text != mask.text:
+ return False
+
+ # Compare attributes. The stanza must include the attributes
+ # defined by the mask, but may include others.
+ for name, value in mask.attrib.items():
+ if source.attrib.get(name, "__None__") != value:
+ return False
+
+ # Recursively check subelements.
+ for subelement in mask:
+ if use_ns:
+ if not self._mask_cmp(source.find(subelement.tag),
+ subelement, use_ns):
+ return False
+ else:
+ if not self._mask_cmp(self._get_child(source, subelement.tag),
+ subelement, use_ns):
+ return False
+
+ # Everything matches.
+ return True
+
+ def _get_child(self, xml, tag):
+ """
+ Return a child element given its tag, ignoring namespace values.
+
+ Returns None if the child was not found.
+
+ Arguments:
+ xml -- The XML object to search for the given child tag.
+ tag -- The name of the subelement to find.
+ """
+ tag = tag.split('}')[-1]
+ try:
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ index = children.index(tag)
+ except ValueError:
+ return None
+ return xml.getchildren()[index]
diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py
new file mode 100644
index 0000000..669c9f1
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/xpath.py
@@ -0,0 +1,79 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+# Flag indicating if the builtin XPath matcher should be used, which
+# uses namespaces, or a custom matcher that ignores namespaces.
+# Changing this will affect ALL XPath matchers.
+IGNORE_NS = False
+
+
+class MatchXPath(MatcherBase):
+
+ """
+ The XPath matcher selects stanzas whose XML contents matches a given
+ XPath expression.
+
+ Note that using this matcher may not produce expected behavior when using
+ attribute selectors. For Python 2.6 and 3.1, the ElementTree find method
+ does not support the use of attribute selectors. If you need to support
+ Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher.
+
+ If the value of IGNORE_NS is set to true, then XPath expressions will
+ be matched without using namespaces.
+
+ Methods:
+ match -- Overrides MatcherBase.match.
+ """
+
+ def match(self, xml):
+ """
+ Compare a stanza's XML contents to an XPath expression.
+
+ If the value of IGNORE_NS is set to true, then XPath expressions
+ will be matched without using namespaces.
+
+ Note that in Python 2.6 and 3.1 the ElementTree find method does
+ not support attribute selectors in the XPath expression.
+
+ Arguments:
+ xml -- The stanza object to compare against.
+ """
+ if hasattr(xml, 'xml'):
+ xml = xml.xml
+ x = ET.Element('x')
+ x.append(xml)
+
+ if not IGNORE_NS:
+ # Use builtin, namespace respecting, XPath matcher.
+ if x.find(self._criteria) is not None:
+ return True
+ return False
+ else:
+ # Remove namespaces from the XPath expression.
+ criteria = []
+ for ns_block in self._criteria.split('{'):
+ criteria.extend(ns_block.split('}')[-1].split('/'))
+
+ # Walk the XPath expression.
+ xml = x
+ for tag in criteria:
+ if not tag:
+ # Skip empty tag name artifacts from the cleanup phase.
+ continue
+
+ children = [c.tag.split('}')[-1] for c in xml.getchildren()]
+ try:
+ index = children.index(tag)
+ except ValueError:
+ return False
+ xml = xml.getchildren()[index]
+ return True
diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py
new file mode 100644
index 0000000..240d4a4
--- /dev/null
+++ b/sleekxmpp/xmlstream/scheduler.py
@@ -0,0 +1,202 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import time
+import threading
+import logging
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+
+class Task(object):
+
+ """
+ A scheduled task that will be executed by the scheduler
+ after a given time interval has passed.
+
+ Attributes:
+ name -- The name of the task.
+ seconds -- The number of seconds to wait before executing.
+ callback -- The function to execute.
+ args -- The arguments to pass to the callback.
+ kwargs -- The keyword arguments to pass to the callback.
+ repeat -- Indicates if the task should repeat.
+ Defaults to False.
+ qpointer -- A pointer to an event queue for queuing callback
+ execution instead of executing immediately.
+
+ Methods:
+ run -- Either queue or execute the callback.
+ reset -- Reset the task's timer.
+ """
+
+ def __init__(self, name, seconds, callback, args=None,
+ kwargs=None, repeat=False, qpointer=None):
+ """
+ Create a new task.
+
+ Arguments:
+ name -- The name of the task.
+ seconds -- The number of seconds to wait before executing.
+ callback -- The function to execute.
+ args -- The arguments to pass to the callback.
+ kwargs -- The keyword arguments to pass to the callback.
+ repeat -- Indicates if the task should repeat.
+ Defaults to False.
+ qpointer -- A pointer to an event queue for queuing callback
+ execution instead of executing immediately.
+ """
+ self.name = name
+ self.seconds = seconds
+ self.callback = callback
+ self.args = args or tuple()
+ self.kwargs = kwargs or {}
+ self.repeat = repeat
+ self.next = time.time() + self.seconds
+ self.qpointer = qpointer
+
+ def run(self):
+ """
+ Execute the task's callback.
+
+ If an event queue was supplied, place the callback in the queue;
+ otherwise, execute the callback immediately.
+ """
+ if self.qpointer is not None:
+ self.qpointer.put(('schedule', self.callback, self.args))
+ else:
+ self.callback(*self.args, **self.kwargs)
+ self.reset()
+ return self.repeat
+
+ def reset(self):
+ """
+ Reset the task's timer so that it will repeat.
+ """
+ self.next = time.time() + self.seconds
+
+
+class Scheduler(object):
+
+ """
+ A threaded scheduler that allows for updates mid-execution unlike the
+ scheduler in the standard library.
+
+ http://docs.python.org/library/sched.html#module-sched
+
+ Attributes:
+ addq -- A queue storing added tasks.
+ schedule -- A list of tasks in order of execution times.
+ thread -- If threaded, the thread processing the schedule.
+ run -- Indicates if the scheduler is running.
+ parentqueue -- A parent event queue in control of this scheduler.
+
+ Methods:
+ add -- Add a new task to the schedule.
+ process -- Process and schedule tasks.
+ quit -- Stop the scheduler.
+ """
+
+ def __init__(self, parentqueue=None, parentstop=None):
+ """
+ Create a new scheduler.
+
+ Arguments:
+ parentqueue -- A separate event queue controlling this scheduler.
+ """
+ self.addq = queue.Queue()
+ self.schedule = []
+ self.thread = None
+ self.run = False
+ self.parentqueue = parentqueue
+ self.parentstop = parentstop
+
+ def process(self, threaded=True):
+ """
+ Begin accepting and processing scheduled tasks.
+
+ Arguments:
+ threaded -- Indicates if the scheduler should execute in its own
+ thread. Defaults to True.
+ """
+ if threaded:
+ self.thread = threading.Thread(name='sheduler_process',
+ target=self._process)
+ self.thread.start()
+ else:
+ self._process()
+
+ def _process(self):
+ """Process scheduled tasks."""
+ self.run = True
+ try:
+ while self.run and (self.parentstop is None or not self.parentstop.isSet()):
+ wait = 1
+ updated = False
+ if self.schedule:
+ wait = self.schedule[0].next - time.time()
+ try:
+ if wait <= 0.0:
+ newtask = self.addq.get(False)
+ else:
+ newtask = self.addq.get(True, wait)
+ except queue.Empty:
+ cleanup = []
+ for task in self.schedule:
+ if time.time() >= task.next:
+ updated = True
+ if not task.run():
+ cleanup.append(task)
+ else:
+ break
+ for task in cleanup:
+ x = self.schedule.pop(self.schedule.index(task))
+ else:
+ updated = True
+ self.schedule.append(newtask)
+ finally:
+ if updated:
+ self.schedule = sorted(self.schedule,
+ key=lambda task: task.next)
+ except KeyboardInterrupt:
+ self.run = False
+ if self.parentstop is not None:
+ logging.debug("stopping parent")
+ self.parentstop.set()
+ except SystemExit:
+ self.run = False
+ if self.parentstop is not None:
+ self.parentstop.set()
+ logging.debug("Quitting Scheduler thread")
+ if self.parentqueue is not None:
+ self.parentqueue.put(('quit', None, None))
+
+ def add(self, name, seconds, callback, args=None,
+ kwargs=None, repeat=False, qpointer=None):
+ """
+ Schedule a new task.
+
+ Arguments:
+ name -- The name of the task.
+ seconds -- The number of seconds to wait before executing.
+ callback -- The function to execute.
+ args -- The arguments to pass to the callback.
+ kwargs -- The keyword arguments to pass to the callback.
+ repeat -- Indicates if the task should repeat.
+ Defaults to False.
+ qpointer -- A pointer to an event queue for queuing callback
+ execution instead of executing immediately.
+ """
+ self.addq.put(Task(name, seconds, callback, args,
+ kwargs, repeat, qpointer))
+
+ def quit(self):
+ """Shutdown the scheduler."""
+ self.run = False
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
new file mode 100644
index 0000000..f4d66aa
--- /dev/null
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -0,0 +1,1162 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import copy
+import logging
+import sys
+import weakref
+from xml.etree import cElementTree as ET
+
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.xmlstream.tostring import tostring
+
+
+# Used to check if an argument is an XML object.
+XML_TYPE = type(ET.Element('xml'))
+
+
+def register_stanza_plugin(stanza, plugin):
+ """
+ Associate a stanza object as a plugin for another stanza.
+
+ Arguments:
+ stanza -- The class of the parent stanza.
+ plugin -- The class of the plugin stanza.
+ """
+ tag = "{%s}%s" % (plugin.namespace, plugin.name)
+ stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
+ stanza.plugin_tag_map[tag] = plugin
+
+
+# To maintain backwards compatibility for now, preserve the camel case name.
+registerStanzaPlugin = register_stanza_plugin
+
+
+class ElementBase(object):
+
+ """
+ The core of SleekXMPP's stanza XML manipulation and handling is provided
+ by ElementBase. ElementBase wraps XML cElementTree objects and enables
+ access to the XML contents through dictionary syntax, similar in style
+ to the Ruby XMPP library Blather's stanza implementation.
+
+ Stanzas are defined by their name, namespace, and interfaces. For
+ example, a simplistic Message stanza could be defined as:
+
+ >>> class Message(ElementBase):
+ ... name = "message"
+ ... namespace = "jabber:client"
+ ... interfaces = set(('to', 'from', 'type', 'body'))
+ ... sub_interfaces = set(('body',))
+
+ The resulting Message stanza's contents may be accessed as so:
+
+ >>> message['to'] = "user@example.com"
+ >>> message['body'] = "Hi!"
+ >>> message['body']
+ "Hi!"
+ >>> del message['body']
+ >>> message['body']
+ ""
+
+ The interface values map to either custom access methods, stanza
+ XML attributes, or (if the interface is also in sub_interfaces) the
+ text contents of a stanza's subelement.
+
+ Custom access methods may be created by adding methods of the
+ form "getInterface", "setInterface", or "delInterface", where
+ "Interface" is the titlecase version of the interface name.
+
+ Stanzas may be extended through the use of plugins. A plugin
+ is simply a stanza that has a plugin_attrib value. For example:
+
+ >>> class MessagePlugin(ElementBase):
+ ... name = "custom_plugin"
+ ... namespace = "custom"
+ ... interfaces = set(('useful_thing', 'custom'))
+ ... plugin_attrib = "custom"
+
+ The plugin stanza class must be associated with its intended
+ container stanza by using register_stanza_plugin as so:
+
+ >>> register_stanza_plugin(Message, MessagePlugin)
+
+ The plugin may then be accessed as if it were built-in to the parent
+ stanza.
+
+ >>> message['custom']['useful_thing'] = 'foo'
+
+ If a plugin provides an interface that is the same as the plugin's
+ plugin_attrib value, then the plugin's interface may be accessed
+ directly from the parent stanza, as so:
+
+ >>> message['custom'] = 'bar' # Same as using message['custom']['custom']
+
+ Class Attributes:
+ name -- The name of the stanza's main element.
+ namespace -- The namespace of the stanza's main element.
+ interfaces -- A set of attribute and element names that may
+ be accessed using dictionary syntax.
+ sub_interfaces -- A subset of the set of interfaces which map
+ to subelements instead of attributes.
+ subitem -- A set of stanza classes which are allowed to
+ be added as substanzas.
+ types -- A set of generic type attribute values.
+ plugin_attrib -- The interface name that the stanza uses to be
+ accessed as a plugin from another stanza.
+ plugin_attrib_map -- A mapping of plugin attribute names with the
+ associated plugin stanza classes.
+ plugin_tag_map -- A mapping of plugin stanza tag names with
+ the associated plugin stanza classes.
+
+ Instance Attributes:
+ xml -- The stanza's XML contents.
+ parent -- The parent stanza of this stanza.
+ plugins -- A map of enabled plugin names with the
+ initialized plugin stanza objects.
+ values -- A dictionary of the stanza's interfaces
+ and interface values, including plugins.
+
+ Methods:
+ setup -- Initialize the stanza's XML contents.
+ enable -- Instantiate a stanza plugin.
+ Alias for init_plugin.
+ init_plugin -- Instantiate a stanza plugin.
+ _get_stanza_values -- Return a dictionary of stanza interfaces and
+ their values.
+ _set_stanza_values -- Set stanza interface values given a dictionary
+ of interfaces and values.
+ __getitem__ -- Return the value of a stanza interface.
+ __setitem__ -- Set the value of a stanza interface.
+ __delitem__ -- Remove the value of a stanza interface.
+ _set_attr -- Set an attribute value of the main
+ stanza element.
+ _del_attr -- Remove an attribute from the main
+ stanza element.
+ _get_attr -- Return an attribute's value from the main
+ stanza element.
+ _get_sub_text -- Return the text contents of a subelement.
+ _set_sub_ext -- Set the text contents of a subelement.
+ _del_sub -- Remove a subelement.
+ match -- Compare the stanza against an XPath expression.
+ find -- Return subelement matching an XPath expression.
+ findall -- Return subelements matching an XPath expression.
+ get -- Return the value of a stanza interface, with an
+ optional default value.
+ keys -- Return the set of interface names accepted by
+ the stanza.
+ append -- Add XML content or a substanza to the stanza.
+ appendxml -- Add XML content to the stanza.
+ pop -- Remove a substanza.
+ next -- Return the next iterable substanza.
+ _fix_ns -- Apply the stanza's namespace to non-namespaced
+ elements in an XPath expression.
+ """
+
+ name = 'stanza'
+ plugin_attrib = 'plugin'
+ namespace = 'jabber:client'
+ interfaces = set(('type', 'to', 'from', 'id', 'payload'))
+ types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat'))
+ sub_interfaces = tuple()
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = None
+
+ def __init__(self, xml=None, parent=None):
+ """
+ Create a new stanza object.
+
+ Arguments:
+ xml -- Initialize the stanza with optional existing XML.
+ parent -- Optional stanza object that contains this stanza.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.initPlugin = self.init_plugin
+ self._getAttr = self._get_attr
+ self._setAttr = self._set_attr
+ self._delAttr = self._del_attr
+ self._getSubText = self._get_sub_text
+ self._setSubText = self._set_sub_text
+ self._delSub = self._del_sub
+ self.getStanzaValues = self._get_stanza_values
+ self.setStanzaValues = self._set_stanza_values
+
+ self.xml = xml
+ self.plugins = {}
+ self.iterables = []
+ self._index = 0
+ if parent is None:
+ self.parent = None
+ else:
+ self.parent = weakref.ref(parent)
+
+ ElementBase.values = property(ElementBase._get_stanza_values,
+ ElementBase._set_stanza_values)
+
+ if self.setup(xml):
+ # If we generated our own XML, then everything is ready.
+ return
+
+ # Initialize values using provided XML
+ for child in self.xml.getchildren():
+ if child.tag in self.plugin_tag_map:
+ plugin = self.plugin_tag_map[child.tag]
+ self.plugins[plugin.plugin_attrib] = plugin(child, self)
+ if self.subitem is not None:
+ for sub in self.subitem:
+ if child.tag == "{%s}%s" % (sub.namespace, sub.name):
+ self.iterables.append(sub(child, self))
+ break
+
+ def setup(self, xml=None):
+ """
+ Initialize the stanza's XML contents.
+
+ Will return True if XML was generated according to the stanza's
+ definition.
+
+ Arguments:
+ xml -- Optional XML object to use for the stanza's content
+ instead of generating XML.
+ """
+ if self.xml is None:
+ self.xml = xml
+
+ if self.xml is None:
+ # Generate XML from the stanza definition
+ for ename in self.name.split('/'):
+ new = ET.Element("{%s}%s" % (self.namespace, ename))
+ if self.xml is None:
+ self.xml = new
+ else:
+ last_xml.append(new)
+ last_xml = new
+ if self.parent is not None:
+ self.parent().xml.append(self.xml)
+
+ # We had to generate XML
+ return True
+ else:
+ # We did not generate XML
+ return False
+
+ def enable(self, attrib):
+ """
+ Enable and initialize a stanza plugin.
+
+ Alias for init_plugin.
+
+ Arguments:
+ attrib -- The stanza interface for the plugin.
+ """
+ return self.init_plugin(attrib)
+
+ def init_plugin(self, attrib):
+ """
+ Enable and initialize a stanza plugin.
+
+ Arguments:
+ attrib -- The stanza interface for the plugin.
+ """
+ if attrib not in self.plugins:
+ plugin_class = self.plugin_attrib_map[attrib]
+ self.plugins[attrib] = plugin_class(parent=self)
+ return self
+
+ def _get_stanza_values(self):
+ """
+ Return a dictionary of the stanza's interface values.
+
+ Stanza plugin values are included as nested dictionaries.
+ """
+ values = {}
+ for interface in self.interfaces:
+ values[interface] = self[interface]
+ for plugin, stanza in self.plugins.items():
+ values[plugin] = stanza._get_stanza_values()
+ if self.iterables:
+ iterables = []
+ for stanza in self.iterables:
+ iterables.append(stanza._get_stanza_values())
+ iterables[-1].update({
+ '__childtag__': "{%s}%s" % (stanza.namespace,
+ stanza.name)})
+ values['substanzas'] = iterables
+ return values
+
+ def _set_stanza_values(self, values):
+ """
+ Set multiple stanza interface values using a dictionary.
+
+ Stanza plugin values may be set using nested dictionaries.
+
+ Arguments:
+ values -- A dictionary mapping stanza interface with values.
+ Plugin interfaces may accept a nested dictionary that
+ will be used recursively.
+ """
+ for interface, value in values.items():
+ if interface == 'substanzas':
+ for subdict in value:
+ if '__childtag__' in subdict:
+ for subclass in self.subitem:
+ child_tag = "{%s}%s" % (subclass.namespace,
+ subclass.name)
+ if subdict['__childtag__'] == child_tag:
+ sub = subclass(parent=self)
+ sub._set_stanza_values(subdict)
+ self.iterables.append(sub)
+ break
+ elif interface in self.interfaces:
+ self[interface] = value
+ elif interface in self.plugin_attrib_map:
+ if interface not in self.plugins:
+ self.init_plugin(interface)
+ self.plugins[interface]._set_stanza_values(value)
+ return self
+
+ def __getitem__(self, attrib):
+ """
+ Return the value of a stanza interface using dictionary-like syntax.
+
+ Example:
+ >>> msg['body']
+ 'Message contents'
+
+ Stanza interfaces are typically mapped directly to the underlying XML
+ object, but can be overridden by the presence of a get_attrib method
+ (or get_foo where the interface is named foo, etc).
+
+ The search order for interface value retrieval for an interface
+ named 'foo' is:
+ 1. The list of substanzas.
+ 2. The result of calling get_foo.
+ 3. The result of calling getFoo.
+ 4. The contents of the foo subelement, if foo is a sub interface.
+ 5. The value of the foo attribute of the XML object.
+ 6. The plugin named 'foo'
+ 7. An empty string.
+
+ Arguments:
+ attrib -- The name of the requested stanza interface.
+ """
+ if attrib == 'substanzas':
+ return self.iterables
+ elif attrib in self.interfaces:
+ get_method = "get_%s" % attrib.lower()
+ get_method2 = "get%s" % attrib.title()
+ if hasattr(self, get_method):
+ return getattr(self, get_method)()
+ elif hasattr(self, get_method2):
+ return getattr(self, get_method2)()
+ else:
+ if attrib in self.sub_interfaces:
+ return self._get_sub_text(attrib)
+ else:
+ return self._get_attr(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib not in self.plugins:
+ self.init_plugin(attrib)
+ return self.plugins[attrib]
+ else:
+ return ''
+
+ def __setitem__(self, attrib, value):
+ """
+ Set the value of a stanza interface using dictionary-like syntax.
+
+ Example:
+ >>> msg['body'] = "Hi!"
+ >>> msg['body']
+ 'Hi!'
+
+ Stanza interfaces are typically mapped directly to the underlying XML
+ object, but can be overridden by the presence of a set_attrib method
+ (or set_foo where the interface is named foo, etc).
+
+ The effect of interface value assignment for an interface
+ named 'foo' will be one of:
+ 1. Delete the interface's contents if the value is None.
+ 2. Call set_foo, if it exists.
+ 3. Call setFoo, if it exists.
+ 4. Set the text of a foo element, if foo is in sub_interfaces.
+ 5. Set the value of a top level XML attribute name foo.
+ 6. Attempt to pass value to a plugin named foo using the plugin's
+ foo interface.
+ 7. Do nothing.
+
+ Arguments:
+ attrib -- The name of the stanza interface to modify.
+ value -- The new value of the stanza interface.
+ """
+ if attrib in self.interfaces:
+ if value is not None:
+ set_method = "set_%s" % attrib.lower()
+ set_method2 = "set%s" % attrib.title()
+ if hasattr(self, set_method):
+ getattr(self, set_method)(value,)
+ elif hasattr(self, set_method2):
+ getattr(self, set_method2)(value,)
+ else:
+ if attrib in self.sub_interfaces:
+ return self._set_sub_text(attrib, text=value)
+ else:
+ self._set_attr(attrib, value)
+ else:
+ self.__delitem__(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib not in self.plugins:
+ self.init_plugin(attrib)
+ self.plugins[attrib][attrib] = value
+ return self
+
+ def __delitem__(self, attrib):
+ """
+ Delete the value of a stanza interface using dictionary-like syntax.
+
+ Example:
+ >>> msg['body'] = "Hi!"
+ >>> msg['body']
+ 'Hi!'
+ >>> del msg['body']
+ >>> msg['body']
+ ''
+
+ Stanza interfaces are typically mapped directly to the underlyig XML
+ object, but can be overridden by the presence of a del_attrib method
+ (or del_foo where the interface is named foo, etc).
+
+ The effect of deleting a stanza interface value named foo will be
+ one of:
+ 1. Call del_foo, if it exists.
+ 2. Call delFoo, if it exists.
+ 3. Delete foo element, if foo is in sub_interfaces.
+ 4. Delete top level XML attribute named foo.
+ 5. Remove the foo plugin, if it was loaded.
+ 6. Do nothing.
+
+ Arguments:
+ attrib -- The name of the affected stanza interface.
+ """
+ if attrib in self.interfaces:
+ del_method = "del_%s" % attrib.lower()
+ del_method2 = "del%s" % attrib.title()
+ if hasattr(self, del_method):
+ getattr(self, del_method)()
+ elif hasattr(self, del_method2):
+ getattr(self, del_method2)()
+ else:
+ if attrib in self.sub_interfaces:
+ return self._del_sub(attrib)
+ else:
+ self._del_attr(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib in self.plugins:
+ xml = self.plugins[attrib].xml
+ del self.plugins[attrib]
+ self.xml.remove(xml)
+ return self
+
+ def _set_attr(self, name, value):
+ """
+ Set the value of a top level attribute of the underlying XML object.
+
+ If the new value is None or an empty string, then the attribute will
+ be removed.
+
+ Arguments:
+ name -- The name of the attribute.
+ value -- The new value of the attribute, or None or '' to
+ remove it.
+ """
+ if value is None or value == '':
+ self.__delitem__(name)
+ else:
+ self.xml.attrib[name] = value
+
+ def _del_attr(self, name):
+ """
+ Remove a top level attribute of the underlying XML object.
+
+ Arguments:
+ name -- The name of the attribute.
+ """
+ if name in self.xml.attrib:
+ del self.xml.attrib[name]
+
+ def _get_attr(self, name, default=''):
+ """
+ Return the value of a top level attribute of the underlying
+ XML object.
+
+ In case the attribute has not been set, a default value can be
+ returned instead. An empty string is returned if no other default
+ is supplied.
+
+ Arguments:
+ name -- The name of the attribute.
+ default -- Optional value to return if the attribute has not
+ been set. An empty string is returned otherwise.
+ """
+ return self.xml.attrib.get(name, default)
+
+ def _get_sub_text(self, name, default=''):
+ """
+ Return the text contents of a sub element.
+
+ In case the element does not exist, or it has no textual content,
+ a default value can be returned instead. An empty string is returned
+ if no other default is supplied.
+
+ Arguments:
+ name -- The name or XPath expression of the element.
+ default -- Optional default to return if the element does
+ not exists. An empty string is returned otherwise.
+ """
+ name = self._fix_ns(name)
+ stanza = self.xml.find(name)
+ if stanza is None or stanza.text is None:
+ return default
+ else:
+ return stanza.text
+
+ def _set_sub_text(self, name, text=None, keep=False):
+ """
+ Set the text contents of a sub element.
+
+ In case the element does not exist, a element will be created,
+ and its text contents will be set.
+
+ If the text is set to an empty string, or None, then the
+ element will be removed, unless keep is set to True.
+
+ Arguments:
+ name -- The name or XPath expression of the element.
+ text -- The new textual content of the element. If the text
+ is an empty string or None, the element will be removed
+ unless the parameter keep is True.
+ keep -- Indicates if the element should be kept if its text is
+ removed. Defaults to False.
+ """
+ path = self._fix_ns(name, split=True)
+ element = self.xml.find(name)
+
+ if not text and not keep:
+ return self._del_sub(name)
+
+ if element is None:
+ # We need to add the element. If the provided name was
+ # an XPath expression, some of the intermediate elements
+ # may already exist. If so, we want to use those instead
+ # of generating new elements.
+ last_xml = self.xml
+ walked = []
+ for ename in path:
+ walked.append(ename)
+ element = self.xml.find("/".join(walked))
+ if element is None:
+ element = ET.Element(ename)
+ last_xml.append(element)
+ last_xml = element
+ element = last_xml
+
+ element.text = text
+ return element
+
+ def _del_sub(self, name, all=False):
+ """
+ Remove sub elements that match the given name or XPath.
+
+ If the element is in a path, then any parent elements that become
+ empty after deleting the element may also be deleted if requested
+ by setting all=True.
+
+ Arguments:
+ name -- The name or XPath expression for the element(s) to remove.
+ all -- If True, remove all empty elements in the path to the
+ deleted element. Defaults to False.
+ """
+ path = self._fix_ns(name, split=True)
+ original_target = path[-1]
+
+ for level, _ in enumerate(path):
+ # Generate the paths to the target elements and their parent.
+ element_path = "/".join(path[:len(path) - level])
+ parent_path = "/".join(path[:len(path) - level - 1])
+
+ elements = self.xml.findall(element_path)
+ parent = self.xml.find(parent_path)
+
+ if elements:
+ if parent is None:
+ parent = self.xml
+ for element in elements:
+ if element.tag == original_target or \
+ not element.getchildren():
+ # Only delete the originally requested elements, and
+ # any parent elements that have become empty.
+ parent.remove(element)
+ if not all:
+ # If we don't want to delete elements up the tree, stop
+ # after deleting the first level of elements.
+ return
+
+ def match(self, xpath):
+ """
+ Compare a stanza object with an XPath expression. If the XPath matches
+ the contents of the stanza object, the match is successful.
+
+ The XPath expression may include checks for stanza attributes.
+ For example:
+ presence@show=xa@priority=2/status
+ Would match a presence stanza whose show value is set to 'xa', has a
+ priority value of '2', and has a status element.
+
+ Arguments:
+ xpath -- The XPath expression to check against. It may be either a
+ string or a list of element names with attribute checks.
+ """
+ if isinstance(xpath, str):
+ xpath = self._fix_ns(xpath, split=True, propagate_ns=False)
+
+ # Extract the tag name and attribute checks for the first XPath node.
+ components = xpath[0].split('@')
+ tag = components[0]
+ attributes = components[1:]
+
+ if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \
+ tag not in self.plugins and tag not in self.plugin_attrib:
+ # The requested tag is not in this stanza, so no match.
+ return False
+
+ # Check the rest of the XPath against any substanzas.
+ matched_substanzas = False
+ for substanza in self.iterables:
+ if xpath[1:] == []:
+ break
+ matched_substanzas = substanza.match(xpath[1:])
+ if matched_substanzas:
+ break
+
+ # Check attribute values.
+ for attribute in attributes:
+ name, value = attribute.split('=')
+ if self[name] != value:
+ return False
+
+ # Check sub interfaces.
+ if len(xpath) > 1:
+ next_tag = xpath[1]
+ if next_tag in self.sub_interfaces and self[next_tag]:
+ return True
+
+ # Attempt to continue matching the XPath using the stanza's plugins.
+ if not matched_substanzas and len(xpath) > 1:
+ # Convert {namespace}tag@attribs to just tag
+ next_tag = xpath[1].split('@')[0].split('}')[-1]
+ if next_tag in self.plugins:
+ return self.plugins[next_tag].match(xpath[1:])
+ else:
+ return False
+
+ # Everything matched.
+ return True
+
+ def find(self, xpath):
+ """
+ Find an XML object in this stanza given an XPath expression.
+
+ Exposes ElementTree interface for backwards compatibility.
+
+ Note that matching on attribute values is not supported in Python 2.6
+ or Python 3.1
+
+ Arguments:
+ xpath -- An XPath expression matching a single desired element.
+ """
+ return self.xml.find(xpath)
+
+ def findall(self, xpath):
+ """
+ Find multiple XML objects in this stanza given an XPath expression.
+
+ Exposes ElementTree interface for backwards compatibility.
+
+ Note that matching on attribute values is not supported in Python 2.6
+ or Python 3.1.
+
+ Arguments:
+ xpath -- An XPath expression matching multiple desired elements.
+ """
+ return self.xml.findall(xpath)
+
+ def get(self, key, default=None):
+ """
+ Return the value of a stanza interface. If the found value is None
+ or an empty string, return the supplied default value.
+
+ Allows stanza objects to be used like dictionaries.
+
+ Arguments:
+ key -- The name of the stanza interface to check.
+ default -- Value to return if the stanza interface has a value
+ of None or "". Will default to returning None.
+ """
+ value = self[key]
+ if value is None or value == '':
+ return default
+ return value
+
+ def keys(self):
+ """
+ Return the names of all stanza interfaces provided by the
+ stanza object.
+
+ Allows stanza objects to be used like dictionaries.
+ """
+ out = []
+ out += [x for x in self.interfaces]
+ out += [x for x in self.plugins]
+ if self.iterables:
+ out.append('substanzas')
+ return out
+
+ def append(self, item):
+ """
+ Append either an XML object or a substanza to this stanza object.
+
+ If a substanza object is appended, it will be added to the list
+ of iterable stanzas.
+
+ Allows stanza objects to be used like lists.
+
+ Arguments:
+ item -- Either an XML object or a stanza object to add to
+ this stanza's contents.
+ """
+ if not isinstance(item, ElementBase):
+ if type(item) == XML_TYPE:
+ return self.appendxml(item)
+ else:
+ raise TypeError
+ self.xml.append(item.xml)
+ self.iterables.append(item)
+ return self
+
+ def appendxml(self, xml):
+ """
+ Append an XML object to the stanza's XML.
+
+ The added XML will not be included in the list of
+ iterable substanzas.
+
+ Arguments:
+ xml -- The XML object to add to the stanza.
+ """
+ self.xml.append(xml)
+ return self
+
+ def pop(self, index=0):
+ """
+ Remove and return the last substanza in the list of
+ iterable substanzas.
+
+ Allows stanza objects to be used like lists.
+
+ Arguments:
+ index -- The index of the substanza to remove.
+ """
+ substanza = self.iterables.pop(index)
+ self.xml.remove(substanza.xml)
+ return substanza
+
+ def next(self):
+ """
+ Return the next iterable substanza.
+ """
+ return self.__next__()
+
+ @property
+ def attrib(self):
+ """
+ DEPRECATED
+
+ For backwards compatibility, stanza.attrib returns the stanza itself.
+
+ Older implementations of stanza objects used XML objects directly,
+ requiring the use of .attrib to access attribute values.
+
+ Use of the dictionary syntax with the stanza object itself for
+ accessing stanza interfaces is preferred.
+ """
+ return self
+
+ def _fix_ns(self, xpath, split=False, propagate_ns=True):
+ """
+ Apply the stanza's namespace to elements in an XPath expression.
+
+ Arguments:
+ xpath -- The XPath expression to fix with namespaces.
+ split -- Indicates if the fixed XPath should be left as a
+ list of element names with namespaces. Defaults to
+ False, which returns a flat string path.
+ propagate_ns -- Overrides propagating parent element namespaces
+ to child elements. Useful if you wish to simply
+ split an XPath that has non-specified namespaces,
+ and child and parent namespaces are known not to
+ always match. Defaults to True.
+ """
+ fixed = []
+ # Split the XPath into a series of blocks, where a block
+ # is started by an element with a namespace.
+ ns_blocks = xpath.split('{')
+ for ns_block in ns_blocks:
+ if '}' in ns_block:
+ # Apply the found namespace to following elements
+ # that do not have namespaces.
+ namespace = ns_block.split('}')[0]
+ elements = ns_block.split('}')[1].split('/')
+ else:
+ # Apply the stanza's namespace to the following
+ # elements since no namespace was provided.
+ namespace = self.namespace
+ elements = ns_block.split('/')
+
+ for element in elements:
+ if element:
+ # Skip empty entry artifacts from splitting.
+ if propagate_ns:
+ tag = '{%s}%s' % (namespace, element)
+ else:
+ tag = element
+ fixed.append(tag)
+ if split:
+ return fixed
+ return '/'.join(fixed)
+
+ def __eq__(self, other):
+ """
+ Compare the stanza object with another to test for equality.
+
+ Stanzas are equal if their interfaces return the same values,
+ and if they are both instances of ElementBase.
+
+ Arguments:
+ other -- The stanza object to compare against.
+ """
+ if not isinstance(other, ElementBase):
+ return False
+
+ # Check that this stanza is a superset of the other stanza.
+ values = self._get_stanza_values()
+ for key in other.keys():
+ if key not in values or values[key] != other[key]:
+ return False
+
+ # Check that the other stanza is a superset of this stanza.
+ values = other._get_stanza_values()
+ for key in self.keys():
+ if key not in values or values[key] != self[key]:
+ return False
+
+ # Both stanzas are supersets of each other, therefore they
+ # must be equal.
+ return True
+
+ def __ne__(self, other):
+ """
+ Compare the stanza object with another to test for inequality.
+
+ Stanzas are not equal if their interfaces return different values,
+ or if they are not both instances of ElementBase.
+
+ Arguments:
+ other -- The stanza object to compare against.
+ """
+ return not self.__eq__(other)
+
+ def __bool__(self):
+ """
+ Stanza objects should be treated as True in boolean contexts.
+
+ Python 3.x version.
+ """
+ return True
+
+ def __nonzero__(self):
+ """
+ Stanza objects should be treated as True in boolean contexts.
+
+ Python 2.x version.
+ """
+ return True
+
+ def __len__(self):
+ """
+ Return the number of iterable substanzas contained in this stanza.
+ """
+ return len(self.iterables)
+
+ def __iter__(self):
+ """
+ Return an iterator object for iterating over the stanza's substanzas.
+
+ The iterator is the stanza object itself. Attempting to use two
+ iterators on the same stanza at the same time is discouraged.
+ """
+ self._index = 0
+ return self
+
+ def __next__(self):
+ """
+ Return the next iterable substanza.
+ """
+ self._index += 1
+ if self._index > len(self.iterables):
+ self._index = 0
+ raise StopIteration
+ return self.iterables[self._index - 1]
+
+ def __copy__(self):
+ """
+ Return a copy of the stanza object that does not share the same
+ underlying XML object.
+ """
+ return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent)
+
+ def __str__(self):
+ """
+ Return a string serialization of the underlying XML object.
+ """
+ return tostring(self.xml, xmlns='', stanza_ns=self.namespace)
+
+ def __repr__(self):
+ """
+ Use the stanza's serialized XML as its representation.
+ """
+ return self.__str__()
+
+
+class StanzaBase(ElementBase):
+
+ """
+ StanzaBase provides the foundation for all other stanza objects used by
+ SleekXMPP, and defines a basic set of interfaces common to nearly
+ all stanzas. These interfaces are the 'id', 'type', 'to', and 'from'
+ attributes. An additional interface, 'payload', is available to access
+ the XML contents of the stanza. Most stanza objects will provided more
+ specific interfaces, however.
+
+ Stanza Interface:
+ from -- A JID object representing the sender's JID.
+ id -- An optional id value that can be used to associate stanzas
+ with their replies.
+ payload -- The XML contents of the stanza.
+ to -- A JID object representing the recipient's JID.
+ type -- The type of stanza, typically will be 'normal', 'error',
+ 'get', or 'set', etc.
+
+ Attributes:
+ stream -- The XMLStream instance that will handle sending this stanza.
+ tag -- The namespaced version of the stanza's name.
+
+ Methods:
+ set_type -- Set the type of the stanza.
+ get_to -- Return the stanza recipients JID.
+ set_to -- Set the stanza recipient's JID.
+ get_from -- Return the stanza sender's JID.
+ set_from -- Set the stanza sender's JID.
+ get_payload -- Return the stanza's XML contents.
+ set_payload -- Append to the stanza's XML contents.
+ del_payload -- Remove the stanza's XML contents.
+ clear -- Reset the stanza's XML contents.
+ reply -- Reset the stanza and modify the 'to' and 'from'
+ attributes to prepare for sending a reply.
+ error -- Set the stanza's type to 'error'.
+ unhandled -- Callback for when the stanza is not handled by a
+ stream handler.
+ exception -- Callback for if an exception is raised while
+ handling the stanza.
+ send -- Send the stanza using the stanza's stream.
+ """
+
+ name = 'stanza'
+ namespace = 'jabber:client'
+ interfaces = set(('type', 'to', 'from', 'id', 'payload'))
+ types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat'))
+ sub_interfaces = tuple()
+
+ def __init__(self, stream=None, xml=None, stype=None,
+ sto=None, sfrom=None, sid=None):
+ """
+ Create a new stanza.
+
+ Arguments:
+ stream -- Optional XMLStream responsible for sending this stanza.
+ xml -- Optional XML contents to initialize stanza values.
+ stype -- Optional stanza type value.
+ sto -- Optional string or JID object of the recipient's JID.
+ sfrom -- Optional string or JID object of the sender's JID.
+ sid -- Optional ID value for the stanza.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.setType = self.set_type
+ self.getTo = self.get_to
+ self.setTo = self.set_to
+ self.getFrom = self.get_from
+ self.setFrom = self.set_from
+ self.getPayload = self.get_payload
+ self.setPayload = self.set_payload
+ self.delPayload = self.del_payload
+
+ self.stream = stream
+ if stream is not None:
+ self.namespace = stream.default_ns
+ ElementBase.__init__(self, xml)
+ if stype is not None:
+ self['type'] = stype
+ if sto is not None:
+ self['to'] = sto
+ if sfrom is not None:
+ self['from'] = sfrom
+ self.tag = "{%s}%s" % (self.namespace, self.name)
+
+ def set_type(self, value):
+ """
+ Set the stanza's 'type' attribute.
+
+ Only type values contained in StanzaBase.types are accepted.
+
+ Arguments:
+ value -- One of the values contained in StanzaBase.types
+ """
+ if value in self.types:
+ self.xml.attrib['type'] = value
+ return self
+
+ def get_to(self):
+ """Return the value of the stanza's 'to' attribute."""
+ return JID(self._get_attr('to'))
+
+ def set_to(self, value):
+ """
+ Set the 'to' attribute of the stanza.
+
+ Arguments:
+ value -- A string or JID object representing the recipient's JID.
+ """
+ return self._set_attr('to', str(value))
+
+ def get_from(self):
+ """Return the value of the stanza's 'from' attribute."""
+ return JID(self._get_attr('from'))
+
+ def set_from(self, value):
+ """
+ Set the 'from' attribute of the stanza.
+
+ Arguments:
+ from -- A string or JID object representing the sender's JID.
+ """
+ return self._set_attr('from', str(value))
+
+ def get_payload(self):
+ """Return a list of XML objects contained in the stanza."""
+ return self.xml.getchildren()
+
+ def set_payload(self, value):
+ """
+ Add XML content to the stanza.
+
+ Arguments:
+ value -- Either an XML or a stanza object, or a list
+ of XML or stanza objects.
+ """
+ if not isinstance(value, list):
+ value = [value]
+ for val in value:
+ self.append(val)
+ return self
+
+ def del_payload(self):
+ """Remove the XML contents of the stanza."""
+ self.clear()
+ return self
+
+ def clear(self):
+ """
+ Remove all XML element contents and plugins.
+
+ Any attribute values will be preserved.
+ """
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+ for plugin in list(self.plugins.keys()):
+ del self.plugins[plugin]
+ return self
+
+ def reply(self):
+ """
+ Reset the stanza and swap its 'from' and 'to' attributes to prepare
+ for sending a reply stanza.
+
+ For client streams, the 'from' attribute is removed.
+ """
+ # if it's a component, use from
+ if self.stream and hasattr(self.stream, "is_component") and \
+ self.stream.is_component:
+ self['from'], self['to'] = self['to'], self['from']
+ else:
+ self['to'] = self['from']
+ del self['from']
+ self.clear()
+ return self
+
+ def error(self):
+ """Set the stanza's type to 'error'."""
+ self['type'] = 'error'
+ return self
+
+ def unhandled(self):
+ """
+ Called when no handlers have been registered to process this
+ stanza.
+
+ Meant to be overridden.
+ """
+ pass
+
+ def exception(self, e):
+ """
+ Handle exceptions raised during stanza processing.
+
+ Meant to be overridden.
+ """
+ logging.exception('Error handling {%s}%s stanza' % (self.namespace,
+ self.name))
+
+ def send(self):
+ """Queue the stanza to be sent on the XML stream."""
+ self.stream.sendRaw(self.__str__())
+
+ def __copy__(self):
+ """
+ Return a copy of the stanza object that does not share the
+ same underlying XML object, but does share the same XML stream.
+ """
+ return self.__class__(xml=copy.deepcopy(self.xml),
+ stream=self.stream)
+
+ def __str__(self):
+ """Serialize the stanza's XML to a string."""
+ return tostring(self.xml, xmlns='',
+ stanza_ns=self.namespace,
+ stream=self.stream)
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 @@
+<stream>
+</stream>
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("<stream>")
+#s.flush()
+s.send("<test/>")
+s.send("<test/>")
+s.send("<test/>")
+s.send("</stream>")
+#s.flush()
+s.close()
diff --git a/sleekxmpp/xmlstream/tostring/__init__.py b/sleekxmpp/xmlstream/tostring/__init__.py
new file mode 100644
index 0000000..5852cba
--- /dev/null
+++ b/sleekxmpp/xmlstream/tostring/__init__.py
@@ -0,0 +1,19 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import sys
+
+# Import the correct tostring and xml_escape functions based on the Python
+# version in order to properly handle Unicode.
+
+if sys.version_info < (3, 0):
+ from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape
+else:
+ from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape
+
+__all__ = ['tostring', 'xml_escape']
diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py
new file mode 100644
index 0000000..d8f5c5b
--- /dev/null
+++ b/sleekxmpp/xmlstream/tostring/tostring.py
@@ -0,0 +1,95 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
+ """
+ Serialize an XML object to a Unicode string.
+
+ If namespaces are provided using xmlns or stanza_ns, then elements
+ that use those namespaces will not include the xmlns attribute in
+ the output.
+
+ Arguments:
+ xml -- The XML object to serialize. If the value is None,
+ then the XML object contained in this stanza
+ object will be used.
+ xmlns -- Optional namespace of an element wrapping the XML
+ object.
+ stanza_ns -- The namespace of the stanza object that contains
+ the XML object.
+ stream -- The XML stream that generated the XML object.
+ outbuffer -- Optional buffer for storing serializations during
+ recursive calls.
+ """
+ # Add previous results to the start of the output.
+ output = [outbuffer]
+
+ # Extract the element's tag name.
+ tag_name = xml.tag.split('}', 1)[-1]
+
+ # Extract the element's namespace if it is defined.
+ if '}' in xml.tag:
+ tag_xmlns = xml.tag.split('}', 1)[0][1:]
+ else:
+ tag_xmlns = ''
+
+ # Output the tag name and derived namespace of the element.
+ namespace = ''
+ if tag_xmlns not in ['', xmlns, stanza_ns]:
+ namespace = ' xmlns="%s"' % tag_xmlns
+ if stream and tag_xmlns in stream.namespace_map:
+ mapped_namespace = stream.namespace_map[tag_xmlns]
+ if mapped_namespace:
+ tag_name = "%s:%s" % (mapped_namespace, tag_name)
+ output.append("<%s" % tag_name)
+ output.append(namespace)
+
+ # Output escaped attribute values.
+ for attrib, value in xml.attrib.items():
+ if '{' not in attrib:
+ value = xml_escape(value)
+ output.append(' %s="%s"' % (attrib, value))
+
+ if len(xml) or xml.text:
+ # If there are additional child elements to serialize.
+ output.append(">")
+ if xml.text:
+ output.append(xml_escape(xml.text))
+ if len(xml):
+ for child in xml.getchildren():
+ output.append(tostring(child, tag_xmlns, stanza_ns, stream))
+ output.append("</%s>" % tag_name)
+ elif xml.text:
+ # If we only have text content.
+ output.append(">%s</%s>" % (xml_escape(xml.text), tag_name))
+ else:
+ # Empty element.
+ output.append(" />")
+ if xml.tail:
+ # If there is additional text after the element.
+ output.append(xml_escape(xml.tail))
+ return ''.join(output)
+
+
+def xml_escape(text):
+ """
+ Convert special characters in XML to escape sequences.
+
+ Arguments:
+ text -- The XML text to convert.
+ """
+ text = list(text)
+ escapes = {'&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ "'": '&apos;',
+ '"': '&quot;'}
+ for i, c in enumerate(text):
+ text[i] = escapes.get(c, c)
+ return ''.join(text)
diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py
new file mode 100644
index 0000000..0ee432c
--- /dev/null
+++ b/sleekxmpp/xmlstream/tostring/tostring26.py
@@ -0,0 +1,101 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import unicode_literals
+import types
+
+
+def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''):
+ """
+ Serialize an XML object to a Unicode string.
+
+ If namespaces are provided using xmlns or stanza_ns, then elements
+ that use those namespaces will not include the xmlns attribute in
+ the output.
+
+ Arguments:
+ xml -- The XML object to serialize. If the value is None,
+ then the XML object contained in this stanza
+ object will be used.
+ xmlns -- Optional namespace of an element wrapping the XML
+ object.
+ stanza_ns -- The namespace of the stanza object that contains
+ the XML object.
+ stream -- The XML stream that generated the XML object.
+ outbuffer -- Optional buffer for storing serializations during
+ recursive calls.
+ """
+ # Add previous results to the start of the output.
+ output = [outbuffer]
+
+ # Extract the element's tag name.
+ tag_name = xml.tag.split('}', 1)[-1]
+
+ # Extract the element's namespace if it is defined.
+ if '}' in xml.tag:
+ tag_xmlns = xml.tag.split('}', 1)[0][1:]
+ else:
+ tag_xmlns = u''
+
+ # Output the tag name and derived namespace of the element.
+ namespace = u''
+ if tag_xmlns not in ['', xmlns, stanza_ns]:
+ namespace = u' xmlns="%s"' % tag_xmlns
+ if stream and tag_xmlns in stream.namespace_map:
+ mapped_namespace = stream.namespace_map[tag_xmlns]
+ if mapped_namespace:
+ tag_name = u"%s:%s" % (mapped_namespace, tag_name)
+ output.append(u"<%s" % tag_name)
+ output.append(namespace)
+
+ # Output escaped attribute values.
+ for attrib, value in xml.attrib.items():
+ if '{' not in attrib:
+ value = xml_escape(value)
+ output.append(u' %s="%s"' % (attrib, value))
+
+ if len(xml) or xml.text:
+ # If there are additional child elements to serialize.
+ output.append(u">")
+ if xml.text:
+ output.append(xml_escape(xml.text))
+ if len(xml):
+ for child in xml.getchildren():
+ output.append(tostring(child, tag_xmlns, stanza_ns, stream))
+ output.append(u"</%s>" % tag_name)
+ elif xml.text:
+ # If we only have text content.
+ output.append(u">%s</%s>" % (xml_escape(xml.text), tag_name))
+ else:
+ # Empty element.
+ output.append(u" />")
+ if xml.tail:
+ # If there is additional text after the element.
+ output.append(xml_escape(xml.tail))
+ return u''.join(output)
+
+
+def xml_escape(text):
+ """
+ Convert special characters in XML to escape sequences.
+
+ Arguments:
+ text -- The XML text to convert.
+ """
+ if type(text) != types.UnicodeType:
+ text = list(unicode(text, 'utf-8', 'ignore'))
+ else:
+ text = list(text)
+ escapes = {u'&': u'&amp;',
+ u'<': u'&lt;',
+ u'>': u'&gt;',
+ u"'": u'&apos;',
+ u'"': u'&quot;'}
+ for i, c in enumerate(text):
+ text[i] = escapes.get(c, c)
+ return u''.join(text)
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
new file mode 100644
index 0000000..ace93cc
--- /dev/null
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -0,0 +1,892 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import with_statement, unicode_literals
+
+import copy
+import logging
+import socket as Socket
+import ssl
+import sys
+import threading
+import time
+import types
+import signal
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+from sleekxmpp.thirdparty.statemachine import StateMachine
+from sleekxmpp.xmlstream import Scheduler, tostring
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
+
+# In Python 2.x, file socket objects are broken. A patched socket
+# wrapper is provided for this case in filesocket.py.
+if sys.version_info < (3, 0):
+ from sleekxmpp.xmlstream.filesocket import FileSocket, Socket26
+
+
+# The time in seconds to wait before timing out waiting for response stanzas.
+RESPONSE_TIMEOUT = 10
+
+# The number of threads to use to handle XML stream events. This is not the
+# same as the number of custom event handling threads. HANDLER_THREADS must
+# be at least 1.
+HANDLER_THREADS = 1
+
+# Flag indicating if the SSL library is available for use.
+SSL_SUPPORT = True
+
+
+class RestartStream(Exception):
+ """
+ Exception to restart stream processing, including
+ resending the stream header.
+ """
+
+
+class XMLStream(object):
+ """
+ An XML stream connection manager and event dispatcher.
+
+ The XMLStream class abstracts away the issues of establishing a
+ connection with a server and sending and receiving XML "stanzas".
+ A stanza is a complete XML element that is a direct child of a root
+ document element. Two streams are used, one for each communication
+ direction, over the same socket. Once the connection is closed, both
+ streams should be complete and valid XML documents.
+
+ Three types of events are provided to manage the stream:
+ Stream -- Triggered based on received stanzas, similar in concept
+ to events in a SAX XML parser.
+ Custom -- Triggered manually.
+ Scheduled -- Triggered based on time delays.
+
+ Typically, stanzas are first processed by a stream event handler which
+ will then trigger custom events to continue further processing,
+ especially since custom event handlers may run in individual threads.
+
+
+ Attributes:
+ address -- The hostname and port of the server.
+ default_ns -- The default XML namespace that will be applied
+ to all non-namespaced stanzas.
+ event_queue -- A queue of stream, custom, and scheduled
+ events to be processed.
+ filesocket -- A filesocket created from the main connection socket.
+ Required for ElementTree.iterparse.
+ namespace_map -- Optional mapping of namespaces to namespace prefixes.
+ scheduler -- A scheduler object for triggering events
+ after a given period of time.
+ send_queue -- A queue of stanzas to be sent on the stream.
+ socket -- The connection to the server.
+ ssl_support -- Indicates if a SSL library is available for use.
+ state -- A state machine for managing the stream's
+ connection state.
+ stream_footer -- The start tag and any attributes for the stream's
+ root element.
+ stream_header -- The closing tag of the stream's root element.
+ use_ssl -- Flag indicating if SSL should be used.
+ use_tls -- Flag indicating if TLS should be used.
+ stop -- threading Event used to stop all threads.
+ auto_reconnect-- Flag to determine whether we auto reconnect.
+
+ Methods:
+ add_event_handler -- Add a handler for a custom event.
+ add_handler -- Shortcut method for registerHandler.
+ connect -- Connect to the given server.
+ del_event_handler -- Remove a handler for a custom event.
+ disconnect -- Disconnect from the server and terminate
+ processing.
+ event -- Trigger a custom event.
+ get_id -- Return the current stream ID.
+ incoming_filter -- Optionally filter stanzas before processing.
+ new_id -- Generate a new, unique ID value.
+ process -- Read XML stanzas from the stream and apply
+ matching stream handlers.
+ reconnect -- Reestablish a connection to the server.
+ register_handler -- Add a handler for a stream event.
+ register_stanza -- Add a new stanza object type that may appear
+ as a direct child of the stream's root.
+ remove_handler -- Remove a stream handler.
+ remove_stanza -- Remove a stanza object type.
+ schedule -- Schedule an event handler to execute after a
+ given delay.
+ send -- Send a stanza object on the stream.
+ send_raw -- Send a raw string on the stream.
+ send_xml -- Send an XML string on the stream.
+ set_socket -- Set the stream's socket and generate a new
+ filesocket.
+ start_stream_handler -- Perform any stream initialization such
+ as handshakes.
+ start_tls -- Establish a TLS connection and restart
+ the stream.
+ """
+
+ def __init__(self, socket=None, host='', port=0):
+ """
+ Establish a new XML stream.
+
+ Arguments:
+ socket -- Use an existing socket for the stream.
+ Defaults to None to generate a new socket.
+ host -- The name of the target server.
+ Defaults to the empty string.
+ port -- The port to use for the connection.
+ Defaults to 0.
+ """
+ # To comply with PEP8, method names now use underscores.
+ # Deprecated method names are re-mapped for backwards compatibility.
+ self.startTLS = self.start_tls
+ self.registerStanza = self.register_stanza
+ self.removeStanza = self.remove_stanza
+ self.registerHandler = self.register_handler
+ self.removeHandler = self.remove_handler
+ self.setSocket = self.set_socket
+ self.sendRaw = self.send_raw
+ self.getId = self.get_id
+ self.getNewId = self.new_id
+ self.sendXML = self.send_xml
+
+ self.ssl_support = SSL_SUPPORT
+
+ self.state = StateMachine(('disconnected', 'connected'))
+ self.state._set_state('disconnected')
+
+ self.address = (host, int(port))
+ self.filesocket = None
+ self.set_socket(socket)
+
+ if sys.version_info < (3, 0):
+ self.socket_class = Socket26
+ else:
+ self.socket_class = Socket.socket
+
+ self.use_ssl = False
+ self.use_tls = False
+
+ self.default_ns = ''
+ self.stream_header = "<stream>"
+ self.stream_footer = "</stream>"
+
+ self.stop = threading.Event()
+ self.stream_end_event = threading.Event()
+ self.stream_end_event.set()
+ self.event_queue = queue.Queue()
+ self.send_queue = queue.Queue()
+ self.scheduler = Scheduler(self.event_queue, self.stop)
+
+ self.namespace_map = {}
+
+ self.__thread = {}
+ self.__root_stanza = []
+ self.__handlers = []
+ self.__event_handlers = {}
+ self.__event_handlers_lock = threading.Lock()
+
+ self._id = 0
+ self._id_lock = threading.Lock()
+
+ self.auto_reconnect = True
+ self.is_client = False
+
+ signal.signal(signal.SIGHUP, self._handle_kill)
+ signal.signal(signal.SIGTERM, self._handle_kill) # used in Windows
+
+ def _handle_kill(self, signum, frame):
+ """
+ Capture kill event and disconnect cleanly after first
+ spawning the "killed" event.
+ """
+ self.event("killed", direct=True)
+ self.disconnect()
+
+ def new_id(self):
+ """
+ Generate and return a new stream ID in hexadecimal form.
+
+ Many stanzas, handlers, or matchers may require unique
+ ID values. Using this method ensures that all new ID values
+ are unique in this stream.
+ """
+ with self._id_lock:
+ self._id += 1
+ return self.get_id()
+
+ def get_id(self):
+ """
+ Return the current unique stream ID in hexadecimal form.
+ """
+ return "%X" % self._id
+
+ def connect(self, host='', port=0, use_ssl=False,
+ use_tls=True, reattempt=True):
+ """
+ Create a new socket and connect to the server.
+
+ Setting reattempt to True will cause connection attempts to be made
+ every second until a successful connection is established.
+
+ Arguments:
+ host -- The name of the desired server for the connection.
+ port -- Port to connect to on the server.
+ use_ssl -- Flag indicating if SSL should be used.
+ use_tls -- Flag indicating if TLS should be used.
+ reattempt -- Flag indicating if the socket should reconnect
+ after disconnections.
+ """
+ if host and port:
+ self.address = (host, int(port))
+
+ self.is_client = True
+ # Respect previous SSL and TLS usage directives.
+ if use_ssl is not None:
+ self.use_ssl = use_ssl
+ if use_tls is not None:
+ self.use_tls = use_tls
+
+ # Repeatedly attempt to connect until a successful connection
+ # is established.
+ connected = self.state.transition('disconnected', 'connected',
+ func=self._connect)
+ while reattempt and not connected:
+ connected = self.state.transition('disconnected', 'connected',
+ func=self._connect)
+ return connected
+
+ def _connect(self):
+ self.stop.clear()
+ self.socket = self.socket_class(Socket.AF_INET, Socket.SOCK_STREAM)
+ self.socket.settimeout(None)
+ if self.use_ssl and self.ssl_support:
+ logging.debug("Socket Wrapped for SSL")
+ ssl_socket = ssl.wrap_socket(self.socket)
+ if hasattr(self.socket, 'socket'):
+ # We are using a testing socket, so preserve the top
+ # layer of wrapping.
+ self.socket.socket = ssl_socket
+ else:
+ self.socket = ssl_socket
+
+ try:
+ logging.debug("Connecting to %s:%s" % self.address)
+ self.socket.connect(self.address)
+ self.set_socket(self.socket, ignore=True)
+ #this event is where you should set your application state
+ self.event("connected", direct=True)
+ return True
+ except Socket.error as serr:
+ error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
+ logging.error(error_msg % (self.address[0], self.address[1],
+ serr.errno, serr.strerror))
+ time.sleep(1)
+ return False
+
+ def disconnect(self, reconnect=False):
+ """
+ Terminate processing and close the XML streams.
+
+ Optionally, the connection may be reconnected and
+ resume processing afterwards.
+
+ Arguments:
+ reconnect -- Flag indicating if the connection
+ and processing should be restarted.
+ Defaults to False.
+ """
+ self.state.transition('connected', 'disconnected', wait=0.0,
+ func=self._disconnect, args=(reconnect,))
+
+ def _disconnect(self, reconnect=False):
+ # Send the end of stream marker.
+ self.send_raw(self.stream_footer)
+ # Wait for confirmation that the stream was
+ # closed in the other direction.
+ if not reconnect:
+ self.auto_reconnect = False
+ self.stream_end_event.wait(4)
+ if not self.auto_reconnect:
+ self.stop.set()
+ try:
+ self.socket.close()
+ self.filesocket.close()
+ self.socket.shutdown(Socket.SHUT_RDWR)
+ except Socket.error as serr:
+ pass
+ finally:
+ #clear your application state
+ self.event("disconnected", direct=True)
+ return True
+
+ def reconnect(self):
+ """
+ Reset the stream's state and reconnect to the server.
+ """
+ logging.debug("reconnecting...")
+ self.state.transition('connected', 'disconnected', wait=2.0,
+ func=self._disconnect, args=(True,))
+ logging.debug("connecting...")
+ return self.state.transition('disconnected', 'connected',
+ wait=2.0, func=self._connect)
+
+ def set_socket(self, socket, ignore=False):
+ """
+ Set the socket to use for the stream.
+
+ The filesocket will be recreated as well.
+
+ Arguments:
+ socket -- The new socket to use.
+ ignore -- don't set the state
+ """
+ self.socket = socket
+ if socket is not None:
+ # ElementTree.iterparse requires a file.
+ # 0 buffer files have to be binary.
+
+ # Use the correct fileobject type based on the Python
+ # version to work around a broken implementation in
+ # Python 2.x.
+ if sys.version_info < (3, 0):
+ self.filesocket = FileSocket(self.socket)
+ else:
+ self.filesocket = self.socket.makefile('rb', 0)
+ if not ignore:
+ self.state._set_state('connected')
+
+ def start_tls(self):
+ """
+ Perform handshakes for TLS.
+
+ If the handshake is successful, the XML stream will need
+ to be restarted.
+ """
+ if self.ssl_support:
+ logging.info("Negotiating TLS")
+ ssl_socket = ssl.wrap_socket(self.socket,
+ ssl_version=ssl.PROTOCOL_TLSv1,
+ do_handshake_on_connect=False)
+ if hasattr(self.socket, 'socket'):
+ # We are using a testing socket, so preserve the top
+ # layer of wrapping.
+ self.socket.socket = ssl_socket
+ else:
+ self.socket = ssl_socket
+ self.socket.do_handshake()
+ self.set_socket(self.socket)
+ return True
+ else:
+ logging.warning("Tried to enable TLS, but ssl module not found.")
+ return False
+
+ def start_stream_handler(self, xml):
+ """
+ Perform any initialization actions, such as handshakes, once the
+ stream header has been sent.
+
+ Meant to be overridden.
+ """
+ pass
+
+ def register_stanza(self, stanza_class):
+ """
+ Add a stanza object class as a known root stanza. A root stanza is
+ one that appears as a direct child of the stream's root element.
+
+ Stanzas that appear as substanzas of a root stanza do not need to
+ be registered here. That is done using register_stanza_plugin() from
+ sleekxmpp.xmlstream.stanzabase.
+
+ Stanzas that are not registered will not be converted into
+ stanza objects, but may still be processed using handlers and
+ matchers.
+
+ Arguments:
+ stanza_class -- The top-level stanza object's class.
+ """
+ self.__root_stanza.append(stanza_class)
+
+ def remove_stanza(self, stanza_class):
+ """
+ Remove a stanza from being a known root stanza. A root stanza is
+ one that appears as a direct child of the stream's root element.
+
+ Stanzas that are not registered will not be converted into
+ stanza objects, but may still be processed using handlers and
+ matchers.
+ """
+ del self.__root_stanza[stanza_class]
+
+ def add_handler(self, mask, pointer, name=None, disposable=False,
+ threaded=False, filter=False, instream=False):
+ """
+ A shortcut method for registering a handler using XML masks.
+
+ Arguments:
+ mask -- An XML snippet matching the structure of the
+ stanzas that will be passed to this handler.
+ pointer -- The handler function itself.
+ name -- A unique name for the handler. A name will
+ be generated if one is not provided.
+ disposable -- Indicates if the handler should be discarded
+ after one use.
+ threaded -- Deprecated. Remains for backwards compatibility.
+ filter -- Deprecated. Remains for backwards compatibility.
+ instream -- Indicates if the handler should execute during
+ stream processing and not during normal event
+ processing.
+ """
+ # To prevent circular dependencies, we must load the matcher
+ # and handler classes here.
+ from sleekxmpp.xmlstream.matcher import MatchXMLMask
+ from sleekxmpp.xmlstream.handler import XMLCallback
+
+ if name is None:
+ name = 'add_handler_%s' % self.getNewId()
+ self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer,
+ once=disposable, instream=instream))
+
+ def register_handler(self, handler, before=None, after=None):
+ """
+ Add a stream event handler that will be executed when a matching
+ stanza is received.
+
+ Arguments:
+ handler -- The handler object to execute.
+ """
+ if handler.stream is None:
+ self.__handlers.append(handler)
+ handler.stream = self
+
+ def remove_handler(self, name):
+ """
+ Remove any stream event handlers with the given name.
+
+ Arguments:
+ name -- The name of the handler.
+ """
+ idx = 0
+ for handler in self.__handlers:
+ if handler.name == name:
+ self.__handlers.pop(idx)
+ return True
+ idx += 1
+ return False
+
+ def add_event_handler(self, name, pointer,
+ threaded=False, disposable=False):
+ """
+ Add a custom event handler that will be executed whenever
+ its event is manually triggered.
+
+ Arguments:
+ name -- The name of the event that will trigger
+ this handler.
+ pointer -- The function to execute.
+ threaded -- If set to True, the handler will execute
+ in its own thread. Defaults to False.
+ disposable -- If set to True, the handler will be
+ discarded after one use. Defaults to False.
+ """
+ if not name in self.__event_handlers:
+ self.__event_handlers[name] = []
+ self.__event_handlers[name].append((pointer, threaded, disposable))
+
+ def del_event_handler(self, name, pointer):
+ """
+ Remove a function as a handler for an event.
+
+ Arguments:
+ name -- The name of the event.
+ pointer -- The function to remove as a handler.
+ """
+ if not name in self.__event_handlers:
+ return
+
+ # Need to keep handlers that do not use
+ # the given function pointer
+ def filter_pointers(handler):
+ return handler[0] != pointer
+
+ self.__event_handlers[name] = filter(filter_pointers,
+ self.__event_handlers[name])
+
+ def event(self, name, data={}, direct=False):
+ """
+ Manually trigger a custom event.
+
+ Arguments:
+ name -- The name of the event to trigger.
+ data -- Data that will be passed to each event handler.
+ Defaults to an empty dictionary.
+ direct -- Runs the event directly if True.
+ """
+ for handler in self.__event_handlers.get(name, []):
+ if direct:
+ handler[0](copy.copy(data))
+ else:
+ self.event_queue.put(('event', handler, copy.copy(data)))
+ if handler[2]:
+ # If the handler is disposable, we will go ahead and
+ # remove it now instead of waiting for it to be
+ # processed in the queue.
+ with self.__event_handlers_lock:
+ try:
+ h_index = self.__event_handlers[name].index(handler)
+ self.__event_handlers[name].pop(h_index)
+ except:
+ pass
+
+ def schedule(self, name, seconds, callback, args=None,
+ kwargs=None, repeat=False):
+ """
+ Schedule a callback function to execute after a given delay.
+
+ Arguments:
+ name -- A unique name for the scheduled callback.
+ seconds -- The time in seconds to wait before executing.
+ callback -- A pointer to the function to execute.
+ args -- A tuple of arguments to pass to the function.
+ kwargs -- A dictionary of keyword arguments to pass to
+ the function.
+ repeat -- Flag indicating if the scheduled event should
+ be reset and repeat after executing.
+ """
+ self.scheduler.add(name, seconds, callback, args, kwargs,
+ repeat, qpointer=self.event_queue)
+
+ def incoming_filter(self, xml):
+ """
+ Filter incoming XML objects before they are processed.
+
+ Possible uses include remapping namespaces, or correcting elements
+ from sources with incorrect behavior.
+
+ Meant to be overridden.
+ """
+ return xml
+
+ def send(self, data, mask=None, timeout=RESPONSE_TIMEOUT):
+ """
+ A wrapper for send_raw for sending stanza objects.
+
+ May optionally block until an expected response is received.
+
+ Arguments:
+ data -- The stanza object to send on the stream.
+ mask -- Deprecated. An XML snippet matching the structure
+ of the expected response. Execution will block
+ in this thread until the response is received
+ or a timeout occurs.
+ timeout -- Time in seconds to wait for a response before
+ continuing. Defaults to RESPONSE_TIMEOUT.
+ """
+ if hasattr(mask, 'xml'):
+ mask = mask.xml
+ data = str(data)
+ if mask is not None:
+ logging.warning("Use of send mask waiters is deprecated.")
+ wait_for = Waiter("SendWait_%s" % self.new_id(),
+ MatchXMLMask(mask))
+ self.register_handler(wait_for)
+ self.send_raw(data)
+ if mask is not None:
+ return wait_for.wait(timeout)
+
+ def send_raw(self, data):
+ """
+ Send raw data across the stream.
+
+ Arguments:
+ data -- Any string value.
+ """
+ self.send_queue.put(data)
+ return True
+
+ def send_xml(self, data, mask=None, timeout=RESPONSE_TIMEOUT):
+ """
+ Send an XML object on the stream, and optionally wait
+ for a response.
+
+ Arguments:
+ data -- The XML object to send on the stream.
+ mask -- Deprecated. An XML snippet matching the structure
+ of the expected response. Execution will block
+ in this thread until the response is received
+ or a timeout occurs.
+ timeout -- Time in seconds to wait for a response before
+ continuing. Defaults to RESPONSE_TIMEOUT.
+ """
+ return self.send(tostring(data), mask, timeout)
+
+ def process(self, threaded=True):
+ """
+ Initialize the XML streams and begin processing events.
+
+ The number of threads used for processing stream events is determined
+ by HANDLER_THREADS.
+
+ Arguments:
+ threaded -- If threaded=True then event dispatcher will run
+ in a separate thread, allowing for the stream to be
+ used in the background for another application.
+ Defaults to True.
+
+ Event handlers and the send queue will be threaded
+ regardless of this parameter's value.
+ """
+ self.scheduler.process(threaded=True)
+
+ def start_thread(name, target):
+ self.__thread[name] = threading.Thread(name=name, target=target)
+ self.__thread[name].start()
+
+ for t in range(0, HANDLER_THREADS):
+ logging.debug("Starting HANDLER THREAD")
+ start_thread('stream_event_handler_%s' % t, self._event_runner)
+
+ start_thread('send_thread', self._send_thread)
+
+ if threaded:
+ # Run the XML stream in the background for another application.
+ start_thread('process', self._process)
+ else:
+ self._process()
+
+ def _process(self):
+ """
+ Start processing the XML streams.
+
+ Processing will continue after any recoverable errors
+ if reconnections are allowed.
+ """
+ firstrun = True
+
+ # The body of this loop will only execute once per connection.
+ # Additional passes will be made only if an error occurs and
+ # reconnecting is permitted.
+ while firstrun or (self.auto_reconnect and not self.stop.isSet()):
+ firstrun = False
+ try:
+ if self.is_client:
+ self.send_raw(self.stream_header)
+ # The call to self.__read_xml will block and prevent
+ # the body of the loop from running until a disconnect
+ # occurs. After any reconnection, the stream header will
+ # be resent and processing will resume.
+ while not self.stop.isSet() and self.__read_xml():
+ # Ensure the stream header is sent for any
+ # new connections.
+ if self.is_client:
+ self.send_raw(self.stream_header)
+ except KeyboardInterrupt:
+ logging.debug("Keyboard Escape Detected in _process")
+ self.stop.set()
+ except SystemExit:
+ logging.debug("SystemExit in _process")
+ self.stop.set()
+ except Socket.error:
+ logging.exception('Socket Error')
+ except:
+ if not self.stop.isSet():
+ logging.exception('Connection error.')
+ if not self.stop.isSet() and self.auto_reconnect:
+ self.reconnect()
+ else:
+ self.disconnect()
+ self.event_queue.put(('quit', None, None))
+ self.scheduler.run = False
+
+ def __read_xml(self):
+ """
+ Parse the incoming XML stream, raising stream events for
+ each received stanza.
+ """
+ depth = 0
+ root = None
+ for (event, xml) in ET.iterparse(self.filesocket, (b'end', b'start')):
+ if event == b'start':
+ if depth == 0:
+ # We have received the start of the root element.
+ root = xml
+ # Perform any stream initialization actions, such
+ # as handshakes.
+ self.stream_end_event.clear()
+ self.start_stream_handler(root)
+ depth += 1
+ if event == b'end':
+ depth -= 1
+ if depth == 0:
+ # The stream's root element has closed,
+ # terminating the stream.
+ logging.debug("End of stream recieved")
+ self.stream_end_event.set()
+ return False
+ elif depth == 1:
+ # We only raise events for stanzas that are direct
+ # children of the root element.
+ try:
+ self.__spawn_event(xml)
+ except RestartStream:
+ return True
+ if root:
+ # Keep the root element empty of children to
+ # save on memory use.
+ root.clear()
+ logging.debug("Ending read XML loop")
+
+ def __spawn_event(self, xml):
+ """
+ Analyze incoming XML stanzas and convert them into stanza
+ objects if applicable and queue stream events to be processed
+ by matching handlers.
+
+ Arguments:
+ xml -- The XML stanza to analyze.
+ """
+ logging.debug("RECV: %s" % tostring(xml,
+ xmlns=self.default_ns,
+ stream=self))
+ # Apply any preprocessing filters.
+ xml = self.incoming_filter(xml)
+
+ # Convert the raw XML object into a stanza object. If no registered
+ # stanza type applies, a generic StanzaBase stanza will be used.
+ stanza_type = StanzaBase
+ for stanza_class in self.__root_stanza:
+ if xml.tag == "{%s}%s" % (self.default_ns, stanza_class.name):
+ stanza_type = stanza_class
+ break
+ stanza = stanza_type(self, xml)
+
+ # Match the stanza against registered handlers. Handlers marked
+ # to run "in stream" will be executed immediately; the rest will
+ # be queued.
+ unhandled = True
+ for handler in self.__handlers:
+ if handler.match(stanza):
+ stanza_copy = stanza_type(self, copy.deepcopy(xml))
+ handler.prerun(stanza_copy)
+ self.event_queue.put(('stanza', handler, stanza_copy))
+ try:
+ if handler.check_delete():
+ self.__handlers.pop(self.__handlers.index(handler))
+ except:
+ pass # not thread safe
+ unhandled = False
+
+ # Some stanzas require responses, such as Iq queries. A default
+ # handler will be executed immediately for this case.
+ if unhandled:
+ stanza.unhandled()
+
+ def _threaded_event_wrapper(self, func, args):
+ """
+ Capture exceptions for event handlers that run
+ in individual threads.
+
+ Arguments:
+ func -- The event handler to execute.
+ args -- Arguments to the event handler.
+ """
+ try:
+ func(*args)
+ except Exception as e:
+ error_msg = 'Error processing event handler: %s'
+ logging.exception(error_msg % str(func))
+ if hasattr(args[0], 'exception'):
+ args[0].exception(e)
+
+ def _event_runner(self):
+ """
+ Process the event queue and execute handlers.
+
+ The number of event runner threads is controlled by HANDLER_THREADS.
+
+ Stream event handlers will all execute in this thread. Custom event
+ handlers may be spawned in individual threads.
+ """
+ logging.debug("Loading event runner")
+ try:
+ while not self.stop.isSet():
+ try:
+ event = self.event_queue.get(True, timeout=5)
+ except queue.Empty:
+ event = None
+ if event is None:
+ continue
+
+ etype, handler = event[0:2]
+ args = event[2:]
+
+ if etype == 'stanza':
+ try:
+ handler.run(args[0])
+ except Exception as e:
+ error_msg = 'Error processing stream handler: %s'
+ logging.exception(error_msg % handler.name)
+ args[0].exception(e)
+ elif etype == 'schedule':
+ try:
+ logging.debug(args)
+ handler(*args[0])
+ except:
+ logging.exception('Error processing scheduled task')
+ elif etype == 'event':
+ func, threaded, disposable = handler
+ try:
+ if threaded:
+ x = threading.Thread(
+ name="Event_%s" % str(func),
+ target=self._threaded_event_wrapper,
+ args=(func, args))
+ x.start()
+ else:
+ func(*args)
+ except Exception as e:
+ error_msg = 'Error processing event handler: %s'
+ logging.exception(error_msg % str(func))
+ if hasattr(args[0], 'exception'):
+ args[0].exception(e)
+ elif etype == 'quit':
+ logging.debug("Quitting event runner thread")
+ return False
+ except KeyboardInterrupt:
+ logging.debug("Keyboard Escape Detected in _event_runner")
+ self.disconnect()
+ return
+ except SystemExit:
+ self.disconnect()
+ self.event_queue.put(('quit', None, None))
+ return
+
+ def _send_thread(self):
+ """
+ Extract stanzas from the send queue and send them on the stream.
+ """
+ try:
+ while not self.stop.isSet():
+ try:
+ data = self.send_queue.get(True, 1)
+ except queue.Empty:
+ continue
+ logging.debug("SEND: %s" % data)
+ try:
+ self.socket.send(data.encode('utf-8'))
+ except:
+ logging.warning("Failed to send %s" % data)
+ self.disconnect(self.auto_reconnect)
+ except KeyboardInterrupt:
+ logging.debug("Keyboard Escape Detected in _send_thread")
+ self.disconnect()
+ return
+ except SystemExit:
+ self.disconnect()
+ self.event_queue.put(('quit', None, None))
+ return