aboutsummaryrefslogtreecommitdiffstats
path: root/sleekxmpp/xmlstream
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp/xmlstream')
-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
25 files changed, 3440 insertions, 0 deletions
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