summaryrefslogtreecommitdiff
path: root/sleekxmpp
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp')
-rw-r--r--sleekxmpp/__init__.py19
-rw-r--r--sleekxmpp/basexmpp.py12
-rw-r--r--sleekxmpp/clientxmpp.py40
-rw-r--r--sleekxmpp/componentxmpp.py19
-rw-r--r--sleekxmpp/exceptions.py2
-rw-r--r--sleekxmpp/features/feature_bind/bind.py12
-rw-r--r--sleekxmpp/features/feature_mechanisms/mechanisms.py46
-rw-r--r--sleekxmpp/features/feature_mechanisms/stanza/auth.py2
-rw-r--r--sleekxmpp/features/feature_preapproval/preapproval.py2
-rw-r--r--sleekxmpp/features/feature_rosterver/rosterver.py2
-rw-r--r--sleekxmpp/features/feature_session/session.py2
-rw-r--r--sleekxmpp/jid.py144
-rw-r--r--sleekxmpp/plugins/__init__.py12
-rw-r--r--sleekxmpp/plugins/google/__init__.py47
-rw-r--r--sleekxmpp/plugins/google/auth/__init__.py10
-rw-r--r--sleekxmpp/plugins/google/auth/auth.py52
-rw-r--r--sleekxmpp/plugins/google/auth/stanza.py49
-rw-r--r--sleekxmpp/plugins/google/gmail/__init__.py10
-rw-r--r--sleekxmpp/plugins/google/gmail/notifications.py96
-rw-r--r--sleekxmpp/plugins/google/gmail/stanza.py101
-rw-r--r--sleekxmpp/plugins/google/nosave/__init__.py10
-rw-r--r--sleekxmpp/plugins/google/nosave/nosave.py83
-rw-r--r--sleekxmpp/plugins/google/nosave/stanza.py59
-rw-r--r--sleekxmpp/plugins/google/settings/__init__.py10
-rw-r--r--sleekxmpp/plugins/google/settings/settings.py63
-rw-r--r--sleekxmpp/plugins/google/settings/stanza.py110
-rw-r--r--sleekxmpp/plugins/jobs.py49
-rw-r--r--sleekxmpp/plugins/old_0004.py421
-rw-r--r--sleekxmpp/plugins/old_0009.py277
-rw-r--r--sleekxmpp/plugins/old_0050.py133
-rw-r--r--sleekxmpp/plugins/old_0060.py313
-rw-r--r--sleekxmpp/plugins/xep_0004/stanza/field.py5
-rw-r--r--sleekxmpp/plugins/xep_0004/stanza/form.py22
-rw-r--r--sleekxmpp/plugins/xep_0009/remote.py61
-rw-r--r--sleekxmpp/plugins/xep_0009/rpc.py10
-rw-r--r--sleekxmpp/plugins/xep_0020/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0020/feature_negotiation.py36
-rw-r--r--sleekxmpp/plugins/xep_0020/stanza.py17
-rw-r--r--sleekxmpp/plugins/xep_0027/gpg.py2
-rw-r--r--sleekxmpp/plugins/xep_0027/stanza.py2
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py9
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/items.py7
-rw-r--r--sleekxmpp/plugins/xep_0045.py60
-rw-r--r--sleekxmpp/plugins/xep_0047/ibb.py121
-rw-r--r--sleekxmpp/plugins/xep_0047/stream.py60
-rw-r--r--sleekxmpp/plugins/xep_0048/__init__.py15
-rw-r--r--sleekxmpp/plugins/xep_0048/bookmarks.py76
-rw-r--r--sleekxmpp/plugins/xep_0048/stanza.py65
-rw-r--r--sleekxmpp/plugins/xep_0050/adhoc.py124
-rw-r--r--sleekxmpp/plugins/xep_0054/vcard_temp.py18
-rw-r--r--sleekxmpp/plugins/xep_0060/pubsub.py2
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub.py40
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py5
-rw-r--r--sleekxmpp/plugins/xep_0065/__init__.py2
-rw-r--r--sleekxmpp/plugins/xep_0065/proxy.py499
-rw-r--r--sleekxmpp/plugins/xep_0065/stanza.py54
-rw-r--r--sleekxmpp/plugins/xep_0071/__init__.py15
-rw-r--r--sleekxmpp/plugins/xep_0071/stanza.py81
-rw-r--r--sleekxmpp/plugins/xep_0071/xhtml_im.py30
-rw-r--r--sleekxmpp/plugins/xep_0077/register.py21
-rw-r--r--sleekxmpp/plugins/xep_0078/legacyauth.py3
-rw-r--r--sleekxmpp/plugins/xep_0079/__init__.py18
-rw-r--r--sleekxmpp/plugins/xep_0079/amp.py79
-rw-r--r--sleekxmpp/plugins/xep_0079/stanza.py96
-rw-r--r--sleekxmpp/plugins/xep_0082.py1
-rw-r--r--sleekxmpp/plugins/xep_0084/avatar.py1
-rw-r--r--sleekxmpp/plugins/xep_0084/stanza.py9
-rw-r--r--sleekxmpp/plugins/xep_0091/stanza.py5
-rw-r--r--sleekxmpp/plugins/xep_0092/version.py14
-rw-r--r--sleekxmpp/plugins/xep_0095/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0095/stanza.py25
-rw-r--r--sleekxmpp/plugins/xep_0095/stream_initiation.py214
-rw-r--r--sleekxmpp/plugins/xep_0096/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0096/file_transfer.py58
-rw-r--r--sleekxmpp/plugins/xep_0096/stanza.py48
-rw-r--r--sleekxmpp/plugins/xep_0115/caps.py98
-rw-r--r--sleekxmpp/plugins/xep_0152/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0152/reachability.py93
-rw-r--r--sleekxmpp/plugins/xep_0152/stanza.py29
-rw-r--r--sleekxmpp/plugins/xep_0153/vcard_avatar.py25
-rw-r--r--sleekxmpp/plugins/xep_0163.py2
-rw-r--r--sleekxmpp/plugins/xep_0184/receipt.py3
-rw-r--r--sleekxmpp/plugins/xep_0196/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0196/stanza.py20
-rw-r--r--sleekxmpp/plugins/xep_0196/user_gaming.py97
-rw-r--r--sleekxmpp/plugins/xep_0199/ping.py142
-rw-r--r--sleekxmpp/plugins/xep_0202/time.py2
-rw-r--r--sleekxmpp/plugins/xep_0203/stanza.py6
-rw-r--r--sleekxmpp/plugins/xep_0222.py5
-rw-r--r--sleekxmpp/plugins/xep_0223.py5
-rw-r--r--sleekxmpp/plugins/xep_0231/bob.py4
-rw-r--r--sleekxmpp/plugins/xep_0231/stanza.py7
-rw-r--r--sleekxmpp/plugins/xep_0257/stanza.py2
-rw-r--r--sleekxmpp/plugins/xep_0313/mam.py2
-rw-r--r--sleekxmpp/plugins/xep_0313/stanza.py22
-rw-r--r--sleekxmpp/plugins/xep_0319/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0319/idle.py75
-rw-r--r--sleekxmpp/plugins/xep_0319/stanza.py28
-rw-r--r--sleekxmpp/plugins/xep_0323/__init__.py18
-rw-r--r--sleekxmpp/plugins/xep_0323/device.py258
-rw-r--r--sleekxmpp/plugins/xep_0323/sensordata.py723
-rw-r--r--sleekxmpp/plugins/xep_0323/stanza/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0323/stanza/base.py13
-rw-r--r--sleekxmpp/plugins/xep_0323/stanza/sensordata.py792
-rw-r--r--sleekxmpp/plugins/xep_0323/timerreset.py69
-rw-r--r--sleekxmpp/plugins/xep_0325/__init__.py18
-rw-r--r--sleekxmpp/plugins/xep_0325/control.py569
-rw-r--r--sleekxmpp/plugins/xep_0325/device.py125
-rw-r--r--sleekxmpp/plugins/xep_0325/stanza/__init__.py12
-rw-r--r--sleekxmpp/plugins/xep_0325/stanza/base.py13
-rw-r--r--sleekxmpp/plugins/xep_0325/stanza/control.py527
-rw-r--r--sleekxmpp/roster/__init__.py1
-rw-r--r--sleekxmpp/roster/single.py6
-rw-r--r--sleekxmpp/stanza/htmlim.py71
-rw-r--r--sleekxmpp/stanza/iq.py40
-rw-r--r--sleekxmpp/stanza/rootstanza.py4
-rw-r--r--sleekxmpp/test/sleektest.py26
-rw-r--r--sleekxmpp/thirdparty/mini_dateutil.py4
-rw-r--r--sleekxmpp/thirdparty/socks.py15
-rw-r--r--sleekxmpp/thirdparty/statemachine.py15
-rw-r--r--sleekxmpp/util/__init__.py25
-rw-r--r--sleekxmpp/util/misc_ops.py18
-rw-r--r--sleekxmpp/util/sasl/__init__.py4
-rw-r--r--sleekxmpp/util/sasl/client.py4
-rw-r--r--sleekxmpp/util/sasl/mechanisms.py44
-rw-r--r--sleekxmpp/util/stringprep_profiles.py3
-rw-r--r--sleekxmpp/version.py4
-rw-r--r--sleekxmpp/xmlstream/cert.py4
-rw-r--r--sleekxmpp/xmlstream/filesocket.py9
-rw-r--r--sleekxmpp/xmlstream/matcher/__init__.py1
-rw-r--r--sleekxmpp/xmlstream/matcher/idsender.py47
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py71
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py37
-rw-r--r--sleekxmpp/xmlstream/resolver.py58
-rw-r--r--sleekxmpp/xmlstream/scheduler.py34
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py66
-rw-r--r--sleekxmpp/xmlstream/tostring.py35
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py154
138 files changed, 6639 insertions, 2310 deletions
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py
index f0dc2ce2..85ee32b6 100644
--- a/sleekxmpp/__init__.py
+++ b/sleekxmpp/__init__.py
@@ -6,14 +6,25 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.basexmpp import BaseXMPP
-from sleekxmpp.clientxmpp import ClientXMPP
-from sleekxmpp.componentxmpp import ComponentXMPP
+import logging
+if hasattr(logging, 'NullHandler'):
+ NullHandler = logging.NullHandler
+else:
+ class NullHandler(logging.Handler):
+ def handle(self, record):
+ pass
+logging.getLogger(__name__).addHandler(NullHandler())
+del NullHandler
+
+
from sleekxmpp.stanza import Message, Presence, Iq
from sleekxmpp.jid import JID, InvalidJID
+from sleekxmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin
from sleekxmpp.xmlstream.handler import *
from sleekxmpp.xmlstream import XMLStream, RestartStream
from sleekxmpp.xmlstream.matcher import *
-from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
+from sleekxmpp.basexmpp import BaseXMPP
+from sleekxmpp.clientxmpp import ClientXMPP
+from sleekxmpp.componentxmpp import ComponentXMPP
from sleekxmpp.version import __version__, __version_info__
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index c3ff5ba3..8cd61b63 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -18,15 +18,13 @@ import sys
import logging
import threading
-import sleekxmpp
-from sleekxmpp import plugins, features, roster
+from sleekxmpp import plugins, roster, stanza
from sleekxmpp.api import APIRegistry
from sleekxmpp.exceptions import IqError, IqTimeout
from sleekxmpp.stanza import Message, Presence, Iq, StreamError
from sleekxmpp.stanza.roster import Roster
from sleekxmpp.stanza.nick import Nick
-from sleekxmpp.stanza.htmlim import HTMLIM
from sleekxmpp.xmlstream import XMLStream, JID
from sleekxmpp.xmlstream import ET, register_stanza_plugin
@@ -34,8 +32,7 @@ from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.stanzabase import XML_NS
-from sleekxmpp.features import *
-from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin
+from sleekxmpp.plugins import PluginManager, load_plugin
log = logging.getLogger(__name__)
@@ -148,7 +145,7 @@ class BaseXMPP(XMLStream):
#: A reference to :mod:`sleekxmpp.stanza` to make accessing
#: stanza classes easier.
- self.stanza = sleekxmpp.stanza
+ self.stanza = stanza
self.register_handler(
Callback('IM',
@@ -201,7 +198,6 @@ class BaseXMPP(XMLStream):
# Initialize a few default stanza plugins.
register_stanza_plugin(Iq, Roster)
register_stanza_plugin(Message, Nick)
- register_stanza_plugin(Message, HTMLIM)
def start_stream_handler(self, xml):
"""Save the stream ID once the streams have been established.
@@ -248,7 +244,7 @@ class BaseXMPP(XMLStream):
self.plugin[name].post_inited = True
return XMLStream.process(self, *args, **kwargs)
- def register_plugin(self, plugin, pconfig={}, module=None):
+ def register_plugin(self, plugin, pconfig=None, module=None):
"""Register and configure a plugin for use in this stream.
:param plugin: The name of the plugin class. Plugin names must
diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py
index 3fa158af..8db6ef17 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -52,7 +52,6 @@ class ClientXMPP(BaseXMPP):
:param jid: The JID of the XMPP user account.
:param password: The password for the XMPP user account.
- :param ssl: **Deprecated.**
:param plugin_config: A dictionary of plugin configurations.
:param plugin_whitelist: A list of approved plugins that
will be loaded when calling
@@ -60,8 +59,13 @@ class ClientXMPP(BaseXMPP):
:param escape_quotes: **Deprecated.**
"""
- def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[],
- escape_quotes=True, sasl_mech=None, lang='en'):
+ def __init__(self, jid, password, plugin_config=None, plugin_whitelist=None, escape_quotes=True, sasl_mech=None,
+ lang='en'):
+ if not plugin_whitelist:
+ plugin_whitelist = []
+ if not plugin_config:
+ plugin_config = {}
+
BaseXMPP.__init__(self, jid, 'jabber:client')
self.escape_quotes = escape_quotes
@@ -96,6 +100,7 @@ class ClientXMPP(BaseXMPP):
self.add_event_handler('connected', self._reset_connection_state)
self.add_event_handler('session_bind', self._handle_session_bind)
+ self.add_event_handler('roster_update', self._handle_roster)
self.register_stanza(StreamFeatures)
@@ -106,7 +111,7 @@ class ClientXMPP(BaseXMPP):
self.register_handler(
Callback('Roster Update',
StanzaPath('iq@type=set/roster'),
- self._handle_roster))
+ lambda iq: self.event('roster_update', iq)))
# Setup default stream features
self.register_plugin('feature_starttls')
@@ -114,8 +119,10 @@ class ClientXMPP(BaseXMPP):
self.register_plugin('feature_session')
self.register_plugin('feature_rosterver')
self.register_plugin('feature_preapproval')
- self.register_plugin('feature_mechanisms',
- pconfig={'use_mech': sasl_mech} if sasl_mech else None)
+ self.register_plugin('feature_mechanisms')
+
+ if sasl_mech:
+ self['feature_mechanisms'].use_mech = sasl_mech
@property
def password(self):
@@ -133,7 +140,7 @@ class ClientXMPP(BaseXMPP):
be attempted. If that fails, the server user in the JID
will be used.
- :param address -- A tuple containing the server's host and port.
+ :param address: A tuple containing the server's host and port.
:param reattempt: If ``True``, repeat attempting to connect if an
error occurs. Defaults to ``True``.
:param use_tls: Indicates if TLS should be used for the
@@ -152,8 +159,6 @@ class ClientXMPP(BaseXMPP):
address = (self.boundjid.host, 5222)
self.dns_service = 'xmpp-client'
- self._expected_server_name = self.boundjid.host
-
return XMLStream.connect(self, address[0], address[1],
use_tls=use_tls, use_ssl=use_ssl,
reattempt=reattempt)
@@ -242,14 +247,22 @@ class ClientXMPP(BaseXMPP):
if 'rosterver' in self.features:
iq['roster']['ver'] = self.client_roster.version
- if not block and callback is None:
- callback = lambda resp: self._handle_roster(resp)
+
+ if not block or callback is not None:
+ block = False
+ if callback is None:
+ callback = lambda resp: self.event('roster_update', resp)
+ else:
+ orig_cb = callback
+ def wrapped(resp):
+ self.event('roster_update', resp)
+ orig_cb(resp)
+ callback = wrapped
response = iq.send(block, timeout, callback)
- self.event('roster_received', response)
if block:
- self._handle_roster(response)
+ self.event('roster_update', response)
return response
def _reset_connection_state(self, event=None):
@@ -300,7 +313,6 @@ class ClientXMPP(BaseXMPP):
roster[jid].save(remove=(item['subscription'] == 'remove'))
- self.event("roster_update", iq)
if iq['type'] == 'set':
resp = self.Iq(stype='result',
sto=iq['from'],
diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py
index 44f82e96..4b229a6f 100644
--- a/sleekxmpp/componentxmpp.py
+++ b/sleekxmpp/componentxmpp.py
@@ -49,8 +49,13 @@ class ComponentXMPP(BaseXMPP):
Defaults to ``False``.
"""
- def __init__(self, jid, secret, host=None, port=None,
- plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
+ def __init__(self, jid, secret, host=None, port=None, plugin_config=None, plugin_whitelist=None, use_jc_ns=False):
+
+ if not plugin_whitelist:
+ plugin_whitelist = []
+ if not plugin_config:
+ plugin_config = {}
+
if use_jc_ns:
default_ns = 'jabber:client'
else:
@@ -123,12 +128,6 @@ class ComponentXMPP(BaseXMPP):
"""
if xml.tag.startswith('{jabber:client}'):
xml.tag = xml.tag.replace('jabber:client', self.default_ns)
-
- # The incoming_filter call is only made on top level stanza
- # elements. So we manually continue filtering on sub-elements.
- for sub in xml:
- self.incoming_filter(sub)
-
return xml
def start_stream_handler(self, xml):
@@ -158,8 +157,8 @@ class ComponentXMPP(BaseXMPP):
"""
self.session_bind_event.set()
self.session_started_event.set()
- self.event("session_bind", self.boundjid, direct=True)
- self.event("session_start")
+ self.event('session_bind', self.boundjid, direct=True)
+ self.event('session_start')
def _handle_probe(self, pres):
self.roster[pres['to']][pres['from']].handle_probe(pres)
diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py
index 8036532d..8a2aa75c 100644
--- a/sleekxmpp/exceptions.py
+++ b/sleekxmpp/exceptions.py
@@ -42,7 +42,7 @@ class XMPPError(Exception):
Defaults to ``True``.
"""
- def __init__(self, condition='undefined-condition', text=None,
+ def __init__(self, condition='undefined-condition', text='',
etype='cancel', extension=None, extension_ns=None,
extension_args=None, clear=True):
if extension_args is None:
diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py
index 0f97952d..ee4c1e9b 100644
--- a/sleekxmpp/features/feature_bind/bind.py
+++ b/sleekxmpp/features/feature_bind/bind.py
@@ -12,7 +12,7 @@ from sleekxmpp.jid import JID
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.features.feature_bind import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.plugins import BasePlugin, register_plugin
+from sleekxmpp.plugins import BasePlugin
log = logging.getLogger(__name__)
@@ -41,12 +41,12 @@ class FeatureBind(BasePlugin):
Arguments:
features -- The stream features stanza.
"""
- log.debug("Requesting resource: %s", self.xmpp.boundjid.resource)
+ log.debug("Requesting resource: %s", self.xmpp.requested_jid.resource)
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq.enable('bind')
- if self.xmpp.boundjid.resource:
- iq['bind']['resource'] = self.xmpp.boundjid.resource
+ if self.xmpp.requested_jid.resource:
+ iq['bind']['resource'] = self.xmpp.requested_jid.resource
response = iq.send(now=True)
self.xmpp.boundjid = JID(response['bind']['jid'], cache_lock=True)
@@ -56,10 +56,10 @@ class FeatureBind(BasePlugin):
self.xmpp.features.add('bind')
- log.info("Node set to: %s", self.xmpp.boundjid.full)
+ log.info("JID set to: %s", self.xmpp.boundjid.full)
if 'session' not in features['features']:
log.debug("Established Session")
self.xmpp.sessionstarted = True
self.xmpp.session_started_event.set()
- self.xmpp.event("session_start")
+ self.xmpp.event('session_start')
diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py
index b480d5be..1d8f8798 100644
--- a/sleekxmpp/features/feature_mechanisms/mechanisms.py
+++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py
@@ -6,7 +6,6 @@
See the file LICENSE for copying permission.
"""
-import sys
import ssl
import logging
@@ -44,15 +43,16 @@ class FeatureMechanisms(BasePlugin):
}
def plugin_init(self):
- if not self.use_mech and not self.xmpp.requested_jid.user:
- self.use_mech = 'ANONYMOUS'
-
if self.sasl_callback is None:
self.sasl_callback = self._default_credentials
if self.security_callback is None:
self.security_callback = self._default_security
+ creds = self.sasl_callback(set(['username']), set())
+ if not self.use_mech and not creds['username']:
+ self.use_mech = 'ANONYMOUS'
+
self.mech = None
self.mech_list = set()
self.attempted_mechs = set()
@@ -92,27 +92,26 @@ class FeatureMechanisms(BasePlugin):
values = required_values.union(optional_values)
for value in values:
if value == 'username':
- result[value] = self.xmpp.requested_jid.user
- elif value == 'password':
- result[value] = creds['password']
- elif value == 'authzid':
- result[value] = creds.get('authzid', '')
+ result[value] = creds.get('username', self.xmpp.requested_jid.user)
elif value == 'email':
jid = self.xmpp.requested_jid.bare
result[value] = creds.get('email', jid)
elif value == 'channel_binding':
- if sys.version_info >= (3, 3):
+ if hasattr(self.xmpp.socket, 'get_channel_binding'):
result[value] = self.xmpp.socket.get_channel_binding()
else:
+ log.debug("Channel binding not supported.")
+ log.debug("Use Python 3.3+ for channel binding and " + \
+ "SCRAM-SHA-1-PLUS support")
result[value] = None
elif value == 'host':
- result[value] = self.xmpp.requested_jid.domain
+ result[value] = creds.get('host', self.xmpp.requested_jid.domain)
elif value == 'realm':
- result[value] = self.xmpp.requested_jid.domain
+ result[value] = creds.get('realm', self.xmpp.requested_jid.domain)
elif value == 'service-name':
- result[value] = self.xmpp._service_name
+ result[value] = creds.get('service-name', self.xmpp._service_name)
elif value == 'service':
- result[value] = 'xmpp'
+ result[value] = creds.get('service', 'xmpp')
elif value in creds:
result[value] = creds[value]
return result
@@ -174,8 +173,12 @@ class FeatureMechanisms(BasePlugin):
except sasl.SASLNoAppropriateMechanism:
log.error("No appropriate login method.")
self.xmpp.event("no_auth", direct=True)
+ self.xmpp.event("failed_auth", direct=True)
self.attempted_mechs = set()
return self.xmpp.disconnect()
+ except StringPrepError:
+ log.exception("A credential value did not pass SASLprep.")
+ self.xmpp.disconnect()
resp = stanza.Auth(self.xmpp)
resp['mechanism'] = self.mech.name
@@ -184,17 +187,14 @@ class FeatureMechanisms(BasePlugin):
except sasl.SASLCancelled:
self.attempted_mechs.add(self.mech.name)
self._send_auth()
- except sasl.SASLFailed:
- self.attempted_mechs.add(self.mech.name)
- self._send_auth()
except sasl.SASLMutualAuthFailed:
log.error("Mutual authentication failed! " + \
"A security breach is possible.")
self.attempted_mechs.add(self.mech.name)
self.xmpp.disconnect()
- except StringPrepError:
- log.exception("A credential value did not pass SASLprep.")
- self.xmpp.disconnect()
+ except sasl.SASLFailed:
+ self.attempted_mechs.add(self.mech.name)
+ self._send_auth()
else:
resp.send(now=True)
@@ -207,14 +207,16 @@ class FeatureMechanisms(BasePlugin):
resp['value'] = self.mech.process(stanza['value'])
except sasl.SASLCancelled:
self.stanza.Abort(self.xmpp).send()
- except sasl.SASLFailed:
- self.stanza.Abort(self.xmpp).send()
except sasl.SASLMutualAuthFailed:
log.error("Mutual authentication failed! " + \
"A security breach is possible.")
self.attempted_mechs.add(self.mech.name)
self.xmpp.disconnect()
+ except sasl.SASLFailed:
+ self.stanza.Abort(self.xmpp).send()
else:
+ if resp.get_value() == '':
+ resp.del_value()
resp.send(now=True)
def _handle_success(self, stanza):
diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
index 7b665345..6b6f85a3 100644
--- a/sleekxmpp/features/feature_mechanisms/stanza/auth.py
+++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py
@@ -40,7 +40,7 @@ class Auth(StanzaBase):
if not self['mechanism'] in self.plain_mechs:
if values:
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
- else:
+ elif values == b'':
self.xml.text = '='
else:
self.xml.text = bytes(values).decode('utf-8')
diff --git a/sleekxmpp/features/feature_preapproval/preapproval.py b/sleekxmpp/features/feature_preapproval/preapproval.py
index 3823c472..c7106ed3 100644
--- a/sleekxmpp/features/feature_preapproval/preapproval.py
+++ b/sleekxmpp/features/feature_preapproval/preapproval.py
@@ -8,7 +8,7 @@
import logging
-from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.features.feature_preapproval import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins.base import BasePlugin
diff --git a/sleekxmpp/features/feature_rosterver/rosterver.py b/sleekxmpp/features/feature_rosterver/rosterver.py
index 9e0bb8e8..2991f587 100644
--- a/sleekxmpp/features/feature_rosterver/rosterver.py
+++ b/sleekxmpp/features/feature_rosterver/rosterver.py
@@ -8,7 +8,7 @@
import logging
-from sleekxmpp.stanza import Iq, StreamFeatures
+from sleekxmpp.stanza import StreamFeatures
from sleekxmpp.features.feature_rosterver import stanza
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.plugins.base import BasePlugin
diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py
index c799a763..ceadd5f3 100644
--- a/sleekxmpp/features/feature_session/session.py
+++ b/sleekxmpp/features/feature_session/session.py
@@ -51,4 +51,4 @@ class FeatureSession(BasePlugin):
log.debug("Established Session")
self.xmpp.sessionstarted = True
self.xmpp.session_started_event.set()
- self.xmpp.event("session_start")
+ self.xmpp.event('session_start')
diff --git a/sleekxmpp/jid.py b/sleekxmpp/jid.py
index 8f1a81d4..ac5ba30d 100644
--- a/sleekxmpp/jid.py
+++ b/sleekxmpp/jid.py
@@ -19,6 +19,8 @@ import stringprep
import threading
import encodings.idna
+from copy import deepcopy
+
from sleekxmpp.util import stringprep_profiles
from sleekxmpp.thirdparty import OrderedDict
@@ -75,7 +77,7 @@ def _cache(key, parts, locked):
with JID_CACHE_LOCK:
while len(JID_CACHE) > JID_CACHE_MAX_SIZE:
found = None
- for key, item in JID_CACHE.iteritems():
+ for key, item in JID_CACHE.items():
if not item[1]: # if not locked
found = key
break
@@ -202,7 +204,7 @@ def _validate_domain(domain):
socket.inet_pton(socket.AF_INET6, domain.strip('[]'))
domain = '[%s]' % domain.strip('[]')
ip_addr = True
- except socket.error:
+ except (socket.error, ValueError):
pass
if not ip_addr:
@@ -228,7 +230,7 @@ def _validate_domain(domain):
for char in label:
if char in ILLEGAL_CHARS:
- raise InvalidJID('Domain contains illegar characters')
+ raise InvalidJID('Domain contains illegal characters')
if '-' in (label[0], label[-1]):
raise InvalidJID('Domain started or ended with -')
@@ -506,50 +508,100 @@ class JID(object):
"""
self._jid = JID(data)._jid
- # pylint: disable=R0911
- def __getattr__(self, name):
- """Retrieve the given JID component.
+ @property
+ def resource(self):
+ return self._jid[2] or ''
- :param name: one of: user, server, domain, resource,
- full, or bare.
- """
- if name == 'resource':
- return self._jid[2] or ''
- elif name in ('user', 'username', 'local', 'node'):
- return self._jid[0] or ''
- elif name in ('server', 'domain', 'host'):
- return self._jid[1] or ''
- elif name in ('full', 'jid'):
- return _format_jid(*self._jid)
- elif name == 'bare':
- return _format_jid(self._jid[0], self._jid[1])
- elif name == '_jid':
- return getattr(super(JID, self), '_jid')
- else:
- return None
+ @property
+ def user(self):
+ return self._jid[0] or ''
- # pylint: disable=W0212
- def __setattr__(self, name, value):
- """Update the given JID component.
+ @property
+ def local(self):
+ return self._jid[0] or ''
+
+ @property
+ def node(self):
+ return self._jid[0] or ''
+
+ @property
+ def username(self):
+ return self._jid[0] or ''
+
+ @property
+ def bare(self):
+ return _format_jid(self._jid[0], self._jid[1])
+
+ @property
+ def server(self):
+ return self._jid[1] or ''
+
+ @property
+ def domain(self):
+ return self._jid[1] or ''
+
+ @property
+ def host(self):
+ return self._jid[1] or ''
+
+ @property
+ def full(self):
+ return _format_jid(*self._jid)
+
+ @property
+ def jid(self):
+ return _format_jid(*self._jid)
+
+ @property
+ def bare(self):
+ return _format_jid(self._jid[0], self._jid[1])
+
+
+ @resource.setter
+ def resource(self, value):
+ self._jid = JID(self, resource=value)._jid
+
+ @user.setter
+ def user(self, value):
+ self._jid = JID(self, local=value)._jid
+
+ @username.setter
+ def username(self, value):
+ self._jid = JID(self, local=value)._jid
+
+ @local.setter
+ def local(self, value):
+ self._jid = JID(self, local=value)._jid
+
+ @node.setter
+ def node(self, value):
+ self._jid = JID(self, local=value)._jid
+
+ @server.setter
+ def server(self, value):
+ self._jid = JID(self, domain=value)._jid
+
+ @domain.setter
+ def domain(self, value):
+ self._jid = JID(self, domain=value)._jid
+
+ @host.setter
+ def host(self, value):
+ self._jid = JID(self, domain=value)._jid
+
+ @full.setter
+ def full(self, value):
+ self._jid = JID(value)._jid
+
+ @jid.setter
+ def jid(self, value):
+ self._jid = JID(value)._jid
+
+ @bare.setter
+ def bare(self, value):
+ parsed = JID(value)._jid
+ self._jid = (parsed[0], parsed[1], self._jid[2])
- :param name: one of: ``user``, ``username``, ``local``,
- ``node``, ``server``, ``domain``, ``host``,
- ``resource``, ``full``, ``jid``, or ``bare``.
- :param value: The new string value of the JID component.
- """
- if name == '_jid':
- super(JID, self).__setattr__('_jid', value)
- elif name == 'resource':
- self._jid = JID(self, resource=value)._jid
- elif name in ('user', 'username', 'local', 'node'):
- self._jid = JID(self, local=value)._jid
- elif name in ('server', 'domain', 'host'):
- self._jid = JID(self, domain=value)._jid
- elif name in ('full', 'jid'):
- self._jid = JID(value)._jid
- elif name == 'bare':
- parsed = JID(value)._jid
- self._jid = (parsed[0], parsed[1], self._jid[2])
def __str__(self):
"""Use the full JID as the string value."""
@@ -580,3 +632,7 @@ class JID(object):
def __copy__(self):
"""Generate a duplicate JID."""
return JID(self)
+
+ def __deepcopy__(self, memo):
+ """Generate a duplicate JID."""
+ return JID(deepcopy(str(self), memo))
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
index c6617a87..951f31eb 100644
--- a/sleekxmpp/plugins/__init__.py
+++ b/sleekxmpp/plugins/__init__.py
@@ -11,20 +11,19 @@ from sleekxmpp.plugins.base import register_plugin, load_plugin
__all__ = [
- # Non-standard
- 'gmail_notify', # Gmail searching and notifications
-
# XEPS
'xep_0004', # Data Forms
'xep_0009', # Jabber-RPC
'xep_0012', # Last Activity
'xep_0013', # Flexible Offline Message Retrieval
'xep_0016', # Privacy Lists
+ 'xep_0020', # Feature Negotiation
'xep_0027', # Current Jabber OpenPGP Usage
'xep_0030', # Service Discovery
'xep_0033', # Extended Stanza Addresses
'xep_0045', # Multi-User Chat (Client)
'xep_0047', # In-Band Bytestreams
+ 'xep_0048', # Bookmarks
'xep_0049', # Private XML Storage
'xep_0050', # Ad-hoc Commands
'xep_0054', # vcard-temp
@@ -32,8 +31,10 @@ __all__ = [
'xep_0060', # Pubsub (Client)
'xep_0065', # SOCKS5 Bytestreams
'xep_0066', # Out of Band Data
+ 'xep_0071', # XHTML-IM
'xep_0077', # In-Band Registration
# 'xep_0078', # Non-SASL auth. Don't automatically load
+ 'xep_0079', # Advanced Message Processing
'xep_0080', # User Location
'xep_0082', # XMPP Date and Time Profiles
'xep_0084', # User Avatar
@@ -49,12 +50,14 @@ __all__ = [
'xep_0128', # Extended Service Discovery
'xep_0131', # Standard Headers and Internet Metadata
'xep_0133', # Service Administration
+ 'xep_0152', # Reachability Addresses
'xep_0153', # vCard-Based Avatars
'xep_0163', # Personal Eventing Protocol
'xep_0172', # User Nickname
'xep_0184', # Message Receipts
'xep_0186', # Invisible Command
'xep_0191', # Blocking Command
+ 'xep_0196', # User Gaming
'xep_0198', # Stream Management
'xep_0199', # Ping
'xep_0202', # Entity Time
@@ -77,4 +80,7 @@ __all__ = [
'xep_0302', # XMPP Compliance Suites 2012
'xep_0308', # Last Message Correction
'xep_0313', # Message Archive Management
+ 'xep_0319', # Last User Interaction in Presence
+ 'xep_0323', # IoT Systems Sensor Data
+ 'xep_0325', # IoT Systems Control
]
diff --git a/sleekxmpp/plugins/google/__init__.py b/sleekxmpp/plugins/google/__init__.py
new file mode 100644
index 00000000..bd7ca123
--- /dev/null
+++ b/sleekxmpp/plugins/google/__init__.py
@@ -0,0 +1,47 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin, BasePlugin
+
+from sleekxmpp.plugins.google.gmail import Gmail
+from sleekxmpp.plugins.google.auth import GoogleAuth
+from sleekxmpp.plugins.google.settings import GoogleSettings
+from sleekxmpp.plugins.google.nosave import GoogleNoSave
+
+
+class Google(BasePlugin):
+
+ """
+ Google: Custom GTalk Features
+
+ Also see: <https://developers.google.com/talk/jep_extensions/extensions>
+ """
+
+ name = 'google'
+ description = 'Google: Custom GTalk Features'
+ dependencies = set([
+ 'gmail',
+ 'google_settings',
+ 'google_nosave',
+ 'google_auth'
+ ])
+
+ def __getitem__(self, attr):
+ if attr in ('settings', 'nosave', 'auth'):
+ return self.xmpp['google_%s' % attr]
+ elif attr == 'gmail':
+ return self.xmpp['gmail']
+ else:
+ raise KeyError(attr)
+
+
+register_plugin(Gmail)
+register_plugin(GoogleAuth)
+register_plugin(GoogleSettings)
+register_plugin(GoogleNoSave)
+register_plugin(Google)
diff --git a/sleekxmpp/plugins/google/auth/__init__.py b/sleekxmpp/plugins/google/auth/__init__.py
new file mode 100644
index 00000000..5a8feb0d
--- /dev/null
+++ b/sleekxmpp/plugins/google/auth/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.google.auth import stanza
+from sleekxmpp.plugins.google.auth.auth import GoogleAuth
diff --git a/sleekxmpp/plugins/google/auth/auth.py b/sleekxmpp/plugins/google/auth/auth.py
new file mode 100644
index 00000000..042bd404
--- /dev/null
+++ b/sleekxmpp/plugins/google/auth/auth.py
@@ -0,0 +1,52 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.google.auth import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class GoogleAuth(BasePlugin):
+
+ """
+ Google: Auth Extensions (JID Domain Discovery, OAuth2)
+
+ Also see:
+ <https://developers.google.com/talk/jep_extensions/jid_domain_change>
+ <https://developers.google.com/talk/jep_extensions/oauth>
+ """
+
+ name = 'google_auth'
+ description = 'Google: Auth Extensions (JID Domain Discovery, OAuth2)'
+ dependencies = set(['feature_mechanisms'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.namespace_map['http://www.google.com/talk/protocol/auth'] = 'ga'
+
+ register_stanza_plugin(self.xmpp['feature_mechanisms'].stanza.Auth,
+ stanza.GoogleAuth)
+
+ self.xmpp.add_filter('out', self._auth)
+
+ def plugin_end(self):
+ self.xmpp.del_filter('out', self._auth)
+
+ def _auth(self, stanza):
+ if isinstance(stanza, self.xmpp['feature_mechanisms'].stanza.Auth):
+ stanza.stream = self.xmpp
+ stanza['google']['client_uses_full_bind_result'] = True
+ if stanza['mechanism'] == 'X-OAUTH2':
+ stanza['google']['service'] = 'oauth2'
+ print(stanza)
+ return stanza
diff --git a/sleekxmpp/plugins/google/auth/stanza.py b/sleekxmpp/plugins/google/auth/stanza.py
new file mode 100644
index 00000000..2d13f85a
--- /dev/null
+++ b/sleekxmpp/plugins/google/auth/stanza.py
@@ -0,0 +1,49 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class GoogleAuth(ElementBase):
+ name = 'auth'
+ namespace = 'http://www.google.com/talk/protocol/auth'
+ plugin_attrib = 'google'
+ interfaces = set(['client_uses_full_bind_result', 'service'])
+
+ discovery_attr= '{%s}client-uses-full-bind-result' % namespace
+ service_attr= '{%s}service' % namespace
+
+ def setup(self, xml):
+ """Don't create XML for the plugin."""
+ self.xml = ET.Element('')
+ print('setting up google extension')
+
+ def get_client_uses_full_bind_result(self):
+ return self.parent()._get_attr(self.discovery_attr) == 'true'
+
+ def set_client_uses_full_bind_result(self, value):
+ print('>>>', value)
+ if value in (True, 'true'):
+ self.parent()._set_attr(self.discovery_attr, 'true')
+ else:
+ self.parent()._del_attr(self.discovery_attr)
+
+ def del_client_uses_full_bind_result(self):
+ self.parent()._del_attr(self.discovery_attr)
+
+ def get_service(self):
+ return self.parent()._get_attr(self.service_attr, '')
+
+ def set_service(self, value):
+ if value:
+ self.parent()._set_attr(self.service_attr, value)
+ else:
+ self.parent()._del_attr(self.service_attr)
+
+ def del_service(self):
+ self.parent()._del_attr(self.service_attr)
diff --git a/sleekxmpp/plugins/google/gmail/__init__.py b/sleekxmpp/plugins/google/gmail/__init__.py
new file mode 100644
index 00000000..a92e363b
--- /dev/null
+++ b/sleekxmpp/plugins/google/gmail/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.google.gmail import stanza
+from sleekxmpp.plugins.google.gmail.notifications import Gmail
diff --git a/sleekxmpp/plugins/google/gmail/notifications.py b/sleekxmpp/plugins/google/gmail/notifications.py
new file mode 100644
index 00000000..e65b2ca7
--- /dev/null
+++ b/sleekxmpp/plugins/google/gmail/notifications.py
@@ -0,0 +1,96 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Iq
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.google.gmail import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class Gmail(BasePlugin):
+
+ """
+ Google: Gmail Notifications
+
+ Also see <https://developers.google.com/talk/jep_extensions/gmail>.
+ """
+
+ name = 'gmail'
+ description = 'Google: Gmail Notifications'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.GmailQuery)
+ register_stanza_plugin(Iq, stanza.MailBox)
+ register_stanza_plugin(Iq, stanza.NewMail)
+
+ self.xmpp.register_handler(
+ Callback('Gmail New Mail',
+ MatchXPath('{%s}iq/{%s}%s' % (
+ self.xmpp.default_ns,
+ stanza.NewMail.namespace,
+ stanza.NewMail.name)),
+ self._handle_new_mail))
+
+ self._last_result_time = None
+ self._last_result_tid = None
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Gmail New Mail')
+
+ def _handle_new_mail(self, iq):
+ log.info('Gmail: New email!')
+ iq.reply().send()
+ self.xmpp.event('gmail_notification')
+
+ def check(self, block=True, timeout=None, callback=None):
+ last_time = self._last_result_time
+ last_tid = self._last_result_tid
+
+ if not block:
+ callback = lambda iq: self._update_last_results(iq, callback)
+
+ resp = self.search(newer_time=last_time,
+ newer_tid=last_tid,
+ block=block,
+ timeout=timeout,
+ callback=callback)
+
+ if block:
+ self._update_last_results(resp)
+ return resp
+
+ def _update_last_results(self, iq, callback=None):
+ self._last_result_time = iq['gmail_messages']['result_time']
+ threads = iq['gmail_messages']['threads']
+ if threads:
+ self._last_result_tid = threads[0]['tid']
+ if callback:
+ callback(iq)
+
+ def search(self, query=None, newer_time=None, newer_tid=None, block=True,
+ timeout=None, callback=None):
+ if not query:
+ log.info('Gmail: Checking for new email')
+ else:
+ log.info('Gmail: Searching for emails matching: "%s"', query)
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = self.xmpp.boundjid.bare
+ iq['gmail']['search'] = query
+ iq['gmail']['newer_than_time'] = newer_time
+ iq['gmail']['newer_than_tid'] = newer_tid
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/google/gmail/stanza.py b/sleekxmpp/plugins/google/gmail/stanza.py
new file mode 100644
index 00000000..e7e308e1
--- /dev/null
+++ b/sleekxmpp/plugins/google/gmail/stanza.py
@@ -0,0 +1,101 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class GmailQuery(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'query'
+ plugin_attrib = 'gmail'
+ interfaces = set(['newer_than_time', 'newer_than_tid', 'search'])
+
+ def get_search(self):
+ return self._get_attr('q', '')
+
+ def set_search(self, search):
+ self._set_attr('q', search)
+
+ def del_search(self):
+ self._del_attr('q')
+
+ def get_newer_than_time(self):
+ return self._get_attr('newer-than-time', '')
+
+ def set_newer_than_time(self, value):
+ self._set_attr('newer-than-time', value)
+
+ def del_newer_than_time(self):
+ self._del_attr('newer-than-time')
+
+ def get_newer_than_tid(self):
+ return self._get_attr('newer-than-tid', '')
+
+ def set_newer_than_tid(self, value):
+ self._set_attr('newer-than-tid', value)
+
+ def del_newer_than_tid(self):
+ self._del_attr('newer-than-tid')
+
+
+class MailBox(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mailbox'
+ plugin_attrib = 'gmail_messages'
+ interfaces = set(['result_time', 'url', 'matched', 'estimate'])
+
+ def get_matched(self):
+ return self._get_attr('total-matched', '')
+
+ def get_estimate(self):
+ return self._get_attr('total-estimate', '') == '1'
+
+ def get_result_time(self):
+ return self._get_attr('result-time', '')
+
+
+class MailThread(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'mail-thread-info'
+ plugin_attrib = 'thread'
+ plugin_multi_attrib = 'threads'
+ interfaces = set(['tid', 'participation', 'messages', 'date',
+ 'senders', 'url', 'labels', 'subject', 'snippet'])
+ sub_interfaces = set(['labels', 'subject', 'snippet'])
+
+ def get_senders(self):
+ result = []
+ senders = self.xml.findall('{%s}senders/{%s}sender' % (
+ self.namespace, self.namespace))
+
+ for sender in senders:
+ result.append(MailSender(xml=sender))
+
+ return result
+
+
+class MailSender(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'sender'
+ plugin_attrib = name
+ interfaces = set(['address', 'name', 'originator', 'unread'])
+
+ def get_originator(self):
+ return self.xml.attrib.get('originator', '0') == '1'
+
+ def get_unread(self):
+ return self.xml.attrib.get('unread', '0') == '1'
+
+
+class NewMail(ElementBase):
+ namespace = 'google:mail:notify'
+ name = 'new-mail'
+ plugin_attrib = 'gmail_notification'
+
+
+register_stanza_plugin(MailBox, MailThread, iterable=True)
diff --git a/sleekxmpp/plugins/google/nosave/__init__.py b/sleekxmpp/plugins/google/nosave/__init__.py
new file mode 100644
index 00000000..57847af5
--- /dev/null
+++ b/sleekxmpp/plugins/google/nosave/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.google.nosave import stanza
+from sleekxmpp.plugins.google.nosave.nosave import GoogleNoSave
diff --git a/sleekxmpp/plugins/google/nosave/nosave.py b/sleekxmpp/plugins/google/nosave/nosave.py
new file mode 100644
index 00000000..d6bef615
--- /dev/null
+++ b/sleekxmpp/plugins/google/nosave/nosave.py
@@ -0,0 +1,83 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.stanza import Iq, Message
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.google.nosave import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class GoogleNoSave(BasePlugin):
+
+ """
+ Google: Off the Record Chats
+
+ NOTE: This is NOT an encryption method.
+
+ Also see <https://developers.google.com/talk/jep_extensions/otr>.
+ """
+
+ name = 'google_nosave'
+ description = 'Google: Off the Record Chats'
+ dependencies = set(['google_settings'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, stanza.NoSave)
+ register_stanza_plugin(Iq, stanza.NoSaveQuery)
+
+ self.xmpp.register_handler(
+ Callback('Google Nosave',
+ StanzaPath('iq@type=set/google_nosave'),
+ self._handle_nosave_change))
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Google Nosave')
+
+ def enable(self, jid=None, block=True, timeout=None, callback=None):
+ if jid is None:
+ self.xmpp['google_settings'].update({'archiving_enabled': False},
+ block=block, timeout=timeout, callback=callback)
+ else:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['google_nosave']['item']['jid'] = jid
+ iq['google_nosave']['item']['value'] = True
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def disable(self, jid=None, block=True, timeout=None, callback=None):
+ if jid is None:
+ self.xmpp['google_settings'].update({'archiving_enabled': True},
+ block=block, timeout=timeout, callback=callback)
+ else:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['google_nosave']['item']['jid'] = jid
+ iq['google_nosave']['item']['value'] = False
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('google_nosave')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def _handle_nosave_change(self, iq):
+ reply = self.xmpp.Iq()
+ reply['type'] = 'result'
+ reply['id'] = iq['id']
+ reply['to'] = iq['from']
+ reply.send()
+ self.xmpp.event('google_nosave_change', iq)
diff --git a/sleekxmpp/plugins/google/nosave/stanza.py b/sleekxmpp/plugins/google/nosave/stanza.py
new file mode 100644
index 00000000..791d4b0c
--- /dev/null
+++ b/sleekxmpp/plugins/google/nosave/stanza.py
@@ -0,0 +1,59 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.jid import JID
+from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class NoSave(ElementBase):
+ name = 'x'
+ namespace = 'google:nosave'
+ plugin_attrib = 'google_nosave'
+ interfaces = set(['value'])
+
+ def get_value(self):
+ return self._get_attr('value', '') == 'enabled'
+
+ def set_value(self, value):
+ self._set_attr('value', 'enabled' if value else 'disabled')
+
+
+class NoSaveQuery(ElementBase):
+ name = 'query'
+ namespace = 'google:nosave'
+ plugin_attrib = 'google_nosave'
+ interfaces = set()
+
+
+class Item(ElementBase):
+ name = 'item'
+ namespace = 'google:nosave'
+ plugin_attrib = 'item'
+ plugin_multi_attrib = 'items'
+ interfaces = set(['jid', 'source', 'value'])
+
+ def get_value(self):
+ return self._get_attr('value', '') == 'enabled'
+
+ def set_value(self, value):
+ self._set_attr('value', 'enabled' if value else 'disabled')
+
+ def get_jid(self):
+ return JID(self._get_attr('jid', ''))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+ def get_source(self):
+ return JID(self._get_attr('source', ''))
+
+ def set_source(self, value):
+ self._set_attr('source', str(value))
+
+
+register_stanza_plugin(NoSaveQuery, Item)
diff --git a/sleekxmpp/plugins/google/settings/__init__.py b/sleekxmpp/plugins/google/settings/__init__.py
new file mode 100644
index 00000000..c3a0471d
--- /dev/null
+++ b/sleekxmpp/plugins/google/settings/__init__.py
@@ -0,0 +1,10 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.google.settings import stanza
+from sleekxmpp.plugins.google.settings.settings import GoogleSettings
diff --git a/sleekxmpp/plugins/google/settings/settings.py b/sleekxmpp/plugins/google/settings/settings.py
new file mode 100644
index 00000000..591956fc
--- /dev/null
+++ b/sleekxmpp/plugins/google/settings/settings.py
@@ -0,0 +1,63 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Iq
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.google.settings import stanza
+
+
+class GoogleSettings(BasePlugin):
+
+ """
+ Google: Gmail Notifications
+
+ Also see <https://developers.google.com/talk/jep_extensions/usersettings>.
+ """
+
+ name = 'google_settings'
+ description = 'Google: User Settings'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.UserSettings)
+
+ self.xmpp.register_handler(
+ Callback('Google Settings',
+ StanzaPath('iq@type=set/google_settings'),
+ self._handle_settings_change))
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Google Settings')
+
+ def get(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('google_settings')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def update(self, settings, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq.enable('google_settings')
+
+ for setting, value in settings.items():
+ iq['google_settings'][setting] = value
+
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def _handle_settings_change(self, iq):
+ reply = self.xmpp.Iq()
+ reply['type'] = 'result'
+ reply['id'] = iq['id']
+ reply['to'] = iq['from']
+ reply.send()
+ self.xmpp.event('google_settings_change', iq)
diff --git a/sleekxmpp/plugins/google/settings/stanza.py b/sleekxmpp/plugins/google/settings/stanza.py
new file mode 100644
index 00000000..d8161770
--- /dev/null
+++ b/sleekxmpp/plugins/google/settings/stanza.py
@@ -0,0 +1,110 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class UserSettings(ElementBase):
+ name = 'usersetting'
+ namespace = 'google:setting'
+ plugin_attrib = 'google_settings'
+ interfaces = set(['auto_accept_suggestions',
+ 'mail_notifications',
+ 'archiving_enabled',
+ 'gmail',
+ 'email_verified',
+ 'domain_privacy_notice',
+ 'display_name'])
+
+ def _get_setting(self, setting):
+ xml = self.xml.find('{%s}%s' % (self.namespace, setting))
+ if xml is not None:
+ return xml.attrib.get('value', '') == 'true'
+ return False
+
+ def _set_setting(self, setting, value):
+ self._del_setting(setting)
+ if value in (True, False):
+ xml = ET.Element('{%s}%s' % (self.namespace, setting))
+ xml.attrib['value'] = 'true' if value else 'false'
+ self.xml.append(xml)
+
+ def _del_setting(self, setting):
+ xml = self.xml.find('{%s}%s' % (self.namespace, setting))
+ if xml is not None:
+ self.xml.remove(xml)
+
+ def get_display_name(self):
+ xml = self.xml.find('{%s}%s' % (self.namespace, 'displayname'))
+ if xml is not None:
+ return xml.attrib.get('value', '')
+ return ''
+
+ def set_display_name(self, value):
+ self._del_setting(setting)
+ if value:
+ xml = ET.Element('{%s}%s' % (self.namespace, 'displayname'))
+ xml.attrib['value'] = value
+ self.xml.append(xml)
+
+ def del_display_name(self):
+ self._del_setting('displayname')
+
+ def get_auto_accept_suggestions(self):
+ return self._get_setting('autoacceptsuggestions')
+
+ def get_mail_notifications(self):
+ return self._get_setting('mailnotifications')
+
+ def get_archiving_enabled(self):
+ return self._get_setting('archivingenabled')
+
+ def get_gmail(self):
+ return self._get_setting('gmail')
+
+ def get_email_verified(self):
+ return self._get_setting('emailverified')
+
+ def get_domain_privacy_notice(self):
+ return self._get_setting('domainprivacynotice')
+
+ def set_auto_accept_suggestions(self, value):
+ self._set_setting('autoacceptsuggestions', value)
+
+ def set_mail_notifications(self, value):
+ self._set_setting('mailnotifications', value)
+
+ def set_archiving_enabled(self, value):
+ self._set_setting('archivingenabled', value)
+
+ def set_gmail(self, value):
+ self._set_setting('gmail', value)
+
+ def set_email_verified(self, value):
+ self._set_setting('emailverified', value)
+
+ def set_domain_privacy_notice(self, value):
+ self._set_setting('domainprivacynotice', value)
+
+ def del_auto_accept_suggestions(self):
+ self._del_setting('autoacceptsuggestions')
+
+ def del_mail_notifications(self):
+ self._del_setting('mailnotifications')
+
+ def del_archiving_enabled(self):
+ self._del_setting('archivingenabled')
+
+ def del_gmail(self):
+ self._del_setting('gmail')
+
+ def del_email_verified(self):
+ self._del_setting('emailverified')
+
+ def del_domain_privacy_notice(self):
+ self._del_setting('domainprivacynotice')
diff --git a/sleekxmpp/plugins/jobs.py b/sleekxmpp/plugins/jobs.py
deleted file mode 100644
index cb9deba8..00000000
--- a/sleekxmpp/plugins/jobs.py
+++ /dev/null
@@ -1,49 +0,0 @@
-from . import base
-import logging
-from xml.etree import cElementTree as ET
-
-
-log = logging.getLogger(__name__)
-
-
-class jobs(base.base_plugin):
- def plugin_init(self):
- self.xep = 'pubsubjob'
- self.description = "Job distribution over Pubsub"
-
- def post_init(self):
- pass
- #TODO add event
-
- def createJobNode(self, host, jid, node, config=None):
- pass
-
- def createJob(self, host, node, jobid=None, payload=None):
- return self.xmpp.plugin['xep_0060'].setItem(host, node, ((jobid, payload),))
-
- def claimJob(self, host, node, jobid, ifrom=None):
- return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}claimed'))
-
- def unclaimJob(self, host, node, jobid):
- return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}unclaimed'))
-
- def finishJob(self, host, node, jobid, payload=None):
- finished = ET.Element('{http://andyet.net/protocol/pubsubjob}finished')
- if payload is not None:
- finished.append(payload)
- return self._setState(host, node, jobid, finished)
-
- def _setState(self, host, node, jobid, state, ifrom=None):
- iq = self.xmpp.Iq()
- iq['to'] = host
- if ifrom: iq['from'] = ifrom
- iq['type'] = 'set'
- iq['psstate']['node'] = node
- iq['psstate']['item'] = jobid
- iq['psstate']['payload'] = state
- result = iq.send()
- if result is None or type(result) == bool or result['type'] != 'result':
- log.error("Unable to change %s:%s to %s", node, jobid, state)
- return False
- return True
-
diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py
deleted file mode 100644
index 7f086866..00000000
--- a/sleekxmpp/plugins/old_0004.py
+++ /dev/null
@@ -1,421 +0,0 @@
-"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- See the file LICENSE for copying permission.
-"""
-from . import base
-import logging
-from xml.etree import cElementTree as ET
-import copy
-import logging
-#TODO support item groups and results
-
-
-log = logging.getLogger(__name__)
-
-
-class old_0004(base.base_plugin):
-
- def plugin_init(self):
- self.xep = '0004'
- self.description = '*Deprecated Data Forms'
- self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform, name='Old Message Form')
-
- def post_init(self):
- base.base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
- log.warning("This implementation of XEP-0004 is deprecated.")
-
- def handler_message_xform(self, xml):
- object = self.handle_form(xml)
- self.xmpp.event("message_form", object)
-
- def handler_presence_xform(self, xml):
- object = self.handle_form(xml)
- self.xmpp.event("presence_form", object)
-
- def handle_form(self, xml):
- xmlform = xml.find('{jabber:x:data}x')
- object = self.buildForm(xmlform)
- self.xmpp.event("message_xform", object)
- return object
-
- def buildForm(self, xml):
- form = Form(ftype=xml.attrib['type'])
- form.fromXML(xml)
- return form
-
- def makeForm(self, ftype='form', title='', instructions=''):
- return Form(self.xmpp, ftype, title, instructions)
-
-class FieldContainer(object):
- def __init__(self, stanza = 'form'):
- self.fields = []
- self.field = {}
- self.stanza = stanza
-
- def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None):
- self.field[var] = FormField(var, ftype, label, desc, required, value)
- self.fields.append(self.field[var])
- return self.field[var]
-
- def buildField(self, xml):
- self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single'))
- self.fields.append(self.field[xml.get('var', '__unnamed__')])
- self.field[xml.get('var', '__unnamed__')].buildField(xml)
-
- def buildContainer(self, xml):
- self.stanza = xml.tag
- for field in xml.findall('{jabber:x:data}field'):
- self.buildField(field)
-
- def getXML(self, ftype):
- container = ET.Element(self.stanza)
- for field in self.fields:
- container.append(field.getXML(ftype))
- return container
-
-class Form(FieldContainer):
- types = ('form', 'submit', 'cancel', 'result')
- def __init__(self, xmpp=None, ftype='form', title='', instructions=''):
- if not ftype in self.types:
- raise ValueError("Invalid Form Type")
- FieldContainer.__init__(self)
- self.xmpp = xmpp
- self.type = ftype
- self.title = title
- self.instructions = instructions
- self.reported = []
- self.items = []
-
- def merge(self, form2):
- form1 = Form(ftype=self.type)
- form1.fromXML(self.getXML(self.type))
- for field in form2.fields:
- if not field.var in form1.field:
- form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value)
- else:
- form1.field[field.var].value = field.value
- for option, label in field.options:
- if (option, label) not in form1.field[field.var].options:
- form1.fields[field.var].addOption(option, label)
- return form1
-
- def copy(self):
- newform = Form(ftype=self.type)
- newform.fromXML(self.getXML(self.type))
- return newform
-
- def update(self, form):
- values = form.getValues()
- for var in values:
- if var in self.fields:
- self.fields[var].setValue(self.fields[var])
-
- def getValues(self):
- result = {}
- for field in self.fields:
- value = field.value
- if len(value) == 1:
- value = value[0]
- result[field.var] = value
- return result
-
- def setValues(self, values={}):
- for field in values:
- if field in self.field:
- if isinstance(values[field], list) or isinstance(values[field], tuple):
- for value in values[field]:
- self.field[field].setValue(value)
- else:
- self.field[field].setValue(values[field])
-
- def fromXML(self, xml):
- self.buildForm(xml)
-
- def addItem(self):
- newitem = FieldContainer('item')
- self.items.append(newitem)
- return newitem
-
- def buildItem(self, xml):
- newitem = self.addItem()
- newitem.buildContainer(xml)
-
- def addReported(self):
- reported = FieldContainer('reported')
- self.reported.append(reported)
- return reported
-
- def buildReported(self, xml):
- reported = self.addReported()
- reported.buildContainer(xml)
-
- def setTitle(self, title):
- self.title = title
-
- def setInstructions(self, instructions):
- self.instructions = instructions
-
- def setType(self, ftype):
- self.type = ftype
-
- def getXMLMessage(self, to):
- msg = self.xmpp.makeMessage(to)
- msg.append(self.getXML())
- return msg
-
- def buildForm(self, xml):
- self.type = xml.get('type', 'form')
- if xml.find('{jabber:x:data}title') is not None:
- self.setTitle(xml.find('{jabber:x:data}title').text)
- if xml.find('{jabber:x:data}instructions') is not None:
- self.setInstructions(xml.find('{jabber:x:data}instructions').text)
- for field in xml.findall('{jabber:x:data}field'):
- self.buildField(field)
- for reported in xml.findall('{jabber:x:data}reported'):
- self.buildReported(reported)
- for item in xml.findall('{jabber:x:data}item'):
- self.buildItem(item)
-
- #def getXML(self, tostring = False):
- def getXML(self, ftype=None):
- if ftype:
- self.type = ftype
- form = ET.Element('{jabber:x:data}x')
- form.attrib['type'] = self.type
- if self.title and self.type in ('form', 'result'):
- title = ET.Element('{jabber:x:data}title')
- title.text = self.title
- form.append(title)
- if self.instructions and self.type == 'form':
- instructions = ET.Element('{jabber:x:data}instructions')
- instructions.text = self.instructions
- form.append(instructions)
- for field in self.fields:
- form.append(field.getXML(self.type))
- for reported in self.reported:
- form.append(reported.getXML('{jabber:x:data}reported'))
- for item in self.items:
- form.append(item.getXML(self.type))
- #if tostring:
- # form = self.xmpp.tostring(form)
- return form
-
- def getXHTML(self):
- form = ET.Element('{http://www.w3.org/1999/xhtml}form')
- if self.title:
- title = ET.Element('h2')
- title.text = self.title
- form.append(title)
- if self.instructions:
- instructions = ET.Element('p')
- instructions.text = self.instructions
- form.append(instructions)
- for field in self.fields:
- form.append(field.getXHTML())
- for field in self.reported:
- form.append(field.getXHTML())
- for field in self.items:
- form.append(field.getXHTML())
- return form
-
-
- def makeSubmit(self):
- self.setType('submit')
-
-class FormField(object):
- types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single')
- listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single')
- lbtypes = ('fixed', 'text-multi')
- def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None):
- if not ftype in self.types:
- raise ValueError("Invalid Field Type")
- self.type = ftype
- self.var = var
- self.label = label
- self.desc = desc
- self.options = []
- self.required = False
- self.value = []
- if self.type in self.listtypes:
- self.islist = True
- else:
- self.islist = False
- if self.type in self.lbtypes:
- self.islinebreak = True
- else:
- self.islinebreak = False
- if value:
- self.setValue(value)
-
- def addOption(self, value, label):
- if self.islist:
- self.options.append((value, label))
- else:
- raise ValueError("Cannot add options to non-list type field.")
-
- def setTrue(self):
- if self.type == 'boolean':
- self.value = [True]
-
- def setFalse(self):
- if self.type == 'boolean':
- self.value = [False]
-
- def require(self):
- self.required = True
-
- def setDescription(self, desc):
- self.desc = desc
-
- def setValue(self, value):
- if self.type == 'boolean':
- if value in ('1', 1, True, 'true', 'True', 'yes'):
- value = True
- else:
- value = False
- if self.islinebreak and value is not None:
- self.value += value.split('\n')
- else:
- if len(self.value) and (not self.islist or self.type == 'list-single'):
- self.value = [value]
- else:
- self.value.append(value)
-
- def delValue(self, value):
- if type(self.value) == type([]):
- try:
- idx = self.value.index(value)
- if idx != -1:
- self.value.pop(idx)
- except ValueError:
- pass
- else:
- self.value = ''
-
- def setAnswer(self, value):
- self.setValue(value)
-
- def buildField(self, xml):
- self.type = xml.get('type', 'text-single')
- self.label = xml.get('label', '')
- for option in xml.findall('{jabber:x:data}option'):
- self.addOption(option.find('{jabber:x:data}value').text, option.get('label', ''))
- for value in xml.findall('{jabber:x:data}value'):
- self.setValue(value.text)
- if xml.find('{jabber:x:data}required') is not None:
- self.require()
- if xml.find('{jabber:x:data}desc') is not None:
- self.setDescription(xml.find('{jabber:x:data}desc').text)
-
- def getXML(self, ftype):
- field = ET.Element('{jabber:x:data}field')
- if ftype != 'result':
- field.attrib['type'] = self.type
- if self.type != 'fixed':
- if self.var:
- field.attrib['var'] = self.var
- if self.label:
- field.attrib['label'] = self.label
- if ftype == 'form':
- for option in self.options:
- optionxml = ET.Element('{jabber:x:data}option')
- optionxml.attrib['label'] = option[1]
- optionval = ET.Element('{jabber:x:data}value')
- optionval.text = option[0]
- optionxml.append(optionval)
- field.append(optionxml)
- if self.required:
- required = ET.Element('{jabber:x:data}required')
- field.append(required)
- if self.desc:
- desc = ET.Element('{jabber:x:data}desc')
- desc.text = self.desc
- field.append(desc)
- for value in self.value:
- valuexml = ET.Element('{jabber:x:data}value')
- if value is True or value is False:
- if value:
- valuexml.text = '1'
- else:
- valuexml.text = '0'
- else:
- valuexml.text = value
- field.append(valuexml)
- return field
-
- def getXHTML(self):
- field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type})
- if self.label:
- label = ET.Element('p')
- label.text = "%s: " % self.label
- else:
- label = ET.Element('p')
- label.text = "%s: " % self.var
- field.append(label)
- if self.type == 'boolean':
- formf = ET.Element('input', {'type': 'checkbox', 'name': self.var})
- if len(self.value) and self.value[0] in (True, 'true', '1'):
- formf.attrib['checked'] = 'checked'
- elif self.type == 'fixed':
- formf = ET.Element('p')
- try:
- formf.text = ', '.join(self.value)
- except:
- pass
- field.append(formf)
- formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
- try:
- formf.text = ', '.join(self.value)
- except:
- pass
- elif self.type == 'hidden':
- formf = ET.Element('input', {'type': 'hidden', 'name': self.var})
- try:
- formf.text = ', '.join(self.value)
- except:
- pass
- elif self.type in ('jid-multi', 'list-multi'):
- formf = ET.Element('select', {'name': self.var})
- for option in self.options:
- optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'})
- optf.text = option[1]
- if option[1] in self.value:
- optf.attrib['selected'] = 'selected'
- formf.append(option)
- elif self.type in ('jid-single', 'text-single'):
- formf = ET.Element('input', {'type': 'text', 'name': self.var})
- try:
- formf.attrib['value'] = ', '.join(self.value)
- except:
- pass
- elif self.type == 'list-single':
- formf = ET.Element('select', {'name': self.var})
- for option in self.options:
- optf = ET.Element('option', {'value': option[0]})
- optf.text = option[1]
- if not optf.text:
- optf.text = option[0]
- if option[1] in self.value:
- optf.attrib['selected'] = 'selected'
- formf.append(optf)
- elif self.type == 'text-multi':
- formf = ET.Element('textarea', {'name': self.var})
- try:
- formf.text = ', '.join(self.value)
- except:
- pass
- if not formf.text:
- formf.text = ' '
- elif self.type == 'text-private':
- formf = ET.Element('input', {'type': 'password', 'name': self.var})
- try:
- formf.attrib['value'] = ', '.join(self.value)
- except:
- pass
- label.append(formf)
- return field
-
diff --git a/sleekxmpp/plugins/old_0009.py b/sleekxmpp/plugins/old_0009.py
deleted file mode 100644
index 625b03fb..00000000
--- a/sleekxmpp/plugins/old_0009.py
+++ /dev/null
@@ -1,277 +0,0 @@
-"""
-XEP-0009 XMPP Remote Procedure Calls
-"""
-from __future__ import with_statement
-from . import base
-import logging
-from xml.etree import cElementTree as ET
-import copy
-import time
-import base64
-
-def py2xml(*args):
- params = ET.Element("params")
- for x in args:
- param = ET.Element("param")
- param.append(_py2xml(x))
- params.append(param) #<params><param>...
- return params
-
-def _py2xml(*args):
- for x in args:
- val = ET.Element("value")
- if type(x) is int:
- i4 = ET.Element("i4")
- i4.text = str(x)
- val.append(i4)
- if type(x) is bool:
- boolean = ET.Element("boolean")
- boolean.text = str(int(x))
- val.append(boolean)
- elif type(x) is str:
- string = ET.Element("string")
- string.text = x
- val.append(string)
- elif type(x) is float:
- double = ET.Element("double")
- double.text = str(x)
- val.append(double)
- elif type(x) is rpcbase64:
- b64 = ET.Element("Base64")
- b64.text = x.encoded()
- val.append(b64)
- elif type(x) is rpctime:
- iso = ET.Element("dateTime.iso8601")
- iso.text = str(x)
- val.append(iso)
- elif type(x) is list:
- array = ET.Element("array")
- data = ET.Element("data")
- for y in x:
- data.append(_py2xml(y))
- array.append(data)
- val.append(array)
- elif type(x) is dict:
- struct = ET.Element("struct")
- for y in x.keys():
- member = ET.Element("member")
- name = ET.Element("name")
- name.text = y
- member.append(name)
- member.append(_py2xml(x[y]))
- struct.append(member)
- val.append(struct)
- return val
-
-def xml2py(params):
- vals = []
- for param in params.findall('param'):
- vals.append(_xml2py(param.find('value')))
- return vals
-
-def _xml2py(value):
- if value.find('i4') is not None:
- return int(value.find('i4').text)
- if value.find('int') is not None:
- return int(value.find('int').text)
- if value.find('boolean') is not None:
- return bool(value.find('boolean').text)
- if value.find('string') is not None:
- return value.find('string').text
- if value.find('double') is not None:
- return float(value.find('double').text)
- if value.find('Base64') is not None:
- return rpcbase64(value.find('Base64').text)
- if value.find('dateTime.iso8601') is not None:
- return rpctime(value.find('dateTime.iso8601'))
- if value.find('struct') is not None:
- struct = {}
- for member in value.find('struct').findall('member'):
- struct[member.find('name').text] = _xml2py(member.find('value'))
- return struct
- if value.find('array') is not None:
- array = []
- for val in value.find('array').find('data').findall('value'):
- array.append(_xml2py(val))
- return array
- raise ValueError()
-
-class rpcbase64(object):
- def __init__(self, data):
- #base 64 encoded string
- self.data = data
-
- def decode(self):
- return base64.decodestring(data)
-
- def __str__(self):
- return self.decode()
-
- def encoded(self):
- return self.data
-
-class rpctime(object):
- def __init__(self,data=None):
- #assume string data is in iso format YYYYMMDDTHH:MM:SS
- if type(data) is str:
- self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
- elif type(data) is time.struct_time:
- self.timestamp = data
- elif data is None:
- self.timestamp = time.gmtime()
- else:
- raise ValueError()
-
- def iso8601(self):
- #return a iso8601 string
- return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
-
- def __str__(self):
- return self.iso8601()
-
-class JabberRPCEntry(object):
- def __init__(self,call):
- self.call = call
- self.result = None
- self.error = None
- self.allow = {} #{'<jid>':['<resource1>',...],...}
- self.deny = {}
-
- def check_acl(self, jid, resource):
- #Check for deny
- if jid in self.deny.keys():
- if self.deny[jid] == None or resource in self.deny[jid]:
- return False
- #Check for allow
- if allow == None:
- return True
- if jid in self.allow.keys():
- if self.allow[jid] == None or resource in self.allow[jid]:
- return True
- return False
-
- def acl_allow(self, jid, resource):
- if jid == None:
- self.allow = None
- elif resource == None:
- self.allow[jid] = None
- elif jid in self.allow.keys():
- self.allow[jid].append(resource)
- else:
- self.allow[jid] = [resource]
-
- def acl_deny(self, jid, resource):
- if jid == None:
- self.deny = None
- elif resource == None:
- self.deny[jid] = None
- elif jid in self.deny.keys():
- self.deny[jid].append(resource)
- else:
- self.deny[jid] = [resource]
-
- def call_method(self, args):
- ret = self.call(*args)
-
-class xep_0009(base.base_plugin):
-
- def plugin_init(self):
- self.xep = '0009'
- self.description = 'Jabber-RPC'
- self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callMethod, name='Jabber RPC Call')
- self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callResult, name='Jabber RPC Result')
- self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callError, name='Jabber RPC Error')
- self.entries = {}
- self.activeCalls = []
-
- def post_init(self):
- base.base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
- self.xmpp.plugin['xep_0030'].add_identity('automatition','rpc')
-
- def register_call(self, method, name=None):
- #@returns an string that can be used in acl commands.
- with self.lock:
- if name is None:
- self.entries[method.__name__] = JabberRPCEntry(method)
- return method.__name__
- else:
- self.entries[name] = JabberRPCEntry(method)
- return name
-
- def acl_allow(self, entry, jid=None, resource=None):
- #allow the method entry to be called by the given jid and resource.
- #if jid is None it will allow any jid/resource.
- #if resource is None it will allow any resource belonging to the jid.
- with self.lock:
- if self.entries[entry]:
- self.entries[entry].acl_allow(jid,resource)
- else:
- raise ValueError()
-
- def acl_deny(self, entry, jid=None, resource=None):
- #Note: by default all requests are denied unless allowed with acl_allow.
- #If you deny an entry it will not be allowed regardless of acl_allow
- with self.lock:
- if self.entries[entry]:
- self.entries[entry].acl_deny(jid,resource)
- else:
- raise ValueError()
-
- def unregister_call(self, entry):
- #removes the registered call
- with self.lock:
- if self.entries[entry]:
- del self.entries[entry]
- else:
- raise ValueError()
-
- def makeMethodCallQuery(self,pmethod,params):
- query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
- methodCall = ET.Element('methodCall')
- methodName = ET.Element('methodName')
- methodName.text = pmethod
- methodCall.append(methodName)
- methodCall.append(params)
- query.append(methodCall)
- return query
-
- def makeIqMethodCall(self,pto,pmethod,params):
- iq = self.xmpp.makeIqSet()
- iq.set('to',pto)
- iq.append(self.makeMethodCallQuery(pmethod,params))
- return iq
-
- def makeIqMethodResponse(self,pto,pid,params):
- iq = self.xmpp.makeIqResult(pid)
- iq.set('to',pto)
- query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
- methodResponse = ET.Element('methodResponse')
- methodResponse.append(params)
- query.append(methodResponse)
- return iq
-
- def makeIqMethodError(self,pto,id,pmethod,params,condition):
- iq = self.xmpp.makeIqError(id)
- iq.set('to',pto)
- iq.append(self.makeMethodCallQuery(pmethod,params))
- iq.append(self.xmpp['xep_0086'].makeError(condition))
- return iq
-
-
-
- def call_remote(self, pto, pmethod, *args):
- #calls a remote method. Returns the id of the Iq.
- pass
-
- def _callMethod(self,xml):
- pass
-
- def _callResult(self,xml):
- pass
-
- def _callError(self,xml):
- pass
diff --git a/sleekxmpp/plugins/old_0050.py b/sleekxmpp/plugins/old_0050.py
deleted file mode 100644
index 6e969a51..00000000
--- a/sleekxmpp/plugins/old_0050.py
+++ /dev/null
@@ -1,133 +0,0 @@
-"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2010 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- See the file LICENSE for copying permission.
-"""
-from __future__ import with_statement
-from . import base
-import logging
-from xml.etree import cElementTree as ET
-import time
-
-class old_0050(base.base_plugin):
- """
- XEP-0050 Ad-Hoc Commands
- """
-
- def plugin_init(self):
- self.xep = '0050'
- self.description = 'Ad-Hoc Commands'
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None')
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute')
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True)
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel')
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete')
- self.commands = {}
- self.sessions = {}
- self.sd = self.xmpp.plugin['xep_0030']
-
- def post_init(self):
- base.base_plugin.post_init(self)
- self.sd.add_feature('http://jabber.org/protocol/commands')
-
- def addCommand(self, node, name, form, pointer=None, multi=False):
- self.sd.add_item(None, name, 'http://jabber.org/protocol/commands', node)
- self.sd.add_identity('automation', 'command-node', name, node)
- self.sd.add_feature('http://jabber.org/protocol/commands', node)
- self.sd.add_feature('jabber:x:data', node)
- self.commands[node] = (name, form, pointer, multi)
-
- def getNewSession(self):
- return str(time.time()) + '-' + self.xmpp.getNewId()
-
- def handler_command(self, xml):
- in_command = xml.find('{http://jabber.org/protocol/commands}command')
- sessionid = in_command.get('sessionid', None)
- node = in_command.get('node')
- sessionid = self.getNewSession()
- name, form, pointer, multi = self.commands[node]
- self.sessions[sessionid] = {}
- self.sessions[sessionid]['jid'] = xml.get('from')
- self.sessions[sessionid]['to'] = xml.get('to')
- self.sessions[sessionid]['past'] = [(form, None)]
- self.sessions[sessionid]['next'] = pointer
- npointer = pointer
- if multi:
- actions = ['next']
- status = 'executing'
- else:
- if pointer is None:
- status = 'completed'
- actions = []
- else:
- status = 'executing'
- actions = ['complete']
- self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions))
-
- def handler_command_complete(self, xml):
- in_command = xml.find('{http://jabber.org/protocol/commands}command')
- sessionid = in_command.get('sessionid', None)
- pointer = self.sessions[sessionid]['next']
- results = self.xmpp.plugin['old_0004'].makeForm('result')
- results.fromXML(in_command.find('{jabber:x:data}x'))
- pointer(results,sessionid)
- self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[]))
- del self.sessions[in_command.get('sessionid')]
-
-
- def handler_command_next(self, xml):
- in_command = xml.find('{http://jabber.org/protocol/commands}command')
- sessionid = in_command.get('sessionid', None)
- pointer = self.sessions[sessionid]['next']
- results = self.xmpp.plugin['old_0004'].makeForm('result')
- results.fromXML(in_command.find('{jabber:x:data}x'))
- form, npointer, next = pointer(results,sessionid)
- self.sessions[sessionid]['next'] = npointer
- self.sessions[sessionid]['past'].append((form, pointer))
- actions = []
- actions.append('prev')
- if npointer is None:
- status = 'completed'
- else:
- status = 'executing'
- if next:
- actions.append('next')
- else:
- actions.append('complete')
- self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions))
-
- def handler_command_cancel(self, xml):
- command = xml.find('{http://jabber.org/protocol/commands}command')
- try:
- del self.sessions[command.get('sessionid')]
- except:
- pass
- self.xmpp.send(self.makeCommand(xml.attrib['from'], command.attrib['node'], id=xml.attrib['id'], sessionid=command.attrib['sessionid'], status='canceled'))
-
- def makeCommand(self, to, node, id=None, form=None, sessionid=None, status='executing', actions=[]):
- if not id:
- id = self.xmpp.getNewId()
- iq = self.xmpp.makeIqResult(id)
- iq.attrib['from'] = self.xmpp.boundjid.full
- iq.attrib['to'] = to
- command = ET.Element('{http://jabber.org/protocol/commands}command')
- command.attrib['node'] = node
- command.attrib['status'] = status
- xmlactions = ET.Element('actions')
- for action in actions:
- xmlactions.append(ET.Element(action))
- if xmlactions:
- command.append(xmlactions)
- if not sessionid:
- sessionid = self.getNewSession()
- else:
- iq.attrib['from'] = self.sessions[sessionid]['to']
- command.attrib['sessionid'] = sessionid
- if form is not None:
- if hasattr(form,'getXML'):
- form = form.getXML()
- command.append(form)
- iq.append(command)
- return iq
diff --git a/sleekxmpp/plugins/old_0060.py b/sleekxmpp/plugins/old_0060.py
deleted file mode 100644
index 93124fca..00000000
--- a/sleekxmpp/plugins/old_0060.py
+++ /dev/null
@@ -1,313 +0,0 @@
-from __future__ import with_statement
-from . import base
-import logging
-#from xml.etree import cElementTree as ET
-from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET
-from . import stanza_pubsub
-from . xep_0004 import Form
-
-
-log = logging.getLogger(__name__)
-
-
-class xep_0060(base.base_plugin):
- """
- XEP-0060 Publish Subscribe
- """
-
- def plugin_init(self):
- self.xep = '0060'
- self.description = 'Publish-Subscribe'
-
- def create_node(self, jid, node, config=None, collection=False, ntype=None):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
- create = ET.Element('create')
- create.set('node', node)
- pubsub.append(create)
- configure = ET.Element('configure')
- if collection:
- ntype = 'collection'
- #if config is None:
- # submitform = self.xmpp.plugin['xep_0004'].makeForm('submit')
- #else:
- if config is not None:
- submitform = config
- if 'FORM_TYPE' in submitform.field:
- submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config')
- else:
- submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config')
- if ntype:
- if 'pubsub#node_type' in submitform.field:
- submitform.field['pubsub#node_type'].setValue(ntype)
- else:
- submitform.addField('pubsub#node_type', value=ntype)
- else:
- if 'pubsub#node_type' in submitform.field:
- submitform.field['pubsub#node_type'].setValue('leaf')
- else:
- submitform.addField('pubsub#node_type', value='leaf')
- submitform['type'] = 'submit'
- configure.append(submitform.xml)
- pubsub.append(configure)
- iq = self.xmpp.makeIqSet(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is False or result is None or result['type'] == 'error': return False
- return True
-
- def subscribe(self, jid, node, bare=True, subscribee=None):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
- subscribe = ET.Element('subscribe')
- subscribe.attrib['node'] = node
- if subscribee is None:
- if bare:
- subscribe.attrib['jid'] = self.xmpp.boundjid.bare
- else:
- subscribe.attrib['jid'] = self.xmpp.boundjid.full
- else:
- subscribe.attrib['jid'] = subscribee
- pubsub.append(subscribe)
- iq = self.xmpp.makeIqSet(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is False or result is None or result['type'] == 'error': return False
- return True
-
- def unsubscribe(self, jid, node, bare=True, subscribee=None):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
- unsubscribe = ET.Element('unsubscribe')
- unsubscribe.attrib['node'] = node
- if subscribee is None:
- if bare:
- unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare
- else:
- unsubscribe.attrib['jid'] = self.xmpp.boundjid.full
- else:
- unsubscribe.attrib['jid'] = subscribee
- pubsub.append(unsubscribe)
- iq = self.xmpp.makeIqSet(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is False or result is None or result['type'] == 'error': return False
- return True
-
- def getNodeConfig(self, jid, node=None): # if no node, then grab default
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
- if node is not None:
- configure = ET.Element('configure')
- configure.attrib['node'] = node
- else:
- configure = ET.Element('default')
- pubsub.append(configure)
- #TODO: Add configure support.
- iq = self.xmpp.makeIqGet()
- iq.append(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- #self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse)
- result = iq.send()
- if result is None or result == False or result['type'] == 'error':
- log.warning("got error instead of config")
- return False
- if node is not None:
- form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x')
- else:
- form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x')
- if not form or form is None:
- log.error("No form found.")
- return False
- return Form(xml=form)
-
- def getNodeSubscriptions(self, jid, node):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
- subscriptions = ET.Element('subscriptions')
- subscriptions.attrib['node'] = node
- pubsub.append(subscriptions)
- iq = self.xmpp.makeIqGet()
- iq.append(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is None or result == False or result['type'] == 'error':
- log.warning("got error instead of config")
- return False
- else:
- results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription')
- if results is None:
- return False
- subs = {}
- for sub in results:
- subs[sub.get('jid')] = sub.get('subscription')
- return subs
-
- def getNodeAffiliations(self, jid, node):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
- affiliations = ET.Element('affiliations')
- affiliations.attrib['node'] = node
- pubsub.append(affiliations)
- iq = self.xmpp.makeIqGet()
- iq.append(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is None or result == False or result['type'] == 'error':
- log.warning("got error instead of config")
- return False
- else:
- results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation')
- if results is None:
- return False
- subs = {}
- for sub in results:
- subs[sub.get('jid')] = sub.get('affiliation')
- return subs
-
- def deleteNode(self, jid, node):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
- iq = self.xmpp.makeIqSet()
- delete = ET.Element('delete')
- delete.attrib['node'] = node
- pubsub.append(delete)
- iq.append(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- result = iq.send()
- if result is not None and result is not False and result['type'] != 'error':
- return True
- else:
- return False
-
-
- def setNodeConfig(self, jid, node, config):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
- configure = ET.Element('configure')
- configure.attrib['node'] = node
- config = config.getXML('submit')
- configure.append(config)
- pubsub.append(configure)
- iq = self.xmpp.makeIqSet(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is None or result['type'] == 'error':
- return False
- return True
-
- def setItem(self, jid, node, items=[]):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
- publish = ET.Element('publish')
- publish.attrib['node'] = node
- for pub_item in items:
- id, payload = pub_item
- item = ET.Element('item')
- if id is not None:
- item.attrib['id'] = id
- item.append(payload)
- publish.append(item)
- pubsub.append(publish)
- iq = self.xmpp.makeIqSet(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is None or result is False or result['type'] == 'error': return False
- return True
-
- def addItem(self, jid, node, items=[]):
- return self.setItem(jid, node, items)
-
- def deleteItem(self, jid, node, item):
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
- retract = ET.Element('retract')
- retract.attrib['node'] = node
- itemn = ET.Element('item')
- itemn.attrib['id'] = item
- retract.append(itemn)
- pubsub.append(retract)
- iq = self.xmpp.makeIqSet(pubsub)
- iq.attrib['to'] = jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is None or result is False or result['type'] == 'error': return False
- return True
-
- def getNodes(self, jid):
- response = self.xmpp.plugin['xep_0030'].getItems(jid)
- items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
- nodes = {}
- if items is not None and items is not False:
- for item in items:
- nodes[item.get('node')] = item.get('name')
- return nodes
-
- def getItems(self, jid, node):
- response = self.xmpp.plugin['xep_0030'].getItems(jid, node)
- items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item')
- nodeitems = []
- if items is not None and items is not False:
- for item in items:
- nodeitems.append(item.get('node'))
- return nodeitems
-
- def addNodeToCollection(self, jid, child, parent=''):
- config = self.getNodeConfig(jid, child)
- if not config or config is None:
- self.lasterror = "Config Error"
- return False
- try:
- config.field['pubsub#collection'].setValue(parent)
- except KeyError:
- log.warning("pubsub#collection doesn't exist in config, trying to add it")
- config.addField('pubsub#collection', value=parent)
- if not self.setNodeConfig(jid, child, config):
- return False
- return True
-
- def modifyAffiliation(self, ps_jid, node, user_jid, affiliation):
- if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'):
- raise TypeError
- pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub')
- affs = ET.Element('affiliations')
- affs.attrib['node'] = node
- aff = ET.Element('affiliation')
- aff.attrib['jid'] = user_jid
- aff.attrib['affiliation'] = affiliation
- affs.append(aff)
- pubsub.append(affs)
- iq = self.xmpp.makeIqSet(pubsub)
- iq.attrib['to'] = ps_jid
- iq.attrib['from'] = self.xmpp.boundjid.full
- id = iq['id']
- result = iq.send()
- if result is None or result is False or result['type'] == 'error':
- return False
- return True
-
- def addNodeToCollection(self, jid, child, parent=''):
- config = self.getNodeConfig(jid, child)
- if not config or config is None:
- self.lasterror = "Config Error"
- return False
- try:
- config.field['pubsub#collection'].setValue(parent)
- except KeyError:
- log.warning("pubsub#collection doesn't exist in config, trying to add it")
- config.addField('pubsub#collection', value=parent)
- if not self.setNodeConfig(jid, child, config):
- return False
- return True
-
- def removeNodeFromCollection(self, jid, child):
- self.addNodeToCollection(jid, child, '')
-
diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py
index 1e175966..51f85995 100644
--- a/sleekxmpp/plugins/xep_0004/stanza/field.py
+++ b/sleekxmpp/plugins/xep_0004/stanza/field.py
@@ -41,10 +41,11 @@ class FormField(ElementBase):
self._type = value
def add_option(self, label='', value=''):
- if self._type in self.option_types:
- opt = FieldOption(parent=self)
+ if self._type is None or self._type in self.option_types:
+ opt = FieldOption()
opt['label'] = label
opt['value'] = value
+ self.append(opt)
else:
raise ValueError("Cannot add options to " + \
"a %s field." % self['type'])
diff --git a/sleekxmpp/plugins/xep_0004/stanza/form.py b/sleekxmpp/plugins/xep_0004/stanza/form.py
index 721ecc35..1d733760 100644
--- a/sleekxmpp/plugins/xep_0004/stanza/form.py
+++ b/sleekxmpp/plugins/xep_0004/stanza/form.py
@@ -65,7 +65,7 @@ class Form(ElementBase):
if kwtype is None:
kwtype = ftype
- field = FormField(parent=self)
+ field = FormField()
field['var'] = var
field['type'] = kwtype
field['value'] = value
@@ -77,6 +77,7 @@ class Form(ElementBase):
field['options'] = options
else:
del field['type']
+ self.append(field)
return field
def getXML(self, type='submit'):
@@ -144,14 +145,12 @@ class Form(ElementBase):
def get_fields(self, use_dict=False):
fields = OrderedDict()
- fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
- for fieldXML in fieldsXML:
- field = FormField(xml=fieldXML)
- fields[field['var']] = field
+ for stanza in self['substanzas']:
+ if isinstance(stanza, FormField):
+ fields[stanza['var']] = stanza
return fields
def get_instructions(self):
- instructions = ''
instsXML = self.xml.findall('{%s}instructions' % self.namespace)
return "\n".join([instXML.text for instXML in instsXML])
@@ -195,7 +194,14 @@ class Form(ElementBase):
fields = fields.items()
for var, field in fields:
field['var'] = var
- self.add_field(**field)
+ self.add_field(
+ var = field.get('var'),
+ label = field.get('label'),
+ desc = field.get('desc'),
+ required = field.get('required'),
+ value = field.get('value'),
+ options = field.get('options'),
+ type = field.get('type'))
def set_instructions(self, instructions):
del self['instructions']
@@ -221,6 +227,8 @@ class Form(ElementBase):
def set_values(self, values):
fields = self['fields']
for field in values:
+ if field not in fields:
+ fields[field] = self.add_field(var=field)
fields[field]['value'] = values[field]
def merge(self, other):
diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py
index 8c08e8f3..b02f587e 100644
--- a/sleekxmpp/plugins/xep_0009/remote.py
+++ b/sleekxmpp/plugins/xep_0009/remote.py
@@ -6,7 +6,7 @@
See the file LICENSE for copying permission.
"""
-from binding import py2xml, xml2py, xml2fault, fault2xml
+from sleekxmpp.plugins.xep_0009.binding import py2xml, xml2py, xml2fault, fault2xml
from threading import RLock
import abc
import inspect
@@ -18,6 +18,45 @@ import traceback
log = logging.getLogger(__name__)
+# Define a function _isstr() to check if an object is a string in a way
+# compatible with Python 2 and Python 3 (basestring does not exists in Python 3).
+try:
+ basestring # This evaluation will throw an exception if basestring does not exists (Python 3).
+ def _isstr(obj):
+ return isinstance(obj, basestring)
+except NameError:
+ def _isstr(obj):
+ return isinstance(obj, str)
+
+
+# Class decorator to declare a metaclass to a class in a way compatible with Python 2 and 3.
+# This decorator is copied from 'six' (https://bitbucket.org/gutworth/six):
+#
+# Copyright (c) 2010-2015 Benjamin Peterson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+def _add_metaclass(metaclass):
+ def wrapper(cls):
+ orig_vars = cls.__dict__.copy()
+ slots = orig_vars.get('__slots__')
+ if slots is not None:
+ if isinstance(slots, str):
+ slots = [slots]
+ for slots_var in slots:
+ orig_vars.pop(slots_var)
+ orig_vars.pop('__dict__', None)
+ orig_vars.pop('__weakref__', None)
+ return metaclass(cls.__name__, cls.__bases__, orig_vars)
+ return wrapper
+
def _intercept(method, name, public):
def _resolver(instance, *args, **kwargs):
log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args)
@@ -68,7 +107,7 @@ def remote(function_argument, public = True):
if hasattr(function_argument, '__call__'):
return _intercept(function_argument, None, public)
else:
- if not isinstance(function_argument, basestring):
+ if not _isstr(function_argument):
if not isinstance(function_argument, bool):
raise Exception('Expected an RPC method name or visibility modifier!')
else:
@@ -222,12 +261,11 @@ class TimeoutException(Exception):
pass
+@_add_metaclass(abc.ABCMeta)
class Callback(object):
'''
A base class for callback handlers.
'''
- __metaclass__ = abc.ABCMeta
-
@abc.abstractproperty
def set_value(self, value):
@@ -291,7 +329,7 @@ class Future(Callback):
self._event.set()
-
+@_add_metaclass(abc.ABCMeta)
class Endpoint(object):
'''
The Endpoint class is an abstract base class for all objects
@@ -303,8 +341,6 @@ class Endpoint(object):
which specifies which object an RPC call refers to. It is the
first part in a RPC method name '<fqn>.<method>'.
'''
- __metaclass__ = abc.ABCMeta
-
def __init__(self, session, target_jid):
'''
@@ -491,7 +527,7 @@ class RemoteSession(object):
def _find_key(self, dict, value):
"""return the key of dictionary dic given the value"""
- search = [k for k, v in dict.iteritems() if v == value]
+ search = [k for k, v in dict.items() if v == value]
if len(search) == 0:
return None
else:
@@ -547,7 +583,7 @@ class RemoteSession(object):
result = handler_cls(*args, **kwargs)
Endpoint.__init__(result, self, self._client.boundjid.full)
method_dict = result.get_methods()
- for method_name, method in method_dict.iteritems():
+ for method_name, method in method_dict.items():
#!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name)
self._register_call(result.FQN(), method, method_name)
self._register_acl(result.FQN(), acl)
@@ -569,11 +605,11 @@ class RemoteSession(object):
self._register_callback(pid, callback)
iq.send()
- def close(self):
+ def close(self, wait=False):
'''
Closes this session.
'''
- self._client.disconnect(False)
+ self._client.disconnect(wait=wait)
self._session_close_callback()
def _on_jabber_rpc_method_call(self, iq):
@@ -697,7 +733,8 @@ class Remote(object):
if(client.boundjid.bare in cls._sessions):
raise RemoteException("There already is a session associated with these credentials!")
else:
- cls._sessions[client.boundjid.bare] = client;
+ cls._sessions[client.boundjid.bare] = client
+
def _session_close_callback():
with Remote._lock:
del cls._sessions[client.boundjid.bare]
diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py
index 4e1c538b..6179355e 100644
--- a/sleekxmpp/plugins/xep_0009/rpc.py
+++ b/sleekxmpp/plugins/xep_0009/rpc.py
@@ -32,15 +32,15 @@ class XEP_0009(BasePlugin):
register_stanza_plugin(RPCQuery, MethodCall)
register_stanza_plugin(RPCQuery, MethodResponse)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
self._handle_method_call)
)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
self._handle_method_response)
)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)),
self._handle_error)
)
@@ -61,7 +61,7 @@ class XEP_0009(BasePlugin):
iq.enable('rpc_query')
iq['rpc_query']['method_call']['method_name'] = pmethod
iq['rpc_query']['method_call']['params'] = params
- return iq;
+ return iq
def make_iq_method_response(self, pid, pto, params):
iq = self.xmpp.makeIqResult(pid)
@@ -93,7 +93,7 @@ class XEP_0009(BasePlugin):
def _item_not_found(self, iq):
payload = iq.get_payload()
- iq.reply().error().set_payload(payload);
+ iq.reply().error().set_payload(payload)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
diff --git a/sleekxmpp/plugins/xep_0020/__init__.py b/sleekxmpp/plugins/xep_0020/__init__.py
new file mode 100644
index 00000000..c6aafe97
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0020/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0020 import stanza
+from sleekxmpp.plugins.xep_0020.stanza import FeatureNegotiation
+from sleekxmpp.plugins.xep_0020.feature_negotiation import XEP_0020
+
+
+register_plugin(XEP_0020)
diff --git a/sleekxmpp/plugins/xep_0020/feature_negotiation.py b/sleekxmpp/plugins/xep_0020/feature_negotiation.py
new file mode 100644
index 00000000..7cb82cd5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0020/feature_negotiation.py
@@ -0,0 +1,36 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp import Iq, Message
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, JID
+from sleekxmpp.plugins.xep_0020 import stanza, FeatureNegotiation
+from sleekxmpp.plugins.xep_0004 import Form
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0020(BasePlugin):
+
+ name = 'xep_0020'
+ description = 'XEP-0020: Feature Negotiation'
+ dependencies = set(['xep_0004', 'xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp['xep_0030'].add_feature(FeatureNegotiation.namespace)
+
+ register_stanza_plugin(FeatureNegotiation, Form)
+
+ register_stanza_plugin(Iq, FeatureNegotiation)
+ register_stanza_plugin(Message, FeatureNegotiation)
diff --git a/sleekxmpp/plugins/xep_0020/stanza.py b/sleekxmpp/plugins/xep_0020/stanza.py
new file mode 100644
index 00000000..13e4056e
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0020/stanza.py
@@ -0,0 +1,17 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class FeatureNegotiation(ElementBase):
+
+ name = 'feature'
+ namespace = 'http://jabber.org/protocol/feature-neg'
+ plugin_attrib = 'feature_neg'
+ interfaces = set()
diff --git a/sleekxmpp/plugins/xep_0027/gpg.py b/sleekxmpp/plugins/xep_0027/gpg.py
index 2aa6e5a0..52c1c461 100644
--- a/sleekxmpp/plugins/xep_0027/gpg.py
+++ b/sleekxmpp/plugins/xep_0027/gpg.py
@@ -24,7 +24,7 @@ def _extract_data(data, kind):
if not begin_headers and 'BEGIN PGP %s' % kind in line:
begin_headers = True
continue
- if begin_headers and line.stripped() == '':
+ if begin_headers and line.strip() == '':
begin_data = True
continue
if 'END PGP %s' % kind in line:
diff --git a/sleekxmpp/plugins/xep_0027/stanza.py b/sleekxmpp/plugins/xep_0027/stanza.py
index 3170ca6e..08f2032b 100644
--- a/sleekxmpp/plugins/xep_0027/stanza.py
+++ b/sleekxmpp/plugins/xep_0027/stanza.py
@@ -39,7 +39,7 @@ class Encrypted(ElementBase):
def set_encrypted(self, value):
parent = self.parent()
xmpp = parent.stream
- data = xmpp['xep_0027'].encrypt(value, parent['to'].bare)
+ data = xmpp['xep_0027'].encrypt(value, parent['to'])
if data:
self.xml.text = data
else:
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py
index 8a397923..721f73f6 100644
--- a/sleekxmpp/plugins/xep_0030/disco.py
+++ b/sleekxmpp/plugins/xep_0030/disco.py
@@ -324,7 +324,7 @@ class XEP_0030(BasePlugin):
callback -- Optional callback to execute when a reply is
received instead of blocking and waiting for
the reply.
- timeout_callback -- Optional callback to execute when no result
+ timeout_callback -- Optional callback to execute when no result
has been received in timeout seconds.
"""
if local is None:
@@ -408,7 +408,7 @@ class XEP_0030(BasePlugin):
iterator -- If True, return a result set iterator using
the XEP-0059 plugin, if the plugin is loaded.
Otherwise the parameter is ignored.
- timeout_callback -- Optional callback to execute when no result
+ timeout_callback -- Optional callback to execute when no result
has been received in timeout seconds.
"""
if local or local is None and jid is None:
@@ -604,7 +604,7 @@ class XEP_0030(BasePlugin):
"""
self.api['del_features'](jid, node, None, kwargs)
- def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
+ def _run_node_handler(self, htype, jid, node=None, ifrom=None, data=None):
"""
Execute the most specific node handler for the given
JID/node combination.
@@ -615,6 +615,9 @@ class XEP_0030(BasePlugin):
node -- The node requested.
data -- Optional, custom data to pass to the handler.
"""
+ if not data:
+ data = {}
+
return self.api[htype](jid, node, ifrom, data)
def _handle_disco_info(self, iq):
diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py
index 512f2336..10458614 100644
--- a/sleekxmpp/plugins/xep_0030/stanza/items.py
+++ b/sleekxmpp/plugins/xep_0030/stanza/items.py
@@ -128,9 +128,10 @@ class DiscoItems(ElementBase):
def del_items(self):
"""Remove all items."""
self._items = set()
- for item in self['substanzas']:
- if isinstance(item, DiscoItem):
- self.xml.remove(item.xml)
+ items = [i for i in self.iterables if isinstance(i, DiscoItem)]
+ for item in items:
+ self.xml.remove(item.xml)
+ self.iterables.remove(item)
class DiscoItem(ElementBase):
diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py
index cba07702..ca5ed1ef 100644
--- a/sleekxmpp/plugins/xep_0045.py
+++ b/sleekxmpp/plugins/xep_0045.py
@@ -125,11 +125,12 @@ class XEP_0045(BasePlugin):
self.xep = '0045'
# load MUC support in presence stanzas
register_stanza_plugin(Presence, MUCPresence)
- self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
- self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
- self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
- self.xmpp.registerHandler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change))
- self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % (
+ self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
+ self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns), self.handle_groupchat_error_message))
+ self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
+ self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject))
+ self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change))
+ self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % (
self.xmpp.default_ns,
'http://jabber.org/protocol/muc#user',
'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite))
@@ -179,6 +180,14 @@ class XEP_0045(BasePlugin):
self.xmpp.event('groupchat_message', msg)
self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
+ def handle_groupchat_error_message(self, msg):
+ """ Handle a message error event in a muc.
+ """
+ self.xmpp.event('groupchat_message_error', msg)
+ self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
+
+
+
def handle_groupchat_subject(self, msg):
""" Handle a message coming from a muc indicating
a change of subject (or announcing it when joining the room)
@@ -198,30 +207,9 @@ class XEP_0045(BasePlugin):
if entry is not None and entry['jid'].full == jid:
return nick
- def getRoomForm(self, room, ifrom=None):
- iq = self.xmpp.makeIqGet()
- iq['to'] = room
- if ifrom is not None:
- iq['from'] = ifrom
- query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- iq.append(query)
- # For now, swallow errors to preserve existing API
- try:
- result = iq.send()
- except IqError:
- return False
- except IqTimeout:
- return False
- xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
- if xform is None: return False
- form = self.xmpp.plugin['old_0004'].buildForm(xform)
- return form
-
def configureRoom(self, room, form=None, ifrom=None):
if form is None:
- form = self.getRoomForm(room, ifrom=ifrom)
- #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit')
- #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig')
+ form = self.getRoomConfig(room, ifrom=ifrom)
iq = self.xmpp.makeIqSet()
iq['to'] = room
if ifrom is not None:
@@ -310,6 +298,24 @@ class XEP_0045(BasePlugin):
return False
return True
+ def setRole(self, room, nick, role):
+ """ Change role property of a nick in a room.
+ Typically, roles are temporary (they last only as long as you are in the
+ room), whereas affiliations are permanent (they last across groupchat
+ sessions).
+ """
+ if role not in ('moderator', 'participant', 'visitor', 'none'):
+ raise TypeError
+ query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
+ item = ET.Element('item', {'role':role, 'nick':nick})
+ query.append(item)
+ iq = self.xmpp.makeIqSet(query)
+ iq['to'] = room
+ result = iq.send()
+ if result is False or result['type'] != 'result':
+ raise ValueError
+ return True
+
def invite(self, room, jid, reason='', mfrom=''):
""" Invite a jid to a room."""
msg = self.xmpp.makeMessage(room)
diff --git a/sleekxmpp/plugins/xep_0047/ibb.py b/sleekxmpp/plugins/xep_0047/ibb.py
index fb48a9b9..62dddac2 100644
--- a/sleekxmpp/plugins/xep_0047/ibb.py
+++ b/sleekxmpp/plugins/xep_0047/ibb.py
@@ -21,21 +21,25 @@ class XEP_0047(BasePlugin):
dependencies = set(['xep_0030'])
stanza = stanza
default_config = {
+ 'block_size': 4096,
'max_block_size': 8192,
'window_size': 1,
- 'auto_accept': True,
- 'accept_stream': None
+ 'auto_accept': False,
}
def plugin_init(self):
- self.streams = {}
- self.pending_streams = {}
- self.pending_close_streams = {}
+ self._streams = {}
+ self._pending_streams = {}
+ self._pending_lock = threading.Lock()
self._stream_lock = threading.Lock()
+ self._preauthed_sids_lock = threading.Lock()
+ self._preauthed_sids = {}
+
register_stanza_plugin(Iq, Open)
register_stanza_plugin(Iq, Close)
register_stanza_plugin(Iq, Data)
+ register_stanza_plugin(Message, Data)
self.xmpp.register_handler(Callback(
'IBB Open',
@@ -52,27 +56,71 @@ class XEP_0047(BasePlugin):
StanzaPath('iq@type=set/ibb_data'),
self._handle_data))
+ self.xmpp.register_handler(Callback(
+ 'IBB Message Data',
+ StanzaPath('message/ibb_data'),
+ self._handle_data))
+
+ self.api.register(self._authorized, 'authorized', default=True)
+ self.api.register(self._authorized_sid, 'authorized_sid', default=True)
+ self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True)
+ self.api.register(self._get_stream, 'get_stream', default=True)
+ self.api.register(self._set_stream, 'set_stream', default=True)
+ self.api.register(self._del_stream, 'del_stream', default=True)
+
def plugin_end(self):
self.xmpp.remove_handler('IBB Open')
self.xmpp.remove_handler('IBB Close')
self.xmpp.remove_handler('IBB Data')
+ self.xmpp.remove_handler('IBB Message Data')
self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb')
+ def _get_stream(self, jid, sid, peer_jid, data):
+ return self._streams.get((jid, sid, peer_jid), None)
+
+ def _set_stream(self, jid, sid, peer_jid, stream):
+ self._streams[(jid, sid, peer_jid)] = stream
+
+ def _del_stream(self, jid, sid, peer_jid, data):
+ with self._stream_lock:
+ if (jid, sid, peer_jid) in self._streams:
+ del self._streams[(jid, sid, peer_jid)]
+
def _accept_stream(self, iq):
- if self.accept_stream is not None:
- return self.accept_stream(iq)
+ receiver = iq['to']
+ sender = iq['from']
+ sid = iq['ibb_open']['sid']
+
+ if self.api['authorized_sid'](receiver, sid, sender, iq):
+ return True
+ return self.api['authorized'](receiver, sid, sender, iq)
+
+ def _authorized(self, jid, sid, ifrom, iq):
if self.auto_accept:
if iq['ibb_open']['block_size'] <= self.max_block_size:
return True
return False
- def open_stream(self, jid, block_size=4096, sid=None, window=1,
+ def _authorized_sid(self, jid, sid, ifrom, iq):
+ with self._preauthed_sids_lock:
+ if (jid, sid, ifrom) in self._preauthed_sids:
+ del self._preauthed_sids[(jid, sid, ifrom)]
+ return True
+ return False
+
+ def _preauthorize_sid(self, jid, sid, ifrom, data):
+ with self._preauthed_sids_lock:
+ self._preauthed_sids[(jid, sid, ifrom)] = True
+
+ def open_stream(self, jid, block_size=None, sid=None, window=1, use_messages=False,
ifrom=None, block=True, timeout=None, callback=None):
if sid is None:
sid = str(uuid.uuid4())
+ if block_size is None:
+ block_size = self.block_size
iq = self.xmpp.Iq()
iq['type'] = 'set'
@@ -83,12 +131,13 @@ class XEP_0047(BasePlugin):
iq['ibb_open']['stanza'] = 'iq'
stream = IBBytestream(self.xmpp, sid, block_size,
- iq['to'], iq['from'], window)
+ iq['from'], iq['to'], window,
+ use_messages)
with self._stream_lock:
- self.pending_streams[iq['id']] = stream
+ self._pending_streams[iq['id']] = stream
- self.pending_streams[iq['id']] = stream
+ self._pending_streams[iq['id']] = stream
if block:
resp = iq.send(timeout=timeout)
@@ -108,49 +157,59 @@ class XEP_0047(BasePlugin):
def _handle_opened_stream(self, iq):
if iq['type'] == 'result':
with self._stream_lock:
- stream = self.pending_streams.get(iq['id'], None)
- if stream is not None:
- stream.sender = iq['to']
- stream.receiver = iq['from']
- stream.stream_started.set()
- self.streams[stream.sid] = stream
- self.xmpp.event('ibb_stream_start', stream)
+ stream = self._pending_streams.get(iq['id'], None)
+ if stream is not None:
+ log.debug('IBB stream (%s) accepted by %s', stream.sid, iq['from'])
+ stream.self_jid = iq['to']
+ stream.peer_jid = iq['from']
+ stream.stream_started.set()
+ self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
+ self.xmpp.event('ibb_stream_start', stream)
+ self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream)
with self._stream_lock:
- if iq['id'] in self.pending_streams:
- del self.pending_streams[iq['id']]
+ if iq['id'] in self._pending_streams:
+ del self._pending_streams[iq['id']]
def _handle_open_request(self, iq):
sid = iq['ibb_open']['sid']
- size = iq['ibb_open']['block_size']
+ size = iq['ibb_open']['block_size'] or self.block_size
+
+ log.debug('Received IBB stream request from %s', iq['from'])
+
+ if not sid:
+ raise XMPPError(etype='modify', condition='bad-request')
+
if not self._accept_stream(iq):
- raise XMPPError('not-acceptable')
+ raise XMPPError(etype='modify', condition='not-acceptable')
if size > self.max_block_size:
raise XMPPError('resource-constraint')
stream = IBBytestream(self.xmpp, sid, size,
- iq['from'], iq['to'],
+ iq['to'], iq['from'],
self.window_size)
stream.stream_started.set()
- self.streams[sid] = stream
+ self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream)
iq.reply()
iq.send()
self.xmpp.event('ibb_stream_start', stream)
+ self.xmpp.event('stream:%s:%s' % (sid, stream.peer_jid), stream)
- def _handle_data(self, iq):
- sid = iq['ibb_data']['sid']
- stream = self.streams.get(sid, None)
- if stream is not None and iq['from'] != stream.sender:
- stream._recv_data(iq)
+ def _handle_data(self, stanza):
+ sid = stanza['ibb_data']['sid']
+ stream = self.api['get_stream'](stanza['to'], sid, stanza['from'])
+ if stream is not None and stanza['from'] == stream.peer_jid:
+ stream._recv_data(stanza)
else:
raise XMPPError('item-not-found')
def _handle_close(self, iq):
sid = iq['ibb_close']['sid']
- stream = self.streams.get(sid, None)
- if stream is not None and iq['from'] != stream.sender:
+ stream = self.api['get_stream'](iq['to'], sid, iq['from'])
+ if stream is not None and iq['from'] == stream.peer_jid:
stream._closed(iq)
+ self.api['del_stream'](stream.self_jid, stream.sid, stream.peer_jid)
else:
raise XMPPError('item-not-found')
diff --git a/sleekxmpp/plugins/xep_0047/stream.py b/sleekxmpp/plugins/xep_0047/stream.py
index b49a077b..9651edf8 100644
--- a/sleekxmpp/plugins/xep_0047/stream.py
+++ b/sleekxmpp/plugins/xep_0047/stream.py
@@ -2,6 +2,7 @@ import socket
import threading
import logging
+from sleekxmpp.stanza import Iq
from sleekxmpp.util import Queue
from sleekxmpp.exceptions import XMPPError
@@ -11,14 +12,17 @@ log = logging.getLogger(__name__)
class IBBytestream(object):
- def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1):
+ def __init__(self, xmpp, sid, block_size, jid, peer, window_size=1, use_messages=False):
self.xmpp = xmpp
self.sid = sid
self.block_size = block_size
self.window_size = window_size
+ self.use_messages = use_messages
- self.receiver = to
- self.sender = ifrom
+ if jid is None:
+ jid = xmpp.boundjid
+ self.self_jid = jid
+ self.peer_jid = peer
self.send_seq = -1
self.recv_seq = -1
@@ -46,16 +50,27 @@ class IBBytestream(object):
with self._send_seq_lock:
self.send_seq = (self.send_seq + 1) % 65535
seq = self.send_seq
- iq = self.xmpp.Iq()
- iq['type'] = 'set'
- iq['to'] = self.receiver
- iq['from'] = self.sender
- iq['ibb_data']['sid'] = self.sid
- iq['ibb_data']['seq'] = seq
- iq['ibb_data']['data'] = data
- self.window_empty.clear()
- self.window_ids.add(iq['id'])
- iq.send(block=False, callback=self._recv_ack)
+ if self.use_messages:
+ msg = self.xmpp.Message()
+ msg['to'] = self.peer_jid
+ msg['from'] = self.self_jid
+ msg['id'] = self.xmpp.new_id()
+ msg['ibb_data']['sid'] = self.sid
+ msg['ibb_data']['seq'] = seq
+ msg['ibb_data']['data'] = data
+ msg.send()
+ self.send_window.release()
+ else:
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = self.peer_jid
+ iq['from'] = self.self_jid
+ iq['ibb_data']['sid'] = self.sid
+ iq['ibb_data']['seq'] = seq
+ iq['ibb_data']['data'] = data
+ self.window_empty.clear()
+ self.window_ids.add(iq['id'])
+ iq.send(block=False, callback=self._recv_ack)
return len(data)
def sendall(self, data):
@@ -71,23 +86,25 @@ class IBBytestream(object):
if iq['type'] == 'error':
self.close()
- def _recv_data(self, iq):
+ def _recv_data(self, stanza):
with self._recv_seq_lock:
- new_seq = iq['ibb_data']['seq']
+ new_seq = stanza['ibb_data']['seq']
if new_seq != (self.recv_seq + 1) % 65535:
self.close()
raise XMPPError('unexpected-request')
self.recv_seq = new_seq
- data = iq['ibb_data']['data']
+ data = stanza['ibb_data']['data']
if len(data) > self.block_size:
self.close()
raise XMPPError('not-acceptable')
self.recv_queue.put(data)
self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data})
- iq.reply()
- iq.send()
+
+ if isinstance(stanza, Iq):
+ stanza.reply()
+ stanza.send()
def recv(self, *args, **kwargs):
return self.read(block=True)
@@ -106,8 +123,8 @@ class IBBytestream(object):
def close(self):
iq = self.xmpp.Iq()
iq['type'] = 'set'
- iq['to'] = self.receiver
- iq['from'] = self.sender
+ iq['to'] = self.peer_jid
+ iq['from'] = self.self_jid
iq['ibb_close']['sid'] = self.sid
self.stream_out_closed.set()
iq.send(block=False,
@@ -117,9 +134,6 @@ class IBBytestream(object):
def _closed(self, iq):
self.stream_in_closed.set()
self.stream_out_closed.set()
- while not self.window_empty.is_set():
- log.info('waiting for send window to empty')
- self.window_empty.wait(timeout=1)
iq.reply()
iq.send()
self.xmpp.event('ibb_stream_end', self)
diff --git a/sleekxmpp/plugins/xep_0048/__init__.py b/sleekxmpp/plugins/xep_0048/__init__.py
new file mode 100644
index 00000000..2c98d061
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0048/__init__.py
@@ -0,0 +1,15 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0048.stanza import Bookmarks, Conference, URL
+from sleekxmpp.plugins.xep_0048.bookmarks import XEP_0048
+
+
+register_plugin(XEP_0048)
diff --git a/sleekxmpp/plugins/xep_0048/bookmarks.py b/sleekxmpp/plugins/xep_0048/bookmarks.py
new file mode 100644
index 00000000..0bb5ae38
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0048/bookmarks.py
@@ -0,0 +1,76 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp import Iq
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0048 import stanza, Bookmarks, Conference, URL
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0048(BasePlugin):
+
+ name = 'xep_0048'
+ description = 'XEP-0048: Bookmarks'
+ dependencies = set(['xep_0045', 'xep_0049', 'xep_0060', 'xep_0163', 'xep_0223'])
+ stanza = stanza
+ default_config = {
+ 'auto_join': False,
+ 'storage_method': 'xep_0049'
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, Bookmarks)
+
+ self.xmpp['xep_0049'].register(Bookmarks)
+ self.xmpp['xep_0163'].register_pep('bookmarks', Bookmarks)
+
+ self.xmpp.add_event_handler('session_start', self._autojoin)
+
+ def plugin_end(self):
+ self.xmpp.del_event_handler('session_start', self._autojoin)
+
+ def _autojoin(self, __):
+ if not self.auto_join:
+ return
+
+ try:
+ result = self.get_bookmarks(method=self.storage_method)
+ except XMPPError:
+ return
+
+ if self.storage_method == 'xep_0223':
+ bookmarks = result['pubsub']['items']['item']['bookmarks']
+ else:
+ bookmarks = result['private']['bookmarks']
+
+ for conf in bookmarks['conferences']:
+ if conf['autojoin']:
+ log.debug('Auto joining %s as %s', conf['jid'], conf['nick'])
+ self.xmpp['xep_0045'].joinMUC(conf['jid'], conf['nick'],
+ password=conf['password'])
+
+ def set_bookmarks(self, bookmarks, method=None, **iqargs):
+ if not method:
+ method = self.storage_method
+ return self.xmpp[method].store(bookmarks, **iqargs)
+
+ def get_bookmarks(self, method=None, **iqargs):
+ if not method:
+ method = self.storage_method
+
+ loc = 'storage:bookmarks' if method == 'xep_0223' else 'bookmarks'
+
+ return self.xmpp[method].retrieve(loc, **iqargs)
diff --git a/sleekxmpp/plugins/xep_0048/stanza.py b/sleekxmpp/plugins/xep_0048/stanza.py
new file mode 100644
index 00000000..21829392
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0048/stanza.py
@@ -0,0 +1,65 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Bookmarks(ElementBase):
+ name = 'storage'
+ namespace = 'storage:bookmarks'
+ plugin_attrib = 'bookmarks'
+ interfaces = set()
+
+ def add_conference(self, jid, nick, name=None, autojoin=None, password=None):
+ conf = Conference()
+ conf['jid'] = jid
+ conf['nick'] = nick
+ if name is None:
+ name = jid
+ conf['name'] = name
+ conf['autojoin'] = autojoin
+ conf['password'] = password
+ self.append(conf)
+
+ def add_url(self, url, name=None):
+ saved_url = URL()
+ saved_url['url'] = url
+ if name is None:
+ name = url
+ saved_url['name'] = name
+ self.append(saved_url)
+
+
+class Conference(ElementBase):
+ name = 'conference'
+ namespace = 'storage:bookmarks'
+ plugin_attrib = 'conference'
+ plugin_multi_attrib = 'conferences'
+ interfaces = set(['nick', 'password', 'autojoin', 'jid', 'name'])
+ sub_interfaces = set(['nick', 'password'])
+
+ def get_autojoin(self):
+ value = self._get_attr('autojoin')
+ return value in ('1', 'true')
+
+ def set_autojoin(self, value):
+ del self['autojoin']
+ if value in ('1', 'true', True):
+ self._set_attr('autojoin', 'true')
+
+
+class URL(ElementBase):
+ name = 'url'
+ namespace = 'storage:bookmarks'
+ plugin_attrib = 'url'
+ plugin_multi_attrib = 'urls'
+ interfaces = set(['url', 'name'])
+
+
+register_stanza_plugin(Bookmarks, Conference, iterable=True)
+register_stanza_plugin(Bookmarks, URL, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py
index 90256228..e5594c3f 100644
--- a/sleekxmpp/plugins/xep_0050/adhoc.py
+++ b/sleekxmpp/plugins/xep_0050/adhoc.py
@@ -267,20 +267,50 @@ class XEP_0050(BasePlugin):
iq -- The command continuation request.
"""
sessionid = iq['command']['sessionid']
- session = self.sessions[sessionid]
+ session = self.sessions.get(sessionid)
- handler = session['next']
- interfaces = session['interfaces']
- results = []
- for stanza in iq['command']['substanzas']:
- if stanza.plugin_attrib in interfaces:
- results.append(stanza)
- if len(results) == 1:
- results = results[0]
+ if session:
+ handler = session['next']
+ interfaces = session['interfaces']
+ results = []
+ for stanza in iq['command']['substanzas']:
+ if stanza.plugin_attrib in interfaces:
+ results.append(stanza)
+ if len(results) == 1:
+ results = results[0]
- session = handler(results, session)
+ session = handler(results, session)
- self._process_command_response(iq, session)
+ self._process_command_response(iq, session)
+ else:
+ raise XMPPError('item-not-found')
+
+ def _handle_command_prev(self, iq):
+ """
+ Process a request for the prev step in the workflow
+ for a command with multiple steps.
+
+ Arguments:
+ iq -- The command continuation request.
+ """
+ sessionid = iq['command']['sessionid']
+ session = self.sessions.get(sessionid)
+
+ if session:
+ handler = session['prev']
+ interfaces = session['interfaces']
+ results = []
+ for stanza in iq['command']['substanzas']:
+ if stanza.plugin_attrib in interfaces:
+ results.append(stanza)
+ if len(results) == 1:
+ results = results[0]
+
+ session = handler(results, session)
+
+ self._process_command_response(iq, session)
+ else:
+ raise XMPPError('item-not-found')
def _process_command_response(self, iq, session):
"""
@@ -348,23 +378,23 @@ class XEP_0050(BasePlugin):
"""
node = iq['command']['node']
sessionid = iq['command']['sessionid']
- session = self.sessions[sessionid]
- handler = session['cancel']
- if handler:
- handler(iq, session)
+ session = self.sessions.get(sessionid)
- try:
+ if session:
+ handler = session['cancel']
+ if handler:
+ handler(iq, session)
del self.sessions[sessionid]
- except:
- pass
+ iq.reply()
+ iq['command']['node'] = node
+ iq['command']['sessionid'] = sessionid
+ iq['command']['status'] = 'canceled'
+ iq['command']['notes'] = session['notes']
+ iq.send()
+ else:
+ raise XMPPError('item-not-found')
- iq.reply()
- iq['command']['node'] = node
- iq['command']['sessionid'] = sessionid
- iq['command']['status'] = 'canceled'
- iq['command']['notes'] = session['notes']
- iq.send()
def _handle_command_complete(self, iq):
"""
@@ -378,28 +408,32 @@ class XEP_0050(BasePlugin):
"""
node = iq['command']['node']
sessionid = iq['command']['sessionid']
- session = self.sessions[sessionid]
- handler = session['next']
- interfaces = session['interfaces']
- results = []
- for stanza in iq['command']['substanzas']:
- if stanza.plugin_attrib in interfaces:
- results.append(stanza)
- if len(results) == 1:
- results = results[0]
+ session = self.sessions.get(sessionid)
- if handler:
- handler(results, session)
+ if session:
+ handler = session['next']
+ interfaces = session['interfaces']
+ results = []
+ for stanza in iq['command']['substanzas']:
+ if stanza.plugin_attrib in interfaces:
+ results.append(stanza)
+ if len(results) == 1:
+ results = results[0]
- iq.reply()
- iq['command']['node'] = node
- iq['command']['sessionid'] = sessionid
- iq['command']['actions'] = []
- iq['command']['status'] = 'completed'
- iq['command']['notes'] = session['notes']
- iq.send()
+ if handler:
+ handler(results, session)
+
+ del self.sessions[sessionid]
- del self.sessions[sessionid]
+ iq.reply()
+ iq['command']['node'] = node
+ iq['command']['sessionid'] = sessionid
+ iq['command']['actions'] = []
+ iq['command']['status'] = 'completed'
+ iq['command']['notes'] = session['notes']
+ iq.send()
+ else:
+ raise XMPPError('item-not-found')
# =================================================================
# Client side (command user) API
@@ -537,7 +571,7 @@ class XEP_0050(BasePlugin):
else:
iq.send(block=False, callback=self._handle_command_result)
- def continue_command(self, session):
+ def continue_command(self, session, direction='next'):
"""
Execute the next action of the command.
@@ -551,7 +585,7 @@ class XEP_0050(BasePlugin):
self.send_command(session['jid'],
session['node'],
ifrom=session.get('from', None),
- action='next',
+ action=direction,
payload=session.get('payload', None),
sessionid=session['id'],
flow=True,
diff --git a/sleekxmpp/plugins/xep_0054/vcard_temp.py b/sleekxmpp/plugins/xep_0054/vcard_temp.py
index 24da1c05..97da8c7c 100644
--- a/sleekxmpp/plugins/xep_0054/vcard_temp.py
+++ b/sleekxmpp/plugins/xep_0054/vcard_temp.py
@@ -8,7 +8,7 @@
import logging
-from sleekxmpp import Iq
+from sleekxmpp import JID, Iq
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
@@ -59,10 +59,20 @@ class XEP_0054(BasePlugin):
def make_vcard(self):
return VCardTemp()
- def get_vcard(self, jid=None, ifrom=None, local=False, cached=False,
+ def get_vcard(self, jid=None, ifrom=None, local=None, cached=False,
block=True, callback=None, timeout=None):
- if self.xmpp.is_component and jid.domain == self.xmpp.boundjid.domain:
- local = True
+ if local is None:
+ if jid is not None and not isinstance(jid, JID):
+ jid = JID(jid)
+ if self.xmpp.is_component:
+ if jid.domain == self.xmpp.boundjid.domain:
+ local = True
+ else:
+ if str(jid) == str(self.xmpp.boundjid):
+ local = True
+ jid = jid.full
+ elif jid in (None, ''):
+ local = True
if local:
vcard = self.api['get_vcard'](jid, None, ifrom)
diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py
index 952cad85..bec5f565 100644
--- a/sleekxmpp/plugins/xep_0060/pubsub.py
+++ b/sleekxmpp/plugins/xep_0060/pubsub.py
@@ -423,7 +423,7 @@ class XEP_0060(BasePlugin):
callback=None, timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub_owner']['configure']['node'] = node
- iq['pubsub_owner']['configure']['form'].values = config.values
+ iq['pubsub_owner']['configure'].append(config)
return iq.send(block=block, callback=callback, timeout=timeout)
def publish(self, jid, node, id=None, payload=None, options=None,
diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py
index b2fe3010..c1907a13 100644
--- a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py
@@ -74,7 +74,12 @@ class Item(ElementBase):
def set_payload(self, value):
del self['payload']
- self.append(value)
+ if isinstance(value, ElementBase):
+ if value.tag_name() in self.plugin_tag_map:
+ self.init_plugin(value.plugin_attrib, existing_xml=value.xml)
+ self.xml.append(value.xml)
+ else:
+ self.xml.append(value)
def get_payload(self):
childs = list(self.xml)
@@ -243,39 +248,6 @@ class PublishOptions(ElementBase):
self.parent().xml.remove(self.xml)
-class PubsubState(ElementBase):
- """This is an experimental pubsub extension."""
- namespace = 'http://jabber.org/protocol/psstate'
- name = 'state'
- plugin_attrib = 'psstate'
- interfaces = set(('node', 'item', 'payload'))
-
- def set_payload(self, value):
- self.xml.append(value)
-
- def get_payload(self):
- childs = list(self.xml)
- if len(childs) > 0:
- return childs[0]
-
- def del_payload(self):
- for child in self.xml:
- self.xml.remove(child)
-
-
-class PubsubStateEvent(ElementBase):
- """This is an experimental pubsub extension."""
- namespace = 'http://jabber.org/protocol/psstate#event'
- name = 'event'
- plugin_attrib = 'psstate_event'
- intefaces = set(tuple())
-
-
-register_stanza_plugin(Iq, PubsubState)
-register_stanza_plugin(Message, PubsubStateEvent)
-register_stanza_plugin(PubsubStateEvent, PubsubState)
-
-
register_stanza_plugin(Iq, Pubsub)
register_stanza_plugin(Pubsub, Affiliations)
register_stanza_plugin(Pubsub, Configure)
diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py
index 4a35db9d..d975a46d 100644
--- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py
+++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py
@@ -34,7 +34,8 @@ class DefaultConfig(ElementBase):
return self['form']
def set_config(self, value):
- self['form'].values = value.values
+ del self['from']
+ self.append(value)
return self
@@ -93,7 +94,9 @@ class OwnerRedirect(ElementBase):
class OwnerSubscriptions(Subscriptions):
+ name = 'subscriptions'
namespace = 'http://jabber.org/protocol/pubsub#owner'
+ plugin_attrib = name
interfaces = set(('node',))
def append(self, subscription):
diff --git a/sleekxmpp/plugins/xep_0065/__init__.py b/sleekxmpp/plugins/xep_0065/__init__.py
index c577d859..feca2ef1 100644
--- a/sleekxmpp/plugins/xep_0065/__init__.py
+++ b/sleekxmpp/plugins/xep_0065/__init__.py
@@ -1,4 +1,6 @@
from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0065.stanza import Socks5
from sleekxmpp.plugins.xep_0065.proxy import XEP_0065
diff --git a/sleekxmpp/plugins/xep_0065/proxy.py b/sleekxmpp/plugins/xep_0065/proxy.py
index b027e4e0..d890b57a 100644
--- a/sleekxmpp/plugins/xep_0065/proxy.py
+++ b/sleekxmpp/plugins/xep_0065/proxy.py
@@ -1,359 +1,292 @@
-import sys
import logging
-import struct
+import threading
+import socket
-from threading import Thread, Event
from hashlib import sha1
-from select import select
from uuid import uuid4
-from sleekxmpp.plugins.xep_0065 import stanza
+from sleekxmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.stanza import Iq
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5
+from sleekxmpp.plugins.base import base_plugin
+
+from sleekxmpp.plugins.xep_0065 import stanza, Socks5
+
-# Registers the sleekxmpp logger
log = logging.getLogger(__name__)
class XEP_0065(base_plugin):
- """
- XEP-0065 Socks5 Bytestreams
- """
- description = "Socks5 Bytestreams"
- dependencies = set(['xep_0030', ])
- xep = '0065'
name = 'xep_0065'
-
- # A dict contains for each SID, the proxy thread currently
- # running.
- proxy_threads = {}
+ description = "Socks5 Bytestreams"
+ dependencies = set(['xep_0030'])
+ default_config = {
+ 'auto_accept': False
+ }
def plugin_init(self):
- """ Initializes the xep_0065 plugin and all event callbacks.
- """
+ register_stanza_plugin(Iq, Socks5)
- # Shortcuts to access to the xep_0030 plugin.
- self.disco = self.xmpp['xep_0030']
+ self._proxies = {}
+ self._sessions = {}
+ self._sessions_lock = threading.Lock()
- # Handler for the streamhost stanza.
- self.xmpp.registerHandler(
+ self._preauthed_sids_lock = threading.Lock()
+ self._preauthed_sids = {}
+
+ self.xmpp.register_handler(
Callback('Socks5 Bytestreams',
StanzaPath('iq@type=set/socks/streamhost'),
self._handle_streamhost))
- # Handler for the streamhost-used stanza.
- self.xmpp.registerHandler(
- Callback('Socks5 Bytestreams',
- StanzaPath('iq@type=result/socks/streamhost-used'),
- self._handle_streamhost_used))
+ self.api.register(self._authorized, 'authorized', default=True)
+ self.api.register(self._authorized_sid, 'authorized_sid', default=True)
+ self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True)
- def get_socket(self, sid):
- """ Returns the socket associated to the SID.
- """
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Socks5.namespace)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Socks5 Bytestreams')
+ self.xmpp.remove_handler('Socks5 Streamhost Used')
+ self.xmpp['xep_0030'].del_feature(feature=Socks5.namespace)
- proxy = self.proxy_threads.get(sid)
- if proxy:
- return proxy.s
+ def get_socket(self, sid):
+ """Returns the socket associated to the SID."""
+ return self._sessions.get(sid, None)
- def handshake(self, to, streamer=None):
+ def handshake(self, to, ifrom=None, sid=None, timeout=None):
""" Starts the handshake to establish the socks5 bytestreams
connection.
"""
-
- # Discovers the proxy.
- self.streamer = streamer or self.discover_proxy()
-
- # Requester requests network address from the proxy.
- streamhost = self.get_network_address(self.streamer)
- self.proxy_host = streamhost['socks']['streamhost']['host']
- self.proxy_port = streamhost['socks']['streamhost']['port']
-
- # Generates the SID for this new handshake.
- sid = uuid4().hex
-
- # Requester initiates S5B negotation with Target by sending
+ if not self._proxies:
+ self._proxies = self.discover_proxies()
+
+ if sid is None:
+ sid = uuid4().hex
+
+ used = self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout)
+ proxy = used['socks']['streamhost_used']['jid']
+
+ if proxy not in self._proxies:
+ log.warning('Received unknown SOCKS5 proxy: %s', proxy)
+ return
+
+ with self._sessions_lock:
+ self._sessions[sid] = self._connect_proxy(
+ sid,
+ self.xmpp.boundjid,
+ to,
+ self._proxies[proxy][0],
+ self._proxies[proxy][1],
+ peer=to)
+
+ # Request that the proxy activate the session with the target.
+ self.activate(proxy, sid, to, timeout=timeout)
+ socket = self.get_socket(sid)
+ self.xmpp.event('stream:%s:%s' % (sid, to), socket)
+ return socket
+
+ def request_stream(self, to, sid=None, ifrom=None, block=True, timeout=None, callback=None):
+ if sid is None:
+ sid = uuid4().hex
+
+ # Requester initiates S5B negotiation with Target by sending
# IQ-set that includes the JabberID and network address of
# StreamHost as well as the StreamID (SID) of the proposed
# bytestream.
- iq = self.xmpp.Iq(sto=to, stype='set')
+ iq = self.xmpp.Iq()
+ iq['to'] = to
+ iq['from'] = ifrom
+ iq['type'] = 'set'
iq['socks']['sid'] = sid
- iq['socks']['streamhost']['jid'] = self.streamer
- iq['socks']['streamhost']['host'] = self.proxy_host
- iq['socks']['streamhost']['port'] = self.proxy_port
-
- # Sends the new IQ.
- return iq.send()
+ for proxy, (host, port) in self._proxies.items():
+ iq['socks'].add_streamhost(proxy, host, port)
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def discover_proxies(self, jid=None, ifrom=None, timeout=None):
+ """Auto-discover the JIDs of SOCKS5 proxies on an XMPP server."""
+ if jid is None:
+ if self.xmpp.is_component:
+ jid = self.xmpp.server
+ else:
+ jid = self.xmpp.boundjid.server
- def discover_proxy(self):
- """ Auto-discovers (using XEP 0030) the available bytestream
- proxy on the XMPP server.
+ discovered = set()
- Returns the JID of the proxy.
- """
-
- # Gets all disco items.
- disco_items = self.disco.get_items(self.xmpp.server)
+ disco_items = self.xmpp['xep_0030'].get_items(jid, timeout=timeout)
for item in disco_items['disco_items']['items']:
- # For each items, gets the disco info.
- disco_info = self.disco.get_info(item[0])
-
- # Gets and verifies if the identity is a bytestream proxy.
- identities = disco_info['disco_info']['identities']
- for identity in identities:
- if identity[0] == 'proxy' and identity[1] == 'bytestreams':
- # Returns when the first occurence is found.
- return '%s' % disco_info['from']
-
- def get_network_address(self, streamer):
- """ Gets the streamhost information of the proxy.
+ try:
+ disco_info = self.xmpp['xep_0030'].get_info(item[0], timeout=timeout)
+ except XMPPError:
+ continue
+ else:
+ # Verify that the identity is a bytestream proxy.
+ identities = disco_info['disco_info']['identities']
+ for identity in identities:
+ if identity[0] == 'proxy' and identity[1] == 'bytestreams':
+ discovered.add(disco_info['from'])
- streamer : The jid of the proxy.
- """
+ for jid in discovered:
+ try:
+ addr = self.get_network_address(jid, ifrom=ifrom, timeout=timeout)
+ self._proxies[jid] = (addr['socks']['streamhost']['host'],
+ addr['socks']['streamhost']['port'])
+ except XMPPError:
+ continue
- iq = self.xmpp.Iq(sto=streamer, stype='get')
- iq['socks'] # Adds the query eleme to the iq.
+ return self._proxies
- return iq.send()
+ def get_network_address(self, proxy, ifrom=None, block=True, timeout=None, callback=None):
+ """Get the network information of a proxy."""
+ iq = self.xmpp.Iq(sto=proxy, stype='get', sfrom=ifrom)
+ iq.enable('socks')
+ return iq.send(block=block, timeout=timeout, callback=callback)
def _handle_streamhost(self, iq):
- """ Handles all streamhost stanzas.
- """
-
- # Registers the streamhost info.
- self.streamer = iq['socks']['streamhost']['jid']
- self.proxy_host = iq['socks']['streamhost']['host']
- self.proxy_port = iq['socks']['streamhost']['port']
-
- # Sets the SID, the requester and the target.
- sid = iq['socks']['sid']
- requester = '%s' % iq['from']
- target = '%s' % self.xmpp.boundjid
-
- # Next the Target attempts to open a standard TCP socket on
- # the network address of the Proxy.
- self.proxy_thread = Proxy(sid, requester, target, self.proxy_host,
- self.proxy_port, self.on_recv)
- self.proxy_thread.start()
-
- # Registers the new thread in the proxy_thread dict.
- self.proxy_threads[sid] = self.proxy_thread
-
- # Wait until the proxy is connected
- self.proxy_thread.connected.wait()
-
- # Replies to the incoming iq with a streamhost-used stanza.
- res_iq = iq.reply()
- res_iq['socks']['sid'] = sid
- res_iq['socks']['streamhost-used']['jid'] = self.streamer
-
- # Sends the IQ
- return res_iq.send()
-
- def _handle_streamhost_used(self, iq):
- """ Handles all streamhost-used stanzas.
- """
-
- # Sets the SID, the requester and the target.
+ """Handle incoming SOCKS5 session request."""
sid = iq['socks']['sid']
- requester = '%s' % self.xmpp.boundjid
- target = '%s' % iq['from']
-
- # The Requester will establish a connection to the SOCKS5
- # proxy in the same way the Target did.
- self.proxy_thread = Proxy(sid, requester, target, self.proxy_host,
- self.proxy_port, self.on_recv)
- self.proxy_thread.start()
-
- # Registers the new thread in the proxy_thread dict.
- self.proxy_threads[sid] = self.proxy_thread
+ if not sid:
+ raise XMPPError(etype='modify', condition='bad-request')
- # Wait until the proxy is connected
- self.proxy_thread.connected.wait()
+ if not self._accept_stream(iq):
+ raise XMPPError(etype='modify', condition='not-acceptable')
- # Requester sends IQ-set to StreamHost requesting that
- # StreamHost activate the bytestream associated with the
- # StreamID.
- self.activate(iq['socks']['sid'], target)
+ streamhosts = iq['socks']['streamhosts']
+ conn = None
+ used_streamhost = None
- def activate(self, sid, to):
- """ IQ-set to StreamHost requesting that StreamHost activate
- the bytestream associated with the StreamID.
- """
-
- # Creates the activate IQ.
- act_iq = self.xmpp.Iq(sto=self.streamer, stype='set')
- act_iq['socks']['sid'] = sid
- act_iq['socks']['activate'] = to
-
- # Send the IQ.
- act_iq.send()
+ sender = iq['from']
+ for streamhost in streamhosts:
+ try:
+ conn = self._connect_proxy(sid,
+ sender,
+ self.xmpp.boundjid,
+ streamhost['host'],
+ streamhost['port'],
+ peer=sender)
+ used_streamhost = streamhost['jid']
+ break
+ except socket.error:
+ continue
+ else:
+ raise XMPPError(etype='cancel', condition='item-not-found')
+
+ iq.reply()
+ with self._sessions_lock:
+ self._sessions[sid] = conn
+ iq['socks']['sid'] = sid
+ iq['socks']['streamhost_used']['jid'] = used_streamhost
+ iq.send()
+ self.xmpp.event('socks5_stream', conn)
+ self.xmpp.event('stream:%s:%s' % (sid, conn.peer_jid), conn)
+
+ def activate(self, proxy, sid, target, ifrom=None, block=True, timeout=None, callback=None):
+ """Activate the socks5 session that has been negotiated."""
+ iq = self.xmpp.Iq(sto=proxy, stype='set', sfrom=ifrom)
+ iq['socks']['sid'] = sid
+ iq['socks']['activate'] = target
+ iq.send(block=block, timeout=timeout, callback=callback)
def deactivate(self, sid):
- """ Closes the Proxy thread associated to this SID.
- """
-
- proxy = self.proxy_threads.get(sid)
- if proxy:
- proxy.s.close()
- del self.proxy_threads[sid]
+ """Closes the proxy socket associated with this SID."""
+ sock = self._sessions.get(sid)
+ if sock:
+ try:
+ # sock.close() will also delete sid from self._sessions (see _connect_proxy)
+ sock.close()
+ except socket.error:
+ pass
+ # Though this should not be neccessary remove the closed session anyway
+ with self._sessions_lock:
+ if sid in self._sessions:
+ log.warn(('SOCKS5 session with sid = "%s" was not ' +
+ 'removed from _sessions by sock.close()') % sid)
+ del self._sessions[sid]
def close(self):
- """ Closes all Proxy threads.
- """
-
- for sid, proxy in self.proxy_threads.items():
- proxy.s.close()
- del self.proxy_threads[sid]
-
- def send(self, sid, data):
- """ Sends the data over the Proxy socket associated to the
- SID.
- """
-
- proxy = self.proxy_threads.get(sid)
- if proxy:
- proxy.s.sendall(data)
+ """Closes all proxy sockets."""
+ for sid, sock in self._sessions.items():
+ sock.close()
+ with self._sessions_lock:
+ self._sessions = {}
- def on_recv(self, sid, data):
- """ Calls when data is recv from the Proxy socket associated
- to the SID.
-
- Triggers a socks_closed event if the socket is closed. The sid
- is passed to this event.
-
- Triggers a socks_recv event if there's available data. A dict
- that contains the sid and the data is passed to this event.
- """
-
- proxy = self.proxy_threads.get(sid)
- if proxy:
- if not data:
- self.xmpp.event('socks_closed', sid)
- else:
- self.xmpp.event('socks_recv', {'sid': sid, 'data': data})
-
-
-class Proxy(Thread):
- """ Establishes in a thread a connection between the client and
- the server-side Socks5 proxy.
- """
-
- def __init__(self, sid, requester, target, proxy, proxy_port,
- on_recv):
- """ Initializes the proxy thread.
+ def _connect_proxy(self, sid, requester, target, proxy, proxy_port, peer=None):
+ """ Establishes a connection between the client and the server-side
+ Socks5 proxy.
sid : The StreamID. <str>
requester : The JID of the requester. <str>
target : The JID of the target. <str>
proxy_host : The hostname or the IP of the proxy. <str>
proxy_port : The port of the proxy. <str> or <int>
- on_recv : A callback called when data are received from the
- socket. <Callable>
+ peer : The JID for the other side of the stream, regardless
+ of target or requester status.
"""
-
- # Initializes the thread.
- Thread.__init__(self)
-
# Because the xep_0065 plugin uses the proxy_port as string,
# the Proxy class accepts the proxy_port argument as a string
# or an integer. Here, we force to use the port as an integer.
proxy_port = int(proxy_port)
- # Creates a connected event to warn when to proxy is
- # connected.
- self.connected = Event()
-
- # Registers the arguments.
- self.sid = sid
- self.requester = requester
- self.target = target
- self.proxy = proxy
- self.proxy_port = proxy_port
- self.on_recv = on_recv
-
- def run(self):
- """ Starts the thread.
- """
-
- # Creates the socks5 proxy socket
- self.s = socksocket()
- self.s.setproxy(PROXY_TYPE_SOCKS5, self.proxy, port=self.proxy_port)
+ sock = socksocket()
+ sock.setproxy(PROXY_TYPE_SOCKS5, proxy, port=proxy_port)
# The hostname MUST be SHA1(SID + Requester JID + Target JID)
# where the output is hexadecimal-encoded (not binary).
digest = sha1()
- digest.update(self.sid) # SID
- digest.update(self.requester) # Requester JID
- digest.update(self.target) # Target JID
+ digest.update(sid.encode('utf-8'))
+ digest.update(str(requester).encode('utf-8'))
+ digest.update(str(target).encode('utf-8'))
- # Computes the digest in hex.
- dest = '%s' % digest.hexdigest()
+ dest = digest.hexdigest()
# The port MUST be 0.
- self.s.connect((dest, 0))
+ sock.connect((dest, 0))
log.info('Socket connected.')
- self.connected.set()
- # Blocks until the socket need to be closed.
- self.listen()
+ _close = sock.close
+ def close(*args, **kwargs):
+ with self._sessions_lock:
+ if sid in self._sessions:
+ del self._sessions[sid]
+ _close()
+ log.info('Socket closed.')
+ sock.close = close
- # Closes the socket.
- self.s.close()
- log.info('Socket closed.')
+ sock.peer_jid = peer
+ sock.self_jid = target if requester == peer else requester
- def listen(self):
- """ Listen for data on the socket. When receiving data, call
- the callback on_recv callable.
- """
+ self.xmpp.event('socks_connected', sid)
+ return sock
- socket_open = True
- while socket_open:
- ins = []
- try:
- # Wait any read available data on socket. Timeout
- # after 5 secs.
- ins, out, err = select([self.s, ], [], [], 5)
- except Exception as e:
- # There's an error with the socket (maybe the socket
- # has been closed and the file descriptor is bad).
- log.debug('Socket error: %s' % e)
- break
+ def _accept_stream(self, iq):
+ receiver = iq['to']
+ sender = iq['from']
+ sid = iq['socks']['sid']
- for s in ins:
- data = self.recv_size(self.s)
- if not data:
- socket_open = False
-
- self.on_recv(self.sid, data)
-
- def recv_size(self, the_socket):
- total_len = 0
- total_data = []
- size = sys.maxint
- size_data = sock_data = ''
- recv_size = 8192
-
- while total_len < size:
- sock_data = the_socket.recv(recv_size)
- if not sock_data:
- return ''.join(total_data)
-
- if not total_data:
- if len(sock_data) > 4:
- size_data += sock_data
- size = struct.unpack('>i', size_data[:4])[0]
- recv_size = size
- if recv_size > 524288:
- recv_size = 524288
- total_data.append(size_data[4:])
- else:
- size_data += sock_data
- else:
- total_data.append(sock_data)
- total_len = sum([len(i) for i in total_data])
- return ''.join(total_data)
+ if self.api['authorized_sid'](receiver, sid, sender, iq):
+ return True
+ return self.api['authorized'](receiver, sid, sender, iq)
+
+ def _authorized(self, jid, sid, ifrom, iq):
+ return self.auto_accept
+
+ def _authorized_sid(self, jid, sid, ifrom, iq):
+ with self._preauthed_sids_lock:
+ log.debug('>>> authed sids: %s', self._preauthed_sids)
+ log.debug('>>> lookup: %s %s %s', jid, sid, ifrom)
+ if (jid, sid, ifrom) in self._preauthed_sids:
+ del self._preauthed_sids[(jid, sid, ifrom)]
+ return True
+ return False
+
+ def _preauthorize_sid(self, jid, sid, ifrom, data):
+ log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data)
+ with self._preauthed_sids_lock:
+ self._preauthed_sids[(jid, sid, ifrom)] = True
diff --git a/sleekxmpp/plugins/xep_0065/stanza.py b/sleekxmpp/plugins/xep_0065/stanza.py
index ae57aba8..e48bf1b5 100644
--- a/sleekxmpp/plugins/xep_0065/stanza.py
+++ b/sleekxmpp/plugins/xep_0065/stanza.py
@@ -1,41 +1,47 @@
-from sleekxmpp import Iq
+from sleekxmpp.jid import JID
from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
-# The protocol namespace defined in the Socks5Bytestream (0065) spec.
-namespace = 'http://jabber.org/protocol/bytestreams'
+class Socks5(ElementBase):
+ name = 'query'
+ namespace = 'http://jabber.org/protocol/bytestreams'
+ plugin_attrib = 'socks'
+ interfaces = set(['sid', 'activate'])
+ sub_interfaces = set(['activate'])
+ def add_streamhost(self, jid, host, port):
+ sh = StreamHost(parent=self)
+ sh['jid'] = jid
+ sh['host'] = host
+ sh['port'] = port
-class StreamHost(ElementBase):
- """ The streamhost xml element.
- """
- namespace = namespace
+class StreamHost(ElementBase):
name = 'streamhost'
+ namespace = 'http://jabber.org/protocol/bytestreams'
plugin_attrib = 'streamhost'
- interfaces = set(('host', 'jid', 'port'))
+ plugin_multi_attrib = 'streamhosts'
+ interfaces = set(['host', 'jid', 'port'])
+ def set_jid(self, value):
+ return self._set_attr('jid', str(value))
-class StreamHostUsed(ElementBase):
- """ The streamhost-used xml element.
- """
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
- namespace = namespace
+
+class StreamHostUsed(ElementBase):
name = 'streamhost-used'
- plugin_attrib = 'streamhost-used'
- interfaces = set(('jid',))
+ namespace = 'http://jabber.org/protocol/bytestreams'
+ plugin_attrib = 'streamhost_used'
+ interfaces = set(['jid'])
+ def set_jid(self, value):
+ return self._set_attr('jid', str(value))
-class Socks5(ElementBase):
- """ The query xml element.
- """
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
- namespace = namespace
- name = 'query'
- plugin_attrib = 'socks'
- interfaces = set(('sid', 'activate'))
- sub_interfaces = set(('activate',))
-register_stanza_plugin(Iq, Socks5)
-register_stanza_plugin(Socks5, StreamHost)
+register_stanza_plugin(Socks5, StreamHost, iterable=True)
register_stanza_plugin(Socks5, StreamHostUsed)
diff --git a/sleekxmpp/plugins/xep_0071/__init__.py b/sleekxmpp/plugins/xep_0071/__init__.py
new file mode 100644
index 00000000..c21e9265
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0071/__init__.py
@@ -0,0 +1,15 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permissio
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0071.stanza import XHTML_IM
+from sleekxmpp.plugins.xep_0071.xhtml_im import XEP_0071
+
+
+register_plugin(XEP_0071)
diff --git a/sleekxmpp/plugins/xep_0071/stanza.py b/sleekxmpp/plugins/xep_0071/stanza.py
new file mode 100644
index 00000000..d5ff1a1b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0071/stanza.py
@@ -0,0 +1,81 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.stanza import Message
+from sleekxmpp.util import unicode
+from sleekxmpp.thirdparty import OrderedDict
+from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, tostring
+
+
+XHTML_NS = 'http://www.w3.org/1999/xhtml'
+
+
+class XHTML_IM(ElementBase):
+
+ namespace = 'http://jabber.org/protocol/xhtml-im'
+ name = 'html'
+ interfaces = set(['body'])
+ lang_interfaces = set(['body'])
+ plugin_attrib = name
+
+ def set_body(self, content, lang=None):
+ if lang is None:
+ lang = self.get_lang()
+ self.del_body(lang)
+ if lang == '*':
+ for sublang, subcontent in content.items():
+ self.set_body(subcontent, sublang)
+ else:
+ if isinstance(content, type(ET.Element('test'))):
+ content = unicode(ET.tostring(content))
+ else:
+ content = unicode(content)
+ header = '<body xmlns="%s"' % XHTML_NS
+ if lang:
+ header = '%s xml:lang="%s"' % (header, lang)
+ content = '%s>%s</body>' % (header, content)
+ xhtml = ET.fromstring(content)
+ self.xml.append(xhtml)
+
+ def get_body(self, lang=None):
+ """Return the contents of the HTML body."""
+ if lang is None:
+ lang = self.get_lang()
+
+ bodies = self.xml.findall('{%s}body' % XHTML_NS)
+
+ if lang == '*':
+ result = OrderedDict()
+ for body in bodies:
+ body_lang = body.attrib.get('{%s}lang' % self.xml_ns, '')
+ body_result = []
+ body_result.append(body.text if body.text else '')
+ for child in body:
+ body_result.append(tostring(child, xmlns=XHTML_NS))
+ body_result.append(body.tail if body.tail else '')
+ result[body_lang] = ''.join(body_result)
+ return result
+ else:
+ for body in bodies:
+ if body.attrib.get('{%s}lang' % self.xml_ns, self.get_lang()) == lang:
+ result = []
+ result.append(body.text if body.text else '')
+ for child in body:
+ result.append(tostring(child, xmlns=XHTML_NS))
+ result.append(body.tail if body.tail else '')
+ return ''.join(result)
+ return ''
+
+ def del_body(self, lang=None):
+ if lang is None:
+ lang = self.get_lang()
+ bodies = self.xml.findall('{%s}body' % XHTML_NS)
+ for body in bodies:
+ if body.attrib.get('{%s}lang' % self.xml_ns, self.get_lang()) == lang:
+ self.xml.remove(body)
+ return
diff --git a/sleekxmpp/plugins/xep_0071/xhtml_im.py b/sleekxmpp/plugins/xep_0071/xhtml_im.py
new file mode 100644
index 00000000..096a00aa
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0071/xhtml_im.py
@@ -0,0 +1,30 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+
+from sleekxmpp.stanza import Message
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0071 import stanza, XHTML_IM
+
+
+class XEP_0071(BasePlugin):
+
+ name = 'xep_0071'
+ description = 'XEP-0071: XHTML-IM'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, XHTML_IM)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(feature=XHTML_IM.namespace)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=XHTML_IM.namespace)
diff --git a/sleekxmpp/plugins/xep_0077/register.py b/sleekxmpp/plugins/xep_0077/register.py
index d4da21a5..ee07548b 100644
--- a/sleekxmpp/plugins/xep_0077/register.py
+++ b/sleekxmpp/plugins/xep_0077/register.py
@@ -7,6 +7,7 @@
"""
import logging
+import ssl
from sleekxmpp.stanza import StreamFeatures, Iq
from sleekxmpp.xmlstream import register_stanza_plugin, JID
@@ -29,6 +30,7 @@ class XEP_0077(BasePlugin):
stanza = stanza
default_config = {
'create_account': True,
+ 'force_registration': False,
'order': 50
}
@@ -45,10 +47,29 @@ class XEP_0077(BasePlugin):
register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form)
register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB)
+ self.xmpp.add_event_handler('connected', self._force_registration)
+
def plugin_end(self):
if not self.xmpp.is_component:
self.xmpp.unregister_feature('register', self.order)
+ def _force_registration(self, event):
+ if self.force_registration:
+ self.xmpp.add_filter('in', self._force_stream_feature)
+
+ def _force_stream_feature(self, stanza):
+ if isinstance(stanza, StreamFeatures):
+ if self.xmpp.use_tls or self.xmpp.use_ssl:
+ if 'starttls' not in self.xmpp.features:
+ return stanza
+ elif not isinstance(self.xmpp.socket, ssl.SSLSocket):
+ return stanza
+ if 'mechanisms' not in self.xmpp.features:
+ log.debug('Forced adding in-band registration stream feature')
+ stanza.enable('register')
+ self.xmpp.del_filter('in', self._force_stream_feature)
+ return stanza
+
def _handle_register_feature(self, features):
if 'mechanisms' in self.xmpp.features:
# We have already logged in with an account
diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py
index 7e2d7bdf..da6bfa2c 100644
--- a/sleekxmpp/plugins/xep_0078/legacyauth.py
+++ b/sleekxmpp/plugins/xep_0078/legacyauth.py
@@ -6,6 +6,7 @@
See the file LICENSE for copying permission.
"""
+import uuid
import logging
import hashlib
import random
@@ -98,7 +99,7 @@ class XEP_0078(BasePlugin):
# A resource is required, so create a random one if necessary
resource = self.xmpp.requested_jid.resource
if not resource:
- resource = uuid.uuid4()
+ resource = str(uuid.uuid4())
iq['auth']['resource'] = resource
diff --git a/sleekxmpp/plugins/xep_0079/__init__.py b/sleekxmpp/plugins/xep_0079/__init__.py
new file mode 100644
index 00000000..09e66715
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0079/__init__.py
@@ -0,0 +1,18 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0079.stanza import (
+ AMP, Rule, InvalidRules, UnsupportedConditions,
+ UnsupportedActions, FailedRules, FailedRule,
+ AMPFeature)
+from sleekxmpp.plugins.xep_0079.amp import XEP_0079
+
+
+register_plugin(XEP_0079)
diff --git a/sleekxmpp/plugins/xep_0079/amp.py b/sleekxmpp/plugins/xep_0079/amp.py
new file mode 100644
index 00000000..918fb841
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0079/amp.py
@@ -0,0 +1,79 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permissio
+"""
+
+import logging
+
+from sleekxmpp.stanza import Message, Error, StreamFeatures
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.matcher import StanzaPath, MatchMany
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0079 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0079(BasePlugin):
+
+ """
+ XEP-0079 Advanced Message Processing
+ """
+
+ name = 'xep_0079'
+ description = 'XEP-0079: Advanced Message Processing'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, stanza.AMP)
+ register_stanza_plugin(Error, stanza.InvalidRules)
+ register_stanza_plugin(Error, stanza.UnsupportedConditions)
+ register_stanza_plugin(Error, stanza.UnsupportedActions)
+ register_stanza_plugin(Error, stanza.FailedRules)
+
+ self.xmpp.register_handler(
+ Callback('AMP Response',
+ MatchMany([
+ StanzaPath('message/error/failed_rules'),
+ StanzaPath('message/amp')
+ ]),
+ self._handle_amp_response))
+
+ if not self.xmpp.is_component:
+ self.xmpp.register_feature('amp',
+ self._handle_amp_feature,
+ restart=False,
+ order=9000)
+ register_stanza_plugin(StreamFeatures, stanza.AMPFeature)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('AMP Response')
+
+ def _handle_amp_response(self, msg):
+ log.debug('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>')
+ if msg['type'] == 'error':
+ self.xmpp.event('amp_error', msg)
+ elif msg['amp']['status'] in ('alert', 'notify'):
+ self.xmpp.event('amp_%s' % msg['amp']['status'], msg)
+
+ def _handle_amp_feature(self, features):
+ log.debug('Advanced Message Processing is available.')
+ self.xmpp.features.add('amp')
+
+ def discover_support(self, jid=None, **iqargs):
+ if jid is None:
+ if self.xmpp.is_component:
+ jid = self.xmpp.server_host
+ else:
+ jid = self.xmpp.boundjid.host
+
+ return self.xmpp['xep_0030'].get_info(
+ jid=jid,
+ node='http://jabber.org/protocol/amp',
+ **iqargs)
diff --git a/sleekxmpp/plugins/xep_0079/stanza.py b/sleekxmpp/plugins/xep_0079/stanza.py
new file mode 100644
index 00000000..cb6932d6
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0079/stanza.py
@@ -0,0 +1,96 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from __future__ import unicode_literals
+
+from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class AMP(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'amp'
+ plugin_attrib = 'amp'
+ interfaces = set(['from', 'to', 'status', 'per_hop'])
+
+ def get_from(self):
+ return JID(self._get_attr('from'))
+
+ def set_from(self, value):
+ return self._set_attr('from', str(value))
+
+ def get_to(self):
+ return JID(self._get_attr('from'))
+
+ def set_to(self, value):
+ return self._set_attr('to', str(value))
+
+ def get_per_hop(self):
+ return self._get_attr('per-hop') == 'true'
+
+ def set_per_hop(self, value):
+ if value:
+ return self._set_attr('per-hop', 'true')
+ else:
+ return self._del_attr('per-hop')
+
+ def del_per_hop(self):
+ return self._del_attr('per-hop')
+
+ def add_rule(self, action, condition, value):
+ rule = Rule(parent=self)
+ rule['action'] = action
+ rule['condition'] = condition
+ rule['value'] = value
+
+
+class Rule(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'rule'
+ plugin_attrib = name
+ plugin_multi_attrib = 'rules'
+ interfaces = set(['action', 'condition', 'value'])
+
+
+class InvalidRules(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'invalid-rules'
+ plugin_attrib = 'invalid_rules'
+
+
+class UnsupportedConditions(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'unsupported-conditions'
+ plugin_attrib = 'unsupported_conditions'
+
+
+class UnsupportedActions(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp'
+ name = 'unsupported-actions'
+ plugin_attrib = 'unsupported_actions'
+
+
+class FailedRule(Rule):
+ namespace = 'http://jabber.org/protocol/amp#errors'
+
+
+class FailedRules(ElementBase):
+ namespace = 'http://jabber.org/protocol/amp#errors'
+ name = 'failed-rules'
+ plugin_attrib = 'failed_rules'
+
+
+class AMPFeature(ElementBase):
+ namespace = 'http://jabber.org/features/amp'
+ name = 'amp'
+
+
+register_stanza_plugin(AMP, Rule, iterable=True)
+register_stanza_plugin(InvalidRules, Rule, iterable=True)
+register_stanza_plugin(UnsupportedConditions, Rule, iterable=True)
+register_stanza_plugin(UnsupportedActions, Rule, iterable=True)
+register_stanza_plugin(FailedRules, FailedRule, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py
index 02571fa7..26eb68fa 100644
--- a/sleekxmpp/plugins/xep_0082.py
+++ b/sleekxmpp/plugins/xep_0082.py
@@ -6,7 +6,6 @@
See the file LICENSE for copying permission.
"""
-import logging
import datetime as dt
from sleekxmpp.plugins import BasePlugin, register_plugin
diff --git a/sleekxmpp/plugins/xep_0084/avatar.py b/sleekxmpp/plugins/xep_0084/avatar.py
index 2454afc7..677a888d 100644
--- a/sleekxmpp/plugins/xep_0084/avatar.py
+++ b/sleekxmpp/plugins/xep_0084/avatar.py
@@ -82,6 +82,7 @@ class XEP_0084(BasePlugin):
metadata.add_pointer(pointer)
return self.xmpp['xep_0163'].publish(metadata,
+ id=info['id'],
ifrom=ifrom,
block=block,
callback=callback,
diff --git a/sleekxmpp/plugins/xep_0084/stanza.py b/sleekxmpp/plugins/xep_0084/stanza.py
index 22f11b72..fd21e6f1 100644
--- a/sleekxmpp/plugins/xep_0084/stanza.py
+++ b/sleekxmpp/plugins/xep_0084/stanza.py
@@ -8,7 +8,7 @@
from base64 import b64encode, b64decode
-from sleekxmpp.util import bytes
+from sleekxmpp.util import bytes as sbytes
from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
@@ -20,12 +20,15 @@ class Data(ElementBase):
def get_value(self):
if self.xml.text:
- return b64decode(bytes(self.xml.text))
+ return b64decode(sbytes(self.xml.text))
return ''
def set_value(self, value):
if value:
- self.xml.text = b64encode(bytes(value))
+ self.xml.text = b64encode(sbytes(value))
+ # Python3 base64 encoded is bytes and needs to be decoded to string
+ if isinstance(self.xml.text, bytes):
+ self.xml.text = self.xml.text.decode()
else:
self.xml.text = ''
diff --git a/sleekxmpp/plugins/xep_0091/stanza.py b/sleekxmpp/plugins/xep_0091/stanza.py
index 0b70ff63..17e55764 100644
--- a/sleekxmpp/plugins/xep_0091/stanza.py
+++ b/sleekxmpp/plugins/xep_0091/stanza.py
@@ -21,14 +21,15 @@ class LegacyDelay(ElementBase):
interfaces = set(('from', 'stamp', 'text'))
def get_from(self):
- return JID(self._get_attr('from'))
+ from_ = self._get_attr('from')
+ return JID(from_) if from_ else None
def set_from(self, value):
self._set_attr('from', str(value))
def get_stamp(self):
timestamp = self._get_attr('stamp')
- return xep_0082.parse('%sZ' % timestamp)
+ return xep_0082.parse('%sZ' % timestamp) if timestamp else None
def set_stamp(self, value):
if isinstance(value, dt.datetime):
diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py
index 35813e1d..b16ad516 100644
--- a/sleekxmpp/plugins/xep_0092/version.py
+++ b/sleekxmpp/plugins/xep_0092/version.py
@@ -70,7 +70,7 @@ class XEP_0092(BasePlugin):
iq['software_version']['os'] = self.os
iq.send()
- def get_version(self, jid, ifrom=None):
+ def get_version(self, jid, ifrom=None, block=True, timeout=None, callback=None):
"""
Retrieve the software version of a remote agent.
@@ -82,14 +82,4 @@ class XEP_0092(BasePlugin):
iq['from'] = ifrom
iq['type'] = 'get'
iq['query'] = Version.namespace
-
- result = iq.send()
-
- if result and result['type'] != 'error':
- values = result['software_version'].values
- del values['lang']
- return values
- return False
-
-
-XEP_0092.getVersion = XEP_0092.get_version
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0095/__init__.py b/sleekxmpp/plugins/xep_0095/__init__.py
new file mode 100644
index 00000000..4465ef5c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0095/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0095 import stanza
+from sleekxmpp.plugins.xep_0095.stanza import SI
+from sleekxmpp.plugins.xep_0095.stream_initiation import XEP_0095
+
+
+register_plugin(XEP_0095)
diff --git a/sleekxmpp/plugins/xep_0095/stanza.py b/sleekxmpp/plugins/xep_0095/stanza.py
new file mode 100644
index 00000000..34999a11
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0095/stanza.py
@@ -0,0 +1,25 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class SI(ElementBase):
+ name = 'si'
+ namespace = 'http://jabber.org/protocol/si'
+ plugin_attrib = 'si'
+ interfaces = set(['id', 'mime_type', 'profile'])
+
+ def get_mime_type(self):
+ return self._get_attr('mime-type', 'application/octet-stream')
+
+ def set_mime_type(self, value):
+ self._set_attr('mime-type', value)
+
+ def del_mime_type(self):
+ self._del_attr('mime-type')
diff --git a/sleekxmpp/plugins/xep_0095/stream_initiation.py b/sleekxmpp/plugins/xep_0095/stream_initiation.py
new file mode 100644
index 00000000..927248a5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0095/stream_initiation.py
@@ -0,0 +1,214 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import threading
+
+from uuid import uuid4
+
+from sleekxmpp import Iq, Message
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, JID
+from sleekxmpp.plugins.xep_0095 import stanza, SI
+
+
+log = logging.getLogger(__name__)
+
+
+SOCKS5 = 'http://jabber.org/protocol/bytestreams'
+IBB = 'http://jabber.org/protocol/ibb'
+
+
+class XEP_0095(BasePlugin):
+
+ name = 'xep_0095'
+ description = 'XEP-0095: Stream Initiation'
+ dependencies = set(['xep_0020', 'xep_0030', 'xep_0047', 'xep_0065'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self._profiles = {}
+ self._methods = {}
+ self._methods_order = []
+ self._pending_lock = threading.Lock()
+ self._pending= {}
+
+ self.register_method(SOCKS5, 'xep_0065', 100)
+ self.register_method(IBB, 'xep_0047', 50)
+
+ register_stanza_plugin(Iq, SI)
+ register_stanza_plugin(SI, self.xmpp['xep_0020'].stanza.FeatureNegotiation)
+
+ self.xmpp.register_handler(
+ Callback('SI Request',
+ StanzaPath('iq@type=set/si'),
+ self._handle_request))
+
+ self.api.register(self._add_pending, 'add_pending', default=True)
+ self.api.register(self._get_pending, 'get_pending', default=True)
+ self.api.register(self._del_pending, 'del_pending', default=True)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(SI.namespace)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('SI Request')
+ self.xmpp['xep_0030'].del_feature(feature=SI.namespace)
+
+ def register_profile(self, profile_name, plugin):
+ self._profiles[profile_name] = plugin
+
+ def unregister_profile(self, profile_name):
+ try:
+ del self._profiles[profile_name]
+ except KeyError:
+ pass
+
+ def register_method(self, method, plugin_name, order=50):
+ self._methods[method] = (plugin_name, order)
+ self._methods_order.append((order, method, plugin_name))
+ self._methods_order.sort()
+
+ def unregister_method(self, method):
+ if method in self._methods:
+ plugin_name, order = self._methods[method]
+ del self._methods[method]
+ self._methods_order.remove((order, method, plugin_name))
+ self._methods_order.sort()
+
+ def _handle_request(self, iq):
+ profile = iq['si']['profile']
+ sid = iq['si']['id']
+
+ if not sid:
+ raise XMPPError(etype='modify', condition='bad-request')
+ if profile not in self._profiles:
+ raise XMPPError(
+ etype='modify',
+ condition='bad-request',
+ extension='bad-profile',
+ extension_ns=SI.namespace)
+
+ neg = iq['si']['feature_neg']['form']['fields']
+ options = neg['stream-method']['options'] or []
+ methods = []
+ for opt in options:
+ methods.append(opt['value'])
+ for method in methods:
+ if method in self._methods:
+ supported = True
+ break
+ else:
+ raise XMPPError('bad-request',
+ extension='no-valid-streams',
+ extension_ns=SI.namespace)
+
+ selected_method = None
+ log.debug('Available: %s', methods)
+ for order, method, plugin in self._methods_order:
+ log.debug('Testing: %s', method)
+ if method in methods:
+ selected_method = method
+ break
+
+ receiver = iq['to']
+ sender = iq['from']
+
+ self.api['add_pending'](receiver, sid, sender, {
+ 'response_id': iq['id'],
+ 'method': selected_method,
+ 'profile': profile
+ })
+ self.xmpp.event('si_request', iq)
+
+ def offer(self, jid, sid=None, mime_type=None, profile=None,
+ methods=None, payload=None, ifrom=None,
+ **iqargs):
+ if sid is None:
+ sid = uuid4().hex
+ if methods is None:
+ methods = list(self._methods.keys())
+ if not isinstance(methods, (list, tuple, set)):
+ methods = [methods]
+
+ si = self.xmpp.Iq()
+ si['to'] = jid
+ si['from'] = ifrom
+ si['type'] = 'set'
+ si['si']['id'] = sid
+ si['si']['mime_type'] = mime_type
+ si['si']['profile'] = profile
+ if not isinstance(payload, (list, tuple, set)):
+ payload = [payload]
+ for item in payload:
+ si['si'].append(item)
+ si['si']['feature_neg']['form'].add_field(
+ var='stream-method',
+ ftype='list-single',
+ options=methods)
+ return si.send(**iqargs)
+
+ def accept(self, jid, sid, payload=None, ifrom=None, stream_handler=None):
+ stream = self.api['get_pending'](ifrom, sid, jid)
+ iq = self.xmpp.Iq()
+ iq['id'] = stream['response_id']
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'result'
+ if payload:
+ iq['si'].append(payload)
+ iq['si']['feature_neg']['form']['type'] = 'submit'
+ iq['si']['feature_neg']['form'].add_field(
+ var='stream-method',
+ ftype='list-single',
+ value=stream['method'])
+
+ if ifrom is None:
+ ifrom = self.xmpp.boundjid
+
+ method_plugin = self._methods[stream['method']][0]
+ self.xmpp[method_plugin].api['preauthorize_sid'](ifrom, sid, jid)
+
+ self.api['del_pending'](ifrom, sid, jid)
+
+ if stream_handler:
+ self.xmpp.add_event_handler('stream:%s:%s' % (sid, jid),
+ stream_handler,
+ threaded=True,
+ disposable=True)
+ return iq.send()
+
+ def decline(self, jid, sid, ifrom=None):
+ stream = self.api['get_pending'](ifrom, sid, jid)
+ if not stream:
+ return
+ iq = self.xmpp.Iq()
+ iq['id'] = stream['response_id']
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'error'
+ iq['error']['condition'] = 'forbidden'
+ iq['error']['text'] = 'Offer declined'
+ self.api['del_pending'](ifrom, sid, jid)
+ return iq.send()
+
+ def _add_pending(self, jid, node, ifrom, data):
+ with self._pending_lock:
+ self._pending[(jid, node, ifrom)] = data
+
+ def _get_pending(self, jid, node, ifrom, data):
+ with self._pending_lock:
+ return self._pending.get((jid, node, ifrom), None)
+
+ def _del_pending(self, jid, node, ifrom, data):
+ with self._pending_lock:
+ if (jid, node, ifrom) in self._pending:
+ del self._pending[(jid, node, ifrom)]
diff --git a/sleekxmpp/plugins/xep_0096/__init__.py b/sleekxmpp/plugins/xep_0096/__init__.py
new file mode 100644
index 00000000..5f836169
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0096/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0096 import stanza
+from sleekxmpp.plugins.xep_0096.stanza import File
+from sleekxmpp.plugins.xep_0096.file_transfer import XEP_0096
+
+
+register_plugin(XEP_0096)
diff --git a/sleekxmpp/plugins/xep_0096/file_transfer.py b/sleekxmpp/plugins/xep_0096/file_transfer.py
new file mode 100644
index 00000000..6873c7f5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0096/file_transfer.py
@@ -0,0 +1,58 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp import Iq, Message
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin, JID
+from sleekxmpp.plugins.xep_0096 import stanza, File
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0096(BasePlugin):
+
+ name = 'xep_0096'
+ description = 'XEP-0096: SI File Transfer'
+ dependencies = set(['xep_0095'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(self.xmpp['xep_0095'].stanza.SI, File)
+
+ self.xmpp['xep_0095'].register_profile(File.namespace, self)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(File.namespace)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=File.namespace)
+ self.xmpp['xep_0095'].unregister_profile(File.namespace, self)
+
+ def request_file_transfer(self, jid, sid=None, name=None, size=None,
+ desc=None, hash=None, date=None,
+ allow_ranged=False, mime_type=None,
+ **iqargs):
+ data = File()
+ data['name'] = name
+ data['size'] = size
+ data['date'] = date
+ data['desc'] = desc
+ if allow_ranged:
+ data.enable('range')
+
+ return self.xmpp['xep_0095'].offer(jid,
+ sid=sid,
+ mime_type=mime_type,
+ profile=File.namespace,
+ payload=data,
+ **iqargs)
diff --git a/sleekxmpp/plugins/xep_0096/stanza.py b/sleekxmpp/plugins/xep_0096/stanza.py
new file mode 100644
index 00000000..65eb5bc5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0096/stanza.py
@@ -0,0 +1,48 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
+from sleekxmpp.plugins import xep_0082
+
+
+class File(ElementBase):
+ name = 'file'
+ namespace = 'http://jabber.org/protocol/si/profile/file-transfer'
+ plugin_attrib = 'file'
+ interfaces = set(['name', 'size', 'date', 'hash', 'desc'])
+ sub_interfaces = set(['desc'])
+
+ def set_size(self, value):
+ self._set_attr('size', str(value))
+
+ def get_date(self):
+ timestamp = self._get_attr('date')
+ return xep_0082.parse(timestamp)
+
+ def set_date(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_attr('date', value)
+
+
+class Range(ElementBase):
+ name = 'range'
+ namespace = 'http://jabber.org/protocol/si/profile/file-transfer'
+ plugin_attrib = 'range'
+ interfaces = set(['length', 'offset'])
+
+ def set_length(self, value):
+ self._set_attr('length', str(value))
+
+ def set_offset(self, value):
+ self._set_attr('offset', str(value))
+
+
+register_stanza_plugin(File, Range)
diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py
index b7a346c0..41b5c52e 100644
--- a/sleekxmpp/plugins/xep_0115/caps.py
+++ b/sleekxmpp/plugins/xep_0115/caps.py
@@ -9,8 +9,9 @@
import logging
import hashlib
import base64
+import threading
-import sleekxmpp
+from sleekxmpp import __version__
from sleekxmpp.stanza import StreamFeatures, Presence, Iq
from sleekxmpp.xmlstream import register_stanza_plugin, JID
from sleekxmpp.xmlstream.handler import Callback
@@ -45,8 +46,7 @@ class XEP_0115(BasePlugin):
'md5': hashlib.md5}
if self.caps_node is None:
- ver = sleekxmpp.__version__
- self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver
+ self.caps_node = 'http://sleekxmpp.com/ver/%s' % __version__
register_stanza_plugin(Presence, stanza.Capabilities)
register_stanza_plugin(StreamFeatures, stanza.Capabilities)
@@ -90,6 +90,9 @@ class XEP_0115(BasePlugin):
disco.assign_verstring = self.assign_verstring
disco.get_verstring = self.get_verstring
+ self._processing_lock = threading.Lock()
+ self._processing = set()
+
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace)
self.xmpp.del_filter('out', self._filter_add_caps)
@@ -135,17 +138,22 @@ class XEP_0115(BasePlugin):
def _process_caps(self, pres):
if not pres['caps']['hash']:
- log.debug("Received unsupported legacy caps.")
+ log.debug("Received unsupported legacy caps: %s, %s, %s",
+ pres['caps']['node'],
+ pres['caps']['ver'],
+ pres['caps']['ext'])
self.xmpp.event('entity_caps_legacy', pres)
return
+ ver = pres['caps']['ver']
+
existing_verstring = self.get_verstring(pres['from'].full)
- if str(existing_verstring) == str(pres['caps']['ver']):
+ if str(existing_verstring) == str(ver):
return
- existing_caps = self.get_caps(verstring=pres['caps']['ver'])
+ existing_caps = self.get_caps(verstring=ver)
if existing_caps is not None:
- self.assign_verstring(pres['from'], pres['caps']['ver'])
+ self.assign_verstring(pres['from'], ver)
return
if pres['caps']['hash'] not in self.hashes:
@@ -156,9 +164,16 @@ class XEP_0115(BasePlugin):
except XMPPError:
return
- log.debug("New caps verification string: %s", pres['caps']['ver'])
+ # Only lookup the same caps once at a time.
+ with self._processing_lock:
+ if ver in self._processing:
+ log.debug('Already processing verstring %s' % ver)
+ return
+ self._processing.add(ver)
+
+ log.debug("New caps verification string: %s", ver)
try:
- node = '%s#%s' % (pres['caps']['node'], pres['caps']['ver'])
+ node = '%s#%s' % (pres['caps']['node'], ver)
caps = self.xmpp['xep_0030'].get_info(pres['from'], node)
if isinstance(caps, Iq):
@@ -168,7 +183,10 @@ class XEP_0115(BasePlugin):
pres['caps']['ver']):
self.assign_verstring(pres['from'], pres['caps']['ver'])
except XMPPError:
- log.debug("Could not retrieve disco#info results for caps")
+ log.debug("Could not retrieve disco#info results for caps for %s", node)
+
+ with self._processing_lock:
+ self._processing.remove(ver)
def _validate_caps(self, caps, hash, check_verstring):
# Check Identities
@@ -179,7 +197,6 @@ class XEP_0115(BasePlugin):
return False
# Check Features
-
full_features = caps.get_features(dedupe=False)
deduped_features = caps.get_features()
if len(full_features) != len(deduped_features):
@@ -190,29 +207,32 @@ class XEP_0115(BasePlugin):
form_types = []
deduped_form_types = set()
for stanza in caps['substanzas']:
- if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
- if 'FORM_TYPE' in stanza['fields']:
- f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
- form_types.append(f_type)
- deduped_form_types.add(f_type)
- if len(form_types) != len(deduped_form_types):
- log.debug("Duplicated FORM_TYPE values, " + \
- "invalid for caps")
+ if not isinstance(stanza, self.xmpp['xep_0004'].stanza.Form):
+ log.debug("Non form extension found, ignoring for caps")
+ caps.xml.remove(stanza.xml)
+ continue
+ if 'FORM_TYPE' in stanza['fields']:
+ f_type = tuple(stanza['fields']['FORM_TYPE']['value'])
+ form_types.append(f_type)
+ deduped_form_types.add(f_type)
+ if len(form_types) != len(deduped_form_types):
+ log.debug("Duplicated FORM_TYPE values, " + \
+ "invalid for caps")
+ return False
+
+ if len(f_type) > 1:
+ deduped_type = set(f_type)
+ if len(f_type) != len(deduped_type):
+ log.debug("Extra FORM_TYPE data, invalid for caps")
return False
- if len(f_type) > 1:
- deduped_type = set(f_type)
- if len(f_type) != len(deduped_type):
- log.debug("Extra FORM_TYPE data, invalid for caps")
- return False
-
- if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
- log.debug("Field FORM_TYPE type not 'hidden', " + \
- "ignoring form for caps")
- caps.xml.remove(stanza.xml)
- else:
- log.debug("No FORM_TYPE found, ignoring form for caps")
+ if stanza['fields']['FORM_TYPE']['type'] != 'hidden':
+ log.debug("Field FORM_TYPE type not 'hidden', " + \
+ "ignoring form for caps")
caps.xml.remove(stanza.xml)
+ else:
+ log.debug("No FORM_TYPE found, ignoring form for caps")
+ caps.xml.remove(stanza.xml)
verstring = self.generate_verstring(caps, hash)
if verstring != check_verstring:
@@ -272,7 +292,7 @@ class XEP_0115(BasePlugin):
binary = hash(S.encode('utf8')).digest()
return base64.b64encode(binary).decode('utf-8')
- def update_caps(self, jid=None, node=None):
+ def update_caps(self, jid=None, node=None, preserve=False):
try:
info = self.xmpp['xep_0030'].get_info(jid, node, local=True)
if isinstance(info, Iq):
@@ -286,19 +306,11 @@ class XEP_0115(BasePlugin):
self.assign_verstring(jid, ver)
if self.xmpp.session_started_event.is_set() and self.broadcast:
- # Check if we've sent directed presence. If we haven't, we
- # can just send a normal presence stanza. If we have, then
- # we will send presence to each contact individually so
- # that we don't clobber existing statuses.
- directed = False
- for contact in self.xmpp.roster[jid]:
- if self.xmpp.roster[jid][contact].last_status is not None:
- directed = True
- if not directed:
- self.xmpp.roster[jid].send_last_presence()
- else:
+ if self.xmpp.is_component or preserve:
for contact in self.xmpp.roster[jid]:
self.xmpp.roster[jid][contact].send_last_presence()
+ else:
+ self.xmpp.roster[jid].send_last_presence()
except XMPPError:
return
diff --git a/sleekxmpp/plugins/xep_0152/__init__.py b/sleekxmpp/plugins/xep_0152/__init__.py
new file mode 100644
index 00000000..7de031b7
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0152/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0152 import stanza
+from sleekxmpp.plugins.xep_0152.stanza import Reachability
+from sleekxmpp.plugins.xep_0152.reachability import XEP_0152
+
+
+register_plugin(XEP_0152)
diff --git a/sleekxmpp/plugins/xep_0152/reachability.py b/sleekxmpp/plugins/xep_0152/reachability.py
new file mode 100644
index 00000000..4cf81739
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0152/reachability.py
@@ -0,0 +1,93 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0152 import stanza, Reachability
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0152(BasePlugin):
+
+ """
+ XEP-0152: Reachability Addresses
+ """
+
+ name = 'xep_0152'
+ description = 'XEP-0152: Reachability Addresses'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Reachability.namespace)
+ self.xmpp['xep_0163'].remove_interest(Reachability.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('reachability', Reachability)
+
+ def publish_reachability(self, addresses, options=None,
+ ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Publish alternative addresses where the user can be reached.
+
+ Arguments:
+ addresses -- A list of dictionaries containing the URI and
+ optional description for each address.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ if not isinstance(addresses, (list, tuple)):
+ addresses = [addresses]
+ reach = Reachability()
+ for address in addresses:
+ if not hasattr(address, 'items'):
+ address = {'uri': address}
+
+ addr = stanza.Address()
+ for key, val in address.items():
+ addr[key] = val
+ reach.append(addr)
+ return self.xmpp['xep_0163'].publish(reach,
+ node=Reachability.namespace,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+ def stop(self, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Clear existing user activity information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ reach = Reachability()
+ return self.xmpp['xep_0163'].publish(reach,
+ node=Reachability.namespace,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
diff --git a/sleekxmpp/plugins/xep_0152/stanza.py b/sleekxmpp/plugins/xep_0152/stanza.py
new file mode 100644
index 00000000..bd173ce1
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0152/stanza.py
@@ -0,0 +1,29 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class Reachability(ElementBase):
+ name = 'reach'
+ namespace = 'urn:xmpp:reach:0'
+ plugin_attrib = 'reach'
+ interfaces = set()
+
+
+class Address(ElementBase):
+ name = 'addr'
+ namespace = 'urn:xmpp:reach:0'
+ plugin_attrib = 'address'
+ plugin_multi_attrib = 'addresses'
+ interfaces = set(['uri', 'desc'])
+ lang_interfaces = set(['desc'])
+ sub_interfaces = set(['desc'])
+
+
+register_stanza_plugin(Reachability, Address, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0153/vcard_avatar.py b/sleekxmpp/plugins/xep_0153/vcard_avatar.py
index c74713e5..ec1ae782 100644
--- a/sleekxmpp/plugins/xep_0153/vcard_avatar.py
+++ b/sleekxmpp/plugins/xep_0153/vcard_avatar.py
@@ -10,12 +10,9 @@ import hashlib
import logging
import threading
-from sleekxmpp import JID
from sleekxmpp.stanza import Presence
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.plugins.base import BasePlugin
from sleekxmpp.plugins.xep_0153 import stanza, VCardTempUpdate
@@ -78,8 +75,17 @@ class XEP_0153(BasePlugin):
self.xmpp.roster[jid].send_last_presence()
def _start(self, event):
- vcard = self.xmpp['xep_0054'].get_vcard()
- self._allow_advertising.set()
+ try:
+ vcard = self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare)
+ data = vcard['vcard_temp']['PHOTO']['BINVAL']
+ if not data:
+ new_hash = ''
+ else:
+ new_hash = hashlib.sha1(data).hexdigest()
+ self.api['set_hash'](self.xmpp.boundjid, args=new_hash)
+ self._allow_advertising.set()
+ except XMPPError:
+ log.debug('Could not retrieve vCard for %s' % self.xmpp.boundjid.bare)
def _end(self, event):
self._allow_advertising.clear()
@@ -118,6 +124,13 @@ class XEP_0153(BasePlugin):
log.debug('Could not retrieve vCard for %s' % jid)
def _recv_presence(self, pres):
+ try:
+ if pres['muc']['affiliation']:
+ # Don't process vCard avatars for MUC occupants
+ # since they all share the same bare JID.
+ return
+ except: pass
+
if not pres.match('presence/vcard_temp_update'):
self.api['set_hash'](pres['from'], args=None)
return
@@ -125,7 +138,7 @@ class XEP_0153(BasePlugin):
data = pres['vcard_temp_update']['photo']
if data is None:
return
- elif data == '' or data != self.api['get_hash'](pres['to']):
+ elif data == '' or data != self.api['get_hash'](pres['from']):
ifrom = pres['to'] if self.xmpp.is_component else None
self.api['reset_hash'](pres['from'], ifrom=ifrom)
self.xmpp.event('vcard_avatar_update', pres)
diff --git a/sleekxmpp/plugins/xep_0163.py b/sleekxmpp/plugins/xep_0163.py
index 5aa3aef9..2d1a63b7 100644
--- a/sleekxmpp/plugins/xep_0163.py
+++ b/sleekxmpp/plugins/xep_0163.py
@@ -107,6 +107,8 @@ class XEP_0163(BasePlugin):
"""
if node is None:
node = stanza.namespace
+ if id is None:
+ id = 'current'
return self.xmpp['xep_0060'].publish(ifrom, node,
id=id,
diff --git a/sleekxmpp/plugins/xep_0184/receipt.py b/sleekxmpp/plugins/xep_0184/receipt.py
index 1fda2066..3e97d8db 100644
--- a/sleekxmpp/plugins/xep_0184/receipt.py
+++ b/sleekxmpp/plugins/xep_0184/receipt.py
@@ -118,6 +118,9 @@ class XEP_0184(BasePlugin):
if stanza['receipt']:
return stanza
+ if not stanza['body']:
+ return stanza
+
if stanza['to'].resource:
if not self.xmpp['xep_0030'].supports(stanza['to'],
feature='urn:xmpp:receipts',
diff --git a/sleekxmpp/plugins/xep_0196/__init__.py b/sleekxmpp/plugins/xep_0196/__init__.py
new file mode 100644
index 00000000..7aeaf6c9
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0196/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0196 import stanza
+from sleekxmpp.plugins.xep_0196.stanza import UserGaming
+from sleekxmpp.plugins.xep_0196.user_gaming import XEP_0196
+
+
+register_plugin(XEP_0196)
diff --git a/sleekxmpp/plugins/xep_0196/stanza.py b/sleekxmpp/plugins/xep_0196/stanza.py
new file mode 100644
index 00000000..571c89d7
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0196/stanza.py
@@ -0,0 +1,20 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ElementBase, ET
+
+
+class UserGaming(ElementBase):
+
+ name = 'gaming'
+ namespace = 'urn:xmpp:gaming:0'
+ plugin_attrib = 'gaming'
+ interfaces = set(['character_name', 'character_profile', 'name',
+ 'level', 'server_address', 'server_name', 'uri'])
+ sub_interfaces = interfaces
+
diff --git a/sleekxmpp/plugins/xep_0196/user_gaming.py b/sleekxmpp/plugins/xep_0196/user_gaming.py
new file mode 100644
index 00000000..e78f1acc
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0196/user_gaming.py
@@ -0,0 +1,97 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0196 import stanza, UserGaming
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0196(BasePlugin):
+
+ """
+ XEP-0196: User Gaming
+ """
+
+ name = 'xep_0196'
+ description = 'XEP-0196: User Gaming'
+ dependencies = set(['xep_0163'])
+ stanza = stanza
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=UserGaming.namespace)
+ self.xmpp['xep_0163'].remove_interest(UserGaming.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0163'].register_pep('user_gaming', UserGaming)
+
+ def publish_gaming(self, name=None, level=None, server_name=None, uri=None,
+ character_name=None, character_profile=None, server_address=None,
+ options=None, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Publish the user's current gaming status.
+
+ Arguments:
+ name -- The name of the game.
+ level -- The user's level in the game.
+ uri -- A URI for the game or relevant gaming service
+ server_name -- The name of the server where the user is playing.
+ server_address -- The hostname or IP address of the server where the
+ user is playing.
+ character_name -- The name of the user's character in the game.
+ character_profile -- A URI for a profile of the user's character.
+ options -- Optional form of publish options.
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ gaming = UserGaming()
+ gaming['name'] = name
+ gaming['level'] = level
+ gaming['uri'] = uri
+ gaming['character_name'] = character_name
+ gaming['character_profile'] = character_profile
+ gaming['server_name'] = server_name
+ gaming['server_address'] = server_address
+ return self.xmpp['xep_0163'].publish(gaming,
+ node=UserGaming.namespace,
+ options=options,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
+
+ def stop(self, ifrom=None, block=True, callback=None, timeout=None):
+ """
+ Clear existing user gaming information to stop notifications.
+
+ Arguments:
+ ifrom -- Specify the sender's JID.
+ block -- Specify if the send call will block until a response
+ is received, or a timeout occurs. Defaults to True.
+ timeout -- The length of time (in seconds) to wait for a response
+ before exiting the send call if blocking is used.
+ Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT
+ callback -- Optional reference to a stream handler function. Will
+ be executed when a reply stanza is received.
+ """
+ gaming = UserGaming()
+ return self.xmpp['xep_0163'].publish(gaming,
+ node=UserGaming.namespace,
+ ifrom=ifrom,
+ block=block,
+ callback=callback,
+ timeout=timeout)
diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py
index 0bdeabf3..836ff4ae 100644
--- a/sleekxmpp/plugins/xep_0199/ping.py
+++ b/sleekxmpp/plugins/xep_0199/ping.py
@@ -9,8 +9,8 @@
import time
import logging
-import sleekxmpp
-from sleekxmpp import Iq
+from sleekxmpp.jid import JID
+from sleekxmpp.stanza import Iq
from sleekxmpp.exceptions import IqError, IqTimeout
from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.matcher import StanzaPath
@@ -38,7 +38,7 @@ class XEP_0199(BasePlugin):
keepalive -- If True, periodically send ping requests
to the server. If a ping is not answered,
the connection will be reset.
- frequency -- Time in seconds between keepalive pings.
+ interval -- Time in seconds between keepalive pings.
Defaults to 300 seconds.
timeout -- Time in seconds to wait for a ping response.
Defaults to 30 seconds.
@@ -53,7 +53,7 @@ class XEP_0199(BasePlugin):
stanza = stanza
default_config = {
'keepalive': False,
- 'frequency': 300,
+ 'interval': 300,
'timeout': 30
}
@@ -61,6 +61,7 @@ class XEP_0199(BasePlugin):
"""
Start the XEP-0199 plugin.
"""
+
register_stanza_plugin(Iq, Ping)
self.xmpp.register_handler(
@@ -70,88 +71,70 @@ class XEP_0199(BasePlugin):
if self.keepalive:
self.xmpp.add_event_handler('session_start',
- self._handle_keepalive,
+ self.enable_keepalive,
threaded=True)
self.xmpp.add_event_handler('session_end',
- self._handle_session_end)
+ self.disable_keepalive)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Ping.namespace)
self.xmpp.remove_handler('Ping')
if self.keepalive:
self.xmpp.del_event_handler('session_start',
- self._handle_keepalive)
+ self.enable_keepalive)
self.xmpp.del_event_handler('session_end',
- self._handle_session_end)
+ self.disable_keepalive)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Ping.namespace)
- def _handle_keepalive(self, event):
- """
- Begin periodic pinging of the server. If a ping is not
- answered, the connection will be restarted.
-
- The pinging interval can be adjused using self.frequency
- before beginning processing.
+ def enable_keepalive(self, interval=None, timeout=None):
+ if interval:
+ self.interval = interval
+ if timeout:
+ self.timeout = timeout
- Arguments:
- event -- The session_start event.
- """
- def scheduled_ping():
- """Send ping request to the server."""
- log.debug("Pinging...")
- try:
- self.send_ping(self.xmpp.boundjid.host, self.timeout)
- except IqError:
- log.debug("Ping response was an error." + \
- "Requesting Reconnect.")
- self.xmpp.reconnect()
- except IqTimeout:
- log.debug("Did not recieve ping back in time." + \
- "Requesting Reconnect.")
- self.xmpp.reconnect()
-
- self.xmpp.schedule('Ping Keep Alive',
- self.frequency,
- scheduled_ping,
+ self.keepalive = True
+ self.xmpp.schedule('Ping keepalive',
+ self.interval,
+ self._keepalive,
repeat=True)
- def _handle_session_end(self, event):
- self.xmpp.scheduler.remove('Ping Keep Alive')
+ def disable_keepalive(self, event=None):
+ self.xmpp.scheduler.remove('Ping keepalive')
- def _handle_ping(self, iq):
- """
- Automatically reply to ping requests.
+ def _keepalive(self, event=None):
+ log.debug("Keepalive ping...")
+ try:
+ rtt = self.ping(self.xmpp.boundjid.host, timeout=self.timeout)
+ except IqTimeout:
+ log.debug("Did not recieve ping back in time." + \
+ "Requesting Reconnect.")
+ self.xmpp.reconnect()
+ else:
+ log.debug('Keepalive RTT: %s' % rtt)
- Arguments:
- iq -- The ping request.
- """
+ def _handle_ping(self, iq):
+ """Automatically reply to ping requests."""
log.debug("Pinged by %s", iq['from'])
iq.reply().send()
- def send_ping(self, jid, timeout=None, errorfalse=False,
- ifrom=None, block=True, callback=None):
- """
- Send a ping request and calculate the response time.
+ def send_ping(self, jid, ifrom=None, block=True, timeout=None, callback=None):
+ """Send a ping request.
Arguments:
jid -- The JID that will receive the ping.
- timeout -- Time in seconds to wait for a response.
- Defaults to self.timeout.
- errorfalse -- Indicates if False should be returned
- if an error stanza is received. Defaults
- to False.
ifrom -- Specifiy the sender JID.
block -- Indicate if execution should block until
a pong response is received. Defaults
to True.
+ timeout -- Time in seconds to wait for a response.
+ Defaults to self.timeout.
callback -- Optional handler to execute when a pong
is received. Useful in conjunction with
the option block=False.
"""
- log.debug("Pinging %s", jid)
- if timeout is None:
+ if not timeout:
timeout = self.timeout
iq = self.xmpp.Iq()
@@ -160,21 +143,44 @@ class XEP_0199(BasePlugin):
iq['from'] = ifrom
iq.enable('ping')
- start_time = time.clock()
+ return iq.send(block=block, timeout=timeout, callback=callback)
- try:
- resp = iq.send(block=block,
- timeout=timeout,
- callback=callback)
- except IqError as err:
- resp = err.iq
+ def ping(self, jid=None, ifrom=None, timeout=None):
+ """Send a ping request and calculate RTT.
- end_time = time.clock()
-
- delay = end_time - start_time
+ Arguments:
+ jid -- The JID that will receive the ping.
+ ifrom -- Specifiy the sender JID.
+ timeout -- Time in seconds to wait for a response.
+ Defaults to self.timeout.
+ """
+ own_host = False
+ if not jid:
+ if self.xmpp.is_component:
+ jid = self.xmpp.server
+ else:
+ jid = self.xmpp.boundjid.host
+ jid = JID(jid)
+ if jid == self.xmpp.boundjid.host or \
+ self.xmpp.is_component and jid == self.xmpp.server:
+ own_host = True
+
+ if not timeout:
+ timeout = self.timeout
- if not block:
- return None
+ start = time.time()
- log.debug("Pong: %s %f", jid, delay)
- return delay
+ log.debug('Pinging %s' % jid)
+ try:
+ self.send_ping(jid, ifrom=ifrom, timeout=timeout)
+ except IqError as e:
+ if own_host:
+ rtt = time.time() - start
+ log.debug('Pinged %s, RTT: %s', jid, rtt)
+ return rtt
+ else:
+ raise e
+ else:
+ rtt = time.time() - start
+ log.debug('Pinged %s, RTT: %s', jid, rtt)
+ return rtt
diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py
index fe20449d..d5b3af37 100644
--- a/sleekxmpp/plugins/xep_0202/time.py
+++ b/sleekxmpp/plugins/xep_0202/time.py
@@ -48,7 +48,7 @@ class XEP_0202(BasePlugin):
self.local_time = default_local_time
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('Entity Time',
StanzaPath('iq/entity_time'),
self._handle_time_request))
diff --git a/sleekxmpp/plugins/xep_0203/stanza.py b/sleekxmpp/plugins/xep_0203/stanza.py
index 9a11cae9..e147e975 100644
--- a/sleekxmpp/plugins/xep_0203/stanza.py
+++ b/sleekxmpp/plugins/xep_0203/stanza.py
@@ -8,6 +8,7 @@
import datetime as dt
+from sleekxmpp.jid import JID
from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.plugins import xep_0082
@@ -20,14 +21,15 @@ class Delay(ElementBase):
interfaces = set(('from', 'stamp', 'text'))
def get_from(self):
- return JID(self._get_attr('from'))
+ from_ = self._get_attr('from')
+ return JID(from_) if from_ else None
def set_from(self, value):
self._set_attr('from', str(value))
def get_stamp(self):
timestamp = self._get_attr('stamp')
- return xep_0082.parse(timestamp)
+ return xep_0082.parse(timestamp) if timestamp else None
def set_stamp(self, value):
if isinstance(value, dt.datetime):
diff --git a/sleekxmpp/plugins/xep_0222.py b/sleekxmpp/plugins/xep_0222.py
index 1073c1a1..2cc7f703 100644
--- a/sleekxmpp/plugins/xep_0222.py
+++ b/sleekxmpp/plugins/xep_0222.py
@@ -76,10 +76,11 @@ class XEP_0222(BasePlugin):
ftype='hidden',
value='http://jabber.org/protocol/pubsub#publish-options')
+ fields = options['fields']
for field, value in self.profile.items():
- if field not in options.fields:
+ if field not in fields:
options.add_field(var=field)
- options.fields[field]['value'] = value
+ options['fields'][field]['value'] = value
return self.xmpp['xep_0163'].publish(stanza, node,
options=options,
diff --git a/sleekxmpp/plugins/xep_0223.py b/sleekxmpp/plugins/xep_0223.py
index ab99f277..abbecfc7 100644
--- a/sleekxmpp/plugins/xep_0223.py
+++ b/sleekxmpp/plugins/xep_0223.py
@@ -76,10 +76,11 @@ class XEP_0223(BasePlugin):
ftype='hidden',
value='http://jabber.org/protocol/pubsub#publish-options')
+ fields = options['fields']
for field, value in self.profile.items():
- if field not in options.fields:
+ if field not in fields:
options.add_field(var=field)
- options.fields[field]['value'] = value
+ options['fields'][field]['value'] = value
return self.xmpp['xep_0163'].publish(stanza, node,
options=options,
diff --git a/sleekxmpp/plugins/xep_0231/bob.py b/sleekxmpp/plugins/xep_0231/bob.py
index d86a5ddf..5e1f590b 100644
--- a/sleekxmpp/plugins/xep_0231/bob.py
+++ b/sleekxmpp/plugins/xep_0231/bob.py
@@ -10,7 +10,7 @@
import logging
import hashlib
-from sleekxmpp.stanza import Iq
+from sleekxmpp.stanza import Iq, Message, Presence
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
@@ -36,6 +36,8 @@ class XEP_0231(BasePlugin):
self._cids = {}
register_stanza_plugin(Iq, BitsOfBinary)
+ register_stanza_plugin(Message, BitsOfBinary)
+ register_stanza_plugin(Presence, BitsOfBinary)
self.xmpp.register_handler(
Callback('Bits of Binary - Iq',
diff --git a/sleekxmpp/plugins/xep_0231/stanza.py b/sleekxmpp/plugins/xep_0231/stanza.py
index a51f5a03..8bf0d6ee 100644
--- a/sleekxmpp/plugins/xep_0231/stanza.py
+++ b/sleekxmpp/plugins/xep_0231/stanza.py
@@ -7,9 +7,10 @@
See the file LICENSE for copying permission.
"""
+import base64
-from base64 import b64encode, b64decode
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import ElementBase
@@ -26,10 +27,10 @@ class BitsOfBinary(ElementBase):
self._set_attr('max-age', value)
def get_data(self):
- return b64decode(self.xml.text)
+ return base64.b64decode(bytes(self.xml.text))
def set_data(self, value):
- self.xml.text = b64encode(value)
+ self.xml.text = bytes(base64.b64encode(value)).decode('utf-8')
def del_data(self):
self.xml.text = ''
diff --git a/sleekxmpp/plugins/xep_0257/stanza.py b/sleekxmpp/plugins/xep_0257/stanza.py
index 17e20136..c3c41db2 100644
--- a/sleekxmpp/plugins/xep_0257/stanza.py
+++ b/sleekxmpp/plugins/xep_0257/stanza.py
@@ -10,7 +10,7 @@ from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
class Certs(ElementBase):
- name = 'query'
+ name = 'items'
namespace = 'urn:xmpp:saslcert:1'
plugin_attrib = 'sasl_certs'
interfaces = set()
diff --git a/sleekxmpp/plugins/xep_0313/mam.py b/sleekxmpp/plugins/xep_0313/mam.py
index 15aee828..4b82ca03 100644
--- a/sleekxmpp/plugins/xep_0313/mam.py
+++ b/sleekxmpp/plugins/xep_0313/mam.py
@@ -36,6 +36,8 @@ class XEP_0313(BasePlugin):
register_stanza_plugin(Iq, stanza.MAM)
register_stanza_plugin(Iq, stanza.Preferences)
register_stanza_plugin(Message, stanza.Result)
+ register_stanza_plugin(Message, stanza.Archived, iterable=True)
+ register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded)
register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set)
def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None,
diff --git a/sleekxmpp/plugins/xep_0313/stanza.py b/sleekxmpp/plugins/xep_0313/stanza.py
index a33c2e35..81576cd4 100644
--- a/sleekxmpp/plugins/xep_0313/stanza.py
+++ b/sleekxmpp/plugins/xep_0313/stanza.py
@@ -25,13 +25,13 @@ class MAM(ElementBase):
self._results = []
def get_start(self):
- timestamp = self._get_attr('start')
+ timestamp = self._get_sub_text('start')
return xep_0082.parse(timestamp)
def set_start(self, value):
if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value)
- self._set_attr('start', value)
+ self._set_sub_text('start', value)
def get_end(self):
timestamp = self._get_sub_text('end')
@@ -122,10 +122,18 @@ class Result(ElementBase):
name = 'result'
namespace = 'urn:xmpp:mam:tmp'
plugin_attrib = 'mam_result'
- interfaces = set(['forwarded', 'queryid', 'id'])
+ interfaces = set(['queryid', 'id'])
- def get_forwarded(self):
- return self.parent()['forwarded']
- def del_forwarded(self):
- del self.parent()['forwarded']
+class Archived(ElementBase):
+ name = 'archived'
+ namespace = 'urn:xmpp:mam:tmp'
+ plugin_attrib = 'mam_archived'
+ plugin_multi_attrib = 'mam_archives'
+ interfaces = set(['by', 'id'])
+
+ def get_by(self):
+ return JID(self._get_attr('by'))
+
+ def set_by(self):
+ return self._set_attr('by', str(value))
diff --git a/sleekxmpp/plugins/xep_0319/__init__.py b/sleekxmpp/plugins/xep_0319/__init__.py
new file mode 100644
index 00000000..4756e63e
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0319/__init__.py
@@ -0,0 +1,16 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0319 import stanza
+from sleekxmpp.plugins.xep_0319.stanza import Idle
+from sleekxmpp.plugins.xep_0319.idle import XEP_0319
+
+
+register_plugin(XEP_0319)
diff --git a/sleekxmpp/plugins/xep_0319/idle.py b/sleekxmpp/plugins/xep_0319/idle.py
new file mode 100644
index 00000000..90456f9f
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0319/idle.py
@@ -0,0 +1,75 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from datetime import datetime, timedelta
+
+from sleekxmpp.stanza import Presence
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.xep_0319 import stanza
+
+
+class XEP_0319(BasePlugin):
+ name = 'xep_0319'
+ description = 'XEP-0319: Last User Interaction in Presence'
+ dependencies = set(['xep_0012'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self._idle_stamps = {}
+ register_stanza_plugin(Presence, stanza.Idle)
+ self.api.register(self._set_idle,
+ 'set_idle',
+ default=True)
+ self.api.register(self._get_idle,
+ 'get_idle',
+ default=True)
+ self.xmpp.register_handler(
+ Callback('Idle Presence',
+ StanzaPath('presence/idle'),
+ self._idle_presence))
+ self.xmpp.add_filter('out', self._stamp_idle_presence)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:idle:1')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:idle:1')
+ self.xmpp.del_filter('out', self._stamp_idle_presence)
+ self.xmpp.remove_handler('Idle Presence')
+
+ def idle(self, jid=None, since=None):
+ seconds = None
+ if since is None:
+ since = datetime.now()
+ else:
+ seconds = datetime.now() - since
+ self.api['set_idle'](jid, None, None, since)
+ self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds)
+
+ def active(self, jid=None):
+ self.api['set_idle'](jid, None, None, None)
+ self.xmpp['xep_0012'].del_last_activity(jid)
+
+ def _set_idle(self, jid, node, ifrom, data):
+ self._idle_stamps[jid] = data
+
+ def _get_idle(self, jid, node, ifrom, data):
+ return self._idle_stamps.get(jid, None)
+
+ def _idle_presence(self, pres):
+ self.xmpp.event('presence_idle', pres)
+
+ def _stamp_idle_presence(self, stanza):
+ if isinstance(stanza, Presence):
+ since = self.api['get_idle'](stanza['from'] or self.xmpp.boundjid)
+ if since:
+ stanza['idle']['since'] = since
+ return stanza
diff --git a/sleekxmpp/plugins/xep_0319/stanza.py b/sleekxmpp/plugins/xep_0319/stanza.py
new file mode 100644
index 00000000..abfb4f41
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0319/stanza.py
@@ -0,0 +1,28 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime as dt
+
+from sleekxmpp.xmlstream import ElementBase
+from sleekxmpp.plugins import xep_0082
+
+
+class Idle(ElementBase):
+ name = 'idle'
+ namespace = 'urn:xmpp:idle:1'
+ plugin_attrib = 'idle'
+ interfaces = set(['since'])
+
+ def get_since(self):
+ timestamp = self._get_attr('since')
+ return xep_0082.parse(timestamp)
+
+ def set_since(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_attr('since', value)
diff --git a/sleekxmpp/plugins/xep_0323/__init__.py b/sleekxmpp/plugins/xep_0323/__init__.py
new file mode 100644
index 00000000..10779ada
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0323/__init__.py
@@ -0,0 +1,18 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0323.sensordata import XEP_0323
+from sleekxmpp.plugins.xep_0323 import stanza
+
+register_plugin(XEP_0323)
+
+xep_0323=XEP_0323
diff --git a/sleekxmpp/plugins/xep_0323/device.py b/sleekxmpp/plugins/xep_0323/device.py
new file mode 100644
index 00000000..80e6fd95
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0323/device.py
@@ -0,0 +1,258 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime
+import logging
+
+class Device(object):
+ """
+ Example implementation of a device readout object.
+ Is registered in the XEP_0323.register_node call
+ The device object may be any custom implementation to support
+ specific devices, but it must implement the functions:
+ has_field
+ request_fields
+ """
+
+ def __init__(self, nodeId, fields=None):
+ if not fields:
+ fields = {}
+
+ self.nodeId = nodeId
+ self.fields = fields # see fields described below
+ # {'type':'numeric',
+ # 'name':'myname',
+ # 'value': 42,
+ # 'unit':'Z'}];
+ self.timestamp_data = {}
+ self.momentary_data = {}
+ self.momentary_timestamp = ""
+ logging.debug("Device object started nodeId %s",nodeId)
+
+ def has_field(self, field):
+ """
+ Returns true if the supplied field name exists in this device.
+
+ Arguments:
+ field -- The field name
+ """
+ if field in self.fields.keys():
+ return True
+ return False
+
+ def refresh(self, fields):
+ """
+ override method to do the refresh work
+ refresh values from hardware or other
+ """
+ pass
+
+
+ def request_fields(self, fields, flags, session, callback):
+ """
+ Starts a data readout. Verifies the requested fields,
+ refreshes the data (if needed) and calls the callback
+ with requested data.
+
+
+ Arguments:
+ fields -- List of field names to readout
+ flags -- [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ session -- Session id, only used in the callback as identifier
+ callback -- Callback function to call when data is available.
+
+ The callback function must support the following arguments:
+
+ session -- Session id, as supplied in the request_fields call
+ nodeId -- Identifier for this device
+ result -- The current result status of the readout. Valid values are:
+ "error" - Readout failed.
+ "fields" - Contains readout data.
+ "done" - Indicates that the readout is complete. May contain
+ readout data.
+ timestamp_block -- [optional] Only applies when result != "error"
+ The readout data. Structured as a dictionary:
+ {
+ timestamp: timestamp for this datablock,
+ fields: list of field dictionary (one per readout field).
+ readout field dictionary format:
+ {
+ type: The field type (numeric, boolean, dateTime, timeSpan, string, enum)
+ name: The field name
+ value: The field value
+ unit: The unit of the field. Only applies to type numeric.
+ dataType: The datatype of the field. Only applies to type enum.
+ flags: [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ }
+ }
+ error_msg -- [optional] Only applies when result == "error".
+ Error details when a request failed.
+
+ """
+ logging.debug("request_fields called looking for fields %s",fields)
+ if len(fields) > 0:
+ # Check availiability
+ for f in fields:
+ if f not in self.fields.keys():
+ self._send_reject(session, callback)
+ return False
+ else:
+ # Request all fields
+ fields = self.fields.keys()
+
+
+ # Refresh data from device
+ # ...
+ logging.debug("about to refresh device fields %s",fields)
+ self.refresh(fields)
+
+ if "momentary" in flags and flags['momentary'] == "true" or \
+ "all" in flags and flags['all'] == "true":
+ ts_block = {}
+ timestamp = ""
+
+ if len(self.momentary_timestamp) > 0:
+ timestamp = self.momentary_timestamp
+ else:
+ timestamp = self._get_timestamp()
+
+ field_block = []
+ for f in self.momentary_data:
+ if f in fields:
+ field_block.append({"name": f,
+ "type": self.fields[f]["type"],
+ "unit": self.fields[f]["unit"],
+ "dataType": self.fields[f]["dataType"],
+ "value": self.momentary_data[f]["value"],
+ "flags": self.momentary_data[f]["flags"]})
+ ts_block["timestamp"] = timestamp
+ ts_block["fields"] = field_block
+
+ callback(session, result="done", nodeId=self.nodeId, timestamp_block=ts_block)
+ return
+
+ from_flag = self._datetime_flag_parser(flags, 'from')
+ to_flag = self._datetime_flag_parser(flags, 'to')
+
+ for ts in sorted(self.timestamp_data.keys()):
+ tsdt = datetime.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S")
+ if not from_flag is None:
+ if tsdt < from_flag:
+ #print (str(tsdt) + " < " + str(from_flag))
+ continue
+ if not to_flag is None:
+ if tsdt > to_flag:
+ #print (str(tsdt) + " > " + str(to_flag))
+ continue
+
+ ts_block = {}
+ field_block = []
+
+ for f in self.timestamp_data[ts]:
+ if f in fields:
+ field_block.append({"name": f,
+ "type": self.fields[f]["type"],
+ "unit": self.fields[f]["unit"],
+ "dataType": self.fields[f]["dataType"],
+ "value": self.timestamp_data[ts][f]["value"],
+ "flags": self.timestamp_data[ts][f]["flags"]})
+
+ ts_block["timestamp"] = ts
+ ts_block["fields"] = field_block
+ callback(session, result="fields", nodeId=self.nodeId, timestamp_block=ts_block)
+ callback(session, result="done", nodeId=self.nodeId, timestamp_block=None)
+
+ def _datetime_flag_parser(self, flags, flagname):
+ if not flagname in flags:
+ return None
+
+ dt = None
+ try:
+ dt = datetime.datetime.strptime(flags[flagname], "%Y-%m-%dT%H:%M:%S")
+ except ValueError:
+ # Badly formatted datetime, ignore it
+ pass
+ return dt
+
+
+ def _get_timestamp(self):
+ """
+ Generates a properly formatted timestamp of current time
+ """
+ return datetime.datetime.now().replace(microsecond=0).isoformat()
+
+ def _send_reject(self, session, callback):
+ """
+ Sends a reject to the caller
+
+ Arguments:
+ session -- Session id, see definition in request_fields function
+ callback -- Callback function, see definition in request_fields function
+ """
+ callback(session, result="error", nodeId=self.nodeId, timestamp_block=None, error_msg="Reject")
+
+ def _add_field(self, name, typename, unit=None, dataType=None):
+ """
+ Adds a field to the device
+
+ Arguments:
+ name -- Name of the field
+ typename -- Type of the field (numeric, boolean, dateTime, timeSpan, string, enum)
+ unit -- [optional] only applies to "numeric". Unit for the field.
+ dataType -- [optional] only applies to "enum". Datatype for the field.
+ """
+ self.fields[name] = {"type": typename, "unit": unit, "dataType": dataType}
+
+ def _add_field_timestamp_data(self, name, timestamp, value, flags=None):
+ """
+ Adds timestamped data to a field
+
+ Arguments:
+ name -- Name of the field
+ timestamp -- Timestamp for the data (string)
+ value -- Field value at the timestamp
+ flags -- [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ """
+ if not name in self.fields.keys():
+ return False
+ if not timestamp in self.timestamp_data:
+ self.timestamp_data[timestamp] = {}
+
+ self.timestamp_data[timestamp][name] = {"value": value, "flags": flags}
+ return True
+
+ def _add_field_momentary_data(self, name, value, flags=None):
+ """
+ Sets momentary data to a field
+
+ Arguments:
+ name -- Name of the field
+ value -- Field value at the timestamp
+ flags -- [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ """
+ if name not in self.fields:
+ return False
+ if flags is None:
+ flags = {}
+
+ flags["momentary"] = "true"
+ self.momentary_data[name] = {"value": value, "flags": flags}
+ return True
+
+ def _set_momentary_timestamp(self, timestamp):
+ """
+ This function is only for unit testing to produce predictable results.
+ """
+ self.momentary_timestamp = timestamp
+
diff --git a/sleekxmpp/plugins/xep_0323/sensordata.py b/sleekxmpp/plugins/xep_0323/sensordata.py
new file mode 100644
index 00000000..30c28504
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0323/sensordata.py
@@ -0,0 +1,723 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import time
+import datetime
+from threading import Thread, Lock, Timer
+
+from sleekxmpp.plugins.xep_0323.timerreset import TimerReset
+
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0323 import stanza
+from sleekxmpp.plugins.xep_0323.stanza import Sensordata
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0323(BasePlugin):
+
+ """
+ XEP-0323: IoT Sensor Data
+
+
+ This XEP provides the underlying architecture, basic operations and data
+ structures for sensor data communication over XMPP networks. It includes
+ a hardware abstraction model, removing any technical detail implemented
+ in underlying technologies.
+
+ Also see <http://xmpp.org/extensions/xep-0323.html>
+
+ Configuration Values:
+ threaded -- Indicates if communication with sensors should be threaded.
+ Defaults to True.
+
+ Events:
+ Sensor side
+ -----------
+ Sensordata Event:Req -- Received a request for data
+ Sensordata Event:Cancel -- Received a cancellation for a request
+
+ Client side
+ -----------
+ Sensordata Event:Accepted -- Received a accept from sensor for a request
+ Sensordata Event:Rejected -- Received a reject from sensor for a request
+ Sensordata Event:Cancelled -- Received a cancel confirm from sensor
+ Sensordata Event:Fields -- Received fields from sensor for a request
+ This may be triggered multiple times since
+ the sensor can split up its response in
+ multiple messages.
+ Sensordata Event:Failure -- Received a failure indication from sensor
+ for a request. Typically a comm timeout.
+
+ Attributes:
+ threaded -- Indicates if command events should be threaded.
+ Defaults to True.
+ sessions -- A dictionary or equivalent backend mapping
+ session IDs to dictionaries containing data
+ relevant to a request's session. This dictionary is used
+ both by the client and sensor side. On client side, seqnr
+ is used as key, while on sensor side, a session_id is used
+ as key. This ensures that the two will not collide, so
+ one instance can be both client and sensor.
+ Sensor side
+ -----------
+ nodes -- A dictionary mapping sensor nodes that are serviced through
+ this XMPP instance to their device handlers ("drivers").
+ Client side
+ -----------
+ last_seqnr -- The last used sequence number (integer). One sequence of
+ communication (e.g. -->request, <--accept, <--fields)
+ between client and sensor is identified by a unique
+ sequence number (unique between the client/sensor pair)
+
+ Methods:
+ plugin_init -- Overrides base_plugin.plugin_init
+ post_init -- Overrides base_plugin.post_init
+ plugin_end -- Overrides base_plugin.plugin_end
+
+ Sensor side
+ -----------
+ register_node -- Register a sensor as available from this XMPP
+ instance.
+
+ Client side
+ -----------
+ request_data -- Initiates a request for data from one or more
+ sensors. Non-blocking, a callback function will
+ be called when data is available.
+
+ """
+
+ name = 'xep_0323'
+ description = 'XEP-0323 Internet of Things - Sensor Data'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+
+ default_config = {
+ 'threaded': True
+# 'session_db': None
+ }
+
+ def plugin_init(self):
+ """ Start the XEP-0323 plugin """
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Req',
+ StanzaPath('iq@type=get/req'),
+ self._handle_event_req))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Accepted',
+ StanzaPath('iq@type=result/accepted'),
+ self._handle_event_accepted))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Rejected',
+ StanzaPath('iq@type=error/rejected'),
+ self._handle_event_rejected))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Cancel',
+ StanzaPath('iq@type=get/cancel'),
+ self._handle_event_cancel))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Cancelled',
+ StanzaPath('iq@type=result/cancelled'),
+ self._handle_event_cancelled))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Fields',
+ StanzaPath('message/fields'),
+ self._handle_event_fields))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Failure',
+ StanzaPath('message/failure'),
+ self._handle_event_failure))
+
+ self.xmpp.register_handler(
+ Callback('Sensordata Event:Started',
+ StanzaPath('message/started'),
+ self._handle_event_started))
+
+ # Server side dicts
+ self.nodes = {}
+ self.sessions = {}
+
+ self.last_seqnr = 0
+ self.seqnr_lock = Lock()
+
+ ## For testning only
+ self.test_authenticated_from = ""
+
+ def post_init(self):
+ """ Init complete. Register our features in Serivce discovery. """
+ BasePlugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(Sensordata.namespace)
+ self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple())
+
+ def _new_session(self):
+ """ Return a new session ID. """
+ return str(time.time()) + '-' + self.xmpp.new_id()
+
+ def session_bind(self, jid):
+ logging.debug("setting the Disco discovery for %s" % Sensordata.namespace)
+ self.xmpp['xep_0030'].add_feature(Sensordata.namespace)
+ self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple())
+
+
+ def plugin_end(self):
+ """ Stop the XEP-0323 plugin """
+ self.sessions.clear()
+ self.xmpp.remove_handler('Sensordata Event:Req')
+ self.xmpp.remove_handler('Sensordata Event:Accepted')
+ self.xmpp.remove_handler('Sensordata Event:Rejected')
+ self.xmpp.remove_handler('Sensordata Event:Cancel')
+ self.xmpp.remove_handler('Sensordata Event:Cancelled')
+ self.xmpp.remove_handler('Sensordata Event:Fields')
+ self.xmpp['xep_0030'].del_feature(feature=Sensordata.namespace)
+
+
+ # =================================================================
+ # Sensor side (data provider) API
+
+ def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None):
+ """
+ Register a sensor/device as available for serving of data through this XMPP
+ instance.
+
+ The device object may by any custom implementation to support
+ specific devices, but it must implement the functions:
+ has_field
+ request_fields
+ according to the interfaces shown in the example device.py file.
+
+ Arguments:
+ nodeId -- The identifier for the device
+ device -- The device object
+ commTimeout -- Time in seconds to wait between each callback from device during
+ a data readout. Float.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ self.nodes[nodeId] = {"device": device,
+ "commTimeout": commTimeout,
+ "sourceId": sourceId,
+ "cacheType": cacheType}
+
+ def _set_authenticated(self, auth=''):
+ """ Internal testing function """
+ self.test_authenticated_from = auth
+
+
+ def _handle_event_req(self, iq):
+ """
+ Event handler for reception of an Iq with req - this is a request.
+
+ Verifies that
+ - all the requested nodes are available
+ - at least one of the requested fields is available from at least
+ one of the nodes
+
+ If the request passes verification, an accept response is sent, and
+ the readout process is started in a separate thread.
+ If the verification fails, a reject message is sent.
+ """
+
+ seqnr = iq['req']['seqnr']
+ error_msg = ''
+ req_ok = True
+
+ # Authentication
+ if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from:
+ # Invalid authentication
+ req_ok = False
+ error_msg = "Access denied"
+
+ # Nodes
+ process_nodes = []
+ if len(iq['req']['nodes']) > 0:
+ for n in iq['req']['nodes']:
+ if not n['nodeId'] in self.nodes:
+ req_ok = False
+ error_msg = "Invalid nodeId " + n['nodeId']
+ process_nodes = [n['nodeId'] for n in iq['req']['nodes']]
+ else:
+ process_nodes = self.nodes.keys()
+
+ # Fields - if we just find one we are happy, otherwise we reject
+ process_fields = []
+ if len(iq['req']['fields']) > 0:
+ found = False
+ for f in iq['req']['fields']:
+ for node in self.nodes:
+ if self.nodes[node]["device"].has_field(f['name']):
+ found = True
+ break
+ if not found:
+ req_ok = False
+ error_msg = "Invalid field " + f['name']
+ process_fields = [f['name'] for n in iq['req']['fields']]
+
+ req_flags = iq['req']._get_flags()
+
+ request_delay_sec = None
+ if 'when' in req_flags:
+ # Timed request - requires datetime string in iso format
+ # ex. 2013-04-05T15:00:03
+ dt = None
+ try:
+ dt = datetime.datetime.strptime(req_flags['when'], "%Y-%m-%dT%H:%M:%S")
+ except ValueError:
+ req_ok = False
+ error_msg = "Invalid datetime in 'when' flag, please use ISO format (i.e. 2013-04-05T15:00:03)."
+
+ if not dt is None:
+ # Datetime properly formatted
+ dtnow = datetime.datetime.now()
+ dtdiff = dt - dtnow
+ request_delay_sec = dtdiff.seconds + dtdiff.days * 24 * 3600
+ if request_delay_sec <= 0:
+ req_ok = False
+ error_msg = "Invalid datetime in 'when' flag, cannot set a time in the past. Current time: " + dtnow.isoformat()
+
+ if req_ok:
+ session = self._new_session()
+ self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr}
+ self.sessions[session]["commTimers"] = {}
+ self.sessions[session]["nodeDone"] = {}
+
+ #print("added session: " + str(self.sessions))
+
+ iq.reply()
+ iq['accepted']['seqnr'] = seqnr
+ if not request_delay_sec is None:
+ iq['accepted']['queued'] = "true"
+ iq.send(block=False)
+
+ self.sessions[session]["node_list"] = process_nodes
+
+ if not request_delay_sec is None:
+ # Delay request to requested time
+ timer = Timer(request_delay_sec, self._event_delayed_req, args=(session, process_fields, req_flags))
+ self.sessions[session]["commTimers"]["delaytimer"] = timer
+ timer.start()
+ return
+
+ if self.threaded:
+ #print("starting thread")
+ tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags))
+ tr_req.start()
+ #print("started thread")
+ else:
+ self._threaded_node_request(session, process_fields, req_flags)
+
+ else:
+ iq.reply()
+ iq['type'] = 'error'
+ iq['rejected']['seqnr'] = seqnr
+ iq['rejected']['error'] = error_msg
+ iq.send(block=False)
+
+ def _threaded_node_request(self, session, process_fields, flags):
+ """
+ Helper function to handle the device readouts in a separate thread.
+
+ Arguments:
+ session -- The request session id
+ process_fields -- The fields to request from the devices
+ flags -- [optional] flags to pass to the devices, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ """
+ for node in self.sessions[session]["node_list"]:
+ self.sessions[session]["nodeDone"][node] = False
+
+ for node in self.sessions[session]["node_list"]:
+ timer = TimerReset(self.nodes[node]['commTimeout'], self._event_comm_timeout, args=(session, node))
+ self.sessions[session]["commTimers"][node] = timer
+ #print("Starting timer " + str(timer) + ", timeout: " + str(self.nodes[node]['commTimeout']))
+ timer.start()
+ self.nodes[node]['device'].request_fields(process_fields, flags=flags, session=session, callback=self._device_field_request_callback)
+
+ def _event_comm_timeout(self, session, nodeId):
+ """
+ Triggered if any of the readout operations timeout.
+ Sends a failure message back to the client, stops communicating
+ with the failing device.
+
+ Arguments:
+ session -- The request session id
+ nodeId -- The id of the device which timed out
+ """
+ msg = self.xmpp.Message()
+ msg['from'] = self.sessions[session]['to']
+ msg['to'] = self.sessions[session]['from']
+ msg['failure']['seqnr'] = self.sessions[session]['seqnr']
+ msg['failure']['error']['text'] = "Timeout"
+ msg['failure']['error']['nodeId'] = nodeId
+ msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat()
+
+ # Drop communication with this device and check if we are done
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ msg['failure']['done'] = 'true'
+ msg.send()
+ # The session is complete, delete it
+ #print("del session " + session + " due to timeout")
+ del self.sessions[session]
+
+ def _event_delayed_req(self, session, process_fields, req_flags):
+ """
+ Triggered when the timer from a delayed request fires.
+
+ Arguments:
+ session -- The request session id
+ process_fields -- The fields to request from the devices
+ flags -- [optional] flags to pass to the devices, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ """
+ msg = self.xmpp.Message()
+ msg['from'] = self.sessions[session]['to']
+ msg['to'] = self.sessions[session]['from']
+ msg['started']['seqnr'] = self.sessions[session]['seqnr']
+ msg.send()
+
+ if self.threaded:
+ tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags))
+ tr_req.start()
+ else:
+ self._threaded_node_request(session, process_fields, req_flags)
+
+ def _all_nodes_done(self, session):
+ """
+ Checks wheter all devices are done replying to the readout.
+
+ Arguments:
+ session -- The request session id
+ """
+ for n in self.sessions[session]["nodeDone"]:
+ if not self.sessions[session]["nodeDone"][n]:
+ return False
+ return True
+
+ def _device_field_request_callback(self, session, nodeId, result, timestamp_block, error_msg=None):
+ """
+ Callback function called by the devices when they have any additional data.
+ Composes a message with the data and sends it back to the client, and resets
+ the timeout timer for the device.
+
+ Arguments:
+ session -- The request session id
+ nodeId -- The device id which initiated the callback
+ result -- The current result status of the readout. Valid values are:
+ "error" - Readout failed.
+ "fields" - Contains readout data.
+ "done" - Indicates that the readout is complete. May contain
+ readout data.
+ timestamp_block -- [optional] Only applies when result != "error"
+ The readout data. Structured as a dictionary:
+ {
+ timestamp: timestamp for this datablock,
+ fields: list of field dictionary (one per readout field).
+ readout field dictionary format:
+ {
+ type: The field type (numeric, boolean, dateTime, timeSpan, string, enum)
+ name: The field name
+ value: The field value
+ unit: The unit of the field. Only applies to type numeric.
+ dataType: The datatype of the field. Only applies to type enum.
+ flags: [optional] data classifier flags for the field, e.g. momentary
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ }
+ }
+ error_msg -- [optional] Only applies when result == "error".
+ Error details when a request failed.
+ """
+ if not session in self.sessions:
+ # This can happend if a session was deleted, like in a cancellation. Just drop the data.
+ return
+
+ if result == "error":
+ self.sessions[session]["commTimers"][nodeId].cancel()
+
+ msg = self.xmpp.Message()
+ msg['from'] = self.sessions[session]['to']
+ msg['to'] = self.sessions[session]['from']
+ msg['failure']['seqnr'] = self.sessions[session]['seqnr']
+ msg['failure']['error']['text'] = error_msg
+ msg['failure']['error']['nodeId'] = nodeId
+ msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat()
+
+ # Drop communication with this device and check if we are done
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ msg['failure']['done'] = 'true'
+ # The session is complete, delete it
+ # print("del session " + session + " due to error")
+ del self.sessions[session]
+ msg.send()
+ else:
+ msg = self.xmpp.Message()
+ msg['from'] = self.sessions[session]['to']
+ msg['to'] = self.sessions[session]['from']
+ msg['fields']['seqnr'] = self.sessions[session]['seqnr']
+
+ if timestamp_block is not None and len(timestamp_block) > 0:
+ node = msg['fields'].add_node(nodeId)
+ ts = node.add_timestamp(timestamp_block["timestamp"])
+
+ for f in timestamp_block["fields"]:
+ data = ts.add_data( typename=f['type'],
+ name=f['name'],
+ value=f['value'],
+ unit=f['unit'],
+ dataType=f['dataType'],
+ flags=f['flags'])
+
+ if result == "done":
+ self.sessions[session]["commTimers"][nodeId].cancel()
+ self.sessions[session]["nodeDone"][nodeId] = True
+ msg['fields']['done'] = 'true'
+ if (self._all_nodes_done(session)):
+ # The session is complete, delete it
+ # print("del session " + session + " due to complete")
+ del self.sessions[session]
+ else:
+ # Restart comm timer
+ self.sessions[session]["commTimers"][nodeId].reset()
+
+ msg.send()
+
+ def _handle_event_cancel(self, iq):
+ """ Received Iq with cancel - this is a cancel request.
+ Delete the session and confirm. """
+
+ seqnr = iq['cancel']['seqnr']
+ # Find the session
+ for s in self.sessions:
+ if self.sessions[s]['from'] == iq['from'] and self.sessions[s]['to'] == iq['to'] and self.sessions[s]['seqnr'] == seqnr:
+ # found it. Cancel all timers
+ for n in self.sessions[s]["commTimers"]:
+ self.sessions[s]["commTimers"][n].cancel()
+
+ # Confirm
+ iq.reply()
+ iq['type'] = 'result'
+ iq['cancelled']['seqnr'] = seqnr
+ iq.send(block=False)
+
+ # Delete session
+ del self.sessions[s]
+ return
+
+ # Could not find session, send reject
+ iq.reply()
+ iq['type'] = 'error'
+ iq['rejected']['seqnr'] = seqnr
+ iq['rejected']['error'] = "Cancel request received, no matching request is active."
+ iq.send(block=False)
+
+ # =================================================================
+ # Client side (data retriever) API
+
+ def request_data(self, from_jid, to_jid, callback, nodeIds=None, fields=None, flags=None):
+ """
+ Called on the client side to initiade a data readout.
+ Composes a message with the request and sends it to the device(s).
+ Does not block, the callback will be called when data is available.
+
+ Arguments:
+ from_jid -- The jid of the requester
+ to_jid -- The jid of the device(s)
+ callback -- The callback function to call when data is availble.
+
+ The callback function must support the following arguments:
+
+ from_jid -- The jid of the responding device(s)
+ result -- The current result status of the readout. Valid values are:
+ "accepted" - Readout request accepted
+ "queued" - Readout request accepted and queued
+ "rejected" - Readout request rejected
+ "failure" - Readout failed.
+ "cancelled" - Confirmation of request cancellation.
+ "started" - Previously queued request is now started
+ "fields" - Contains readout data.
+ "done" - Indicates that the readout is complete.
+
+ nodeId -- [optional] Mandatory when result == "fields" or "failure".
+ The node Id of the responding device. One callback will only
+ contain data from one device.
+ timestamp -- [optional] Mandatory when result == "fields".
+ The timestamp of data in this callback. One callback will only
+ contain data from one timestamp.
+ fields -- [optional] Mandatory when result == "fields".
+ List of field dictionaries representing the readout data.
+ Dictionary format:
+ {
+ typename: The field type (numeric, boolean, dateTime, timeSpan, string, enum)
+ name: The field name
+ value: The field value
+ unit: The unit of the field. Only applies to type numeric.
+ dataType: The datatype of the field. Only applies to type enum.
+ flags: [optional] data classifier flags for the field, e.g. momentary.
+ Formatted as a dictionary like { "flag name": "flag value" ... }
+ }
+
+ error_msg -- [optional] Mandatory when result == "rejected" or "failure".
+ Details about why the request is rejected or failed.
+ "rejected" means that the request is stopped, but note that the
+ request will continue even after a "failure". "failure" only means
+ that communication was stopped to that specific device, other
+ device(s) (if any) will continue their readout.
+
+ nodeIds -- [optional] Limits the request to the node Ids in this list.
+ fields -- [optional] Limits the request to the field names in this list.
+ flags -- [optional] Limits the request according to the flags, or sets
+ readout conditions such as timing.
+
+ Return value:
+ session -- Session identifier. Client can use this as a reference to cancel
+ the request.
+ """
+ iq = self.xmpp.Iq()
+ iq['from'] = from_jid
+ iq['to'] = to_jid
+ iq['type'] = "get"
+ seqnr = self._get_new_seqnr()
+ iq['id'] = seqnr
+ iq['req']['seqnr'] = seqnr
+ if nodeIds is not None:
+ for nodeId in nodeIds:
+ iq['req'].add_node(nodeId)
+ if fields is not None:
+ for field in fields:
+ iq['req'].add_field(field)
+
+ iq['req']._set_flags(flags)
+
+ self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr, "callback": callback}
+ iq.send(block=False)
+
+ return seqnr
+
+ def cancel_request(self, session):
+ """
+ Called on the client side to cancel a request for data readout.
+ Composes a message with the cancellation and sends it to the device(s).
+ Does not block, the callback will be called when cancellation is
+ confirmed.
+
+ Arguments:
+ session -- The session id of the request to cancel
+ """
+ seqnr = session
+ iq = self.xmpp.Iq()
+ iq['from'] = self.sessions[seqnr]['from']
+ iq['to'] = self.sessions[seqnr]['to']
+ iq['type'] = "get"
+ iq['id'] = seqnr
+ iq['cancel']['seqnr'] = seqnr
+ iq.send(block=False)
+
+ def _get_new_seqnr(self):
+ """ Returns a unique sequence number (unique across threads) """
+ self.seqnr_lock.acquire()
+ self.last_seqnr += 1
+ self.seqnr_lock.release()
+ return str(self.last_seqnr)
+
+ def _handle_event_accepted(self, iq):
+ """ Received Iq with accepted - request was accepted """
+ seqnr = iq['accepted']['seqnr']
+ result = "accepted"
+ if iq['accepted']['queued'] == 'true':
+ result = "queued"
+
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=iq['from'], result=result)
+
+ def _handle_event_rejected(self, iq):
+ """ Received Iq with rejected - this is a reject.
+ Delete the session. """
+ seqnr = iq['rejected']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=iq['from'], result="rejected", error_msg=iq['rejected']['error'])
+ # Session terminated
+ del self.sessions[seqnr]
+
+ def _handle_event_cancelled(self, iq):
+ """
+ Received Iq with cancelled - this is a cancel confirm.
+ Delete the session.
+ """
+ #print("Got cancelled")
+ seqnr = iq['cancelled']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=iq['from'], result="cancelled")
+ # Session cancelled
+ del self.sessions[seqnr]
+
+ def _handle_event_fields(self, msg):
+ """
+ Received Msg with fields - this is a data reponse to a request.
+ If this is the last data block, issue a "done" callback.
+ """
+ seqnr = msg['fields']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ for node in msg['fields']['nodes']:
+ for ts in node['timestamps']:
+ fields = []
+ for d in ts['datas']:
+ field_block = {}
+ field_block["name"] = d['name']
+ field_block["typename"] = d._get_typename()
+ field_block["value"] = d['value']
+ if not d['unit'] == "": field_block["unit"] = d['unit'];
+ if not d['dataType'] == "": field_block["dataType"] = d['dataType'];
+ flags = d._get_flags()
+ if not len(flags) == 0:
+ field_block["flags"] = flags
+ fields.append(field_block)
+
+ callback(from_jid=msg['from'], result="fields", nodeId=node['nodeId'], timestamp=ts['value'], fields=fields)
+
+ if msg['fields']['done'] == "true":
+ callback(from_jid=msg['from'], result="done")
+ # Session done
+ del self.sessions[seqnr]
+
+ def _handle_event_failure(self, msg):
+ """
+ Received Msg with failure - our request failed
+ Delete the session.
+ """
+ seqnr = msg['failure']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=msg['from'], result="failure", nodeId=msg['failure']['error']['nodeId'], timestamp=msg['failure']['error']['timestamp'], error_msg=msg['failure']['error']['text'])
+
+ # Session failed
+ del self.sessions[seqnr]
+
+ def _handle_event_started(self, msg):
+ """
+ Received Msg with started - our request was queued and is now started.
+ """
+ seqnr = msg['started']['seqnr']
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=msg['from'], result="started")
+
+
diff --git a/sleekxmpp/plugins/xep_0323/stanza/__init__.py b/sleekxmpp/plugins/xep_0323/stanza/__init__.py
new file mode 100644
index 00000000..c039cefa
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0323/stanza/__init__.py
@@ -0,0 +1,12 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0323.stanza.sensordata import *
+
diff --git a/sleekxmpp/plugins/xep_0323/stanza/base.py b/sleekxmpp/plugins/xep_0323/stanza/base.py
new file mode 100644
index 00000000..1dadcf46
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0323/stanza/base.py
@@ -0,0 +1,13 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ET
+
+pass
diff --git a/sleekxmpp/plugins/xep_0323/stanza/sensordata.py b/sleekxmpp/plugins/xep_0323/stanza/sensordata.py
new file mode 100644
index 00000000..e8718161
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0323/stanza/sensordata.py
@@ -0,0 +1,792 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp import Iq, Message
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from re import match
+
+class Sensordata(ElementBase):
+ """ Placeholder for the namespace, not used as a stanza """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'sensordata'
+ plugin_attrib = name
+ interfaces = set(tuple())
+
+class FieldTypes():
+ """
+ All field types are optional booleans that default to False
+ """
+ field_types = set([ 'momentary','peak','status','computed','identity','historicalSecond','historicalMinute','historicalHour', \
+ 'historicalDay','historicalWeek','historicalMonth','historicalQuarter','historicalYear','historicalOther'])
+
+class FieldStatus():
+ """
+ All field statuses are optional booleans that default to False
+ """
+ field_status = set([ 'missing','automaticEstimate','manualEstimate','manualReadout','automaticReadout','timeOffset','warning','error', \
+ 'signed','invoiced','endOfSeries','powerFailure','invoiceConfirmed'])
+
+class Request(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'req'
+ plugin_attrib = name
+ interfaces = set(['seqnr','nodes','fields','serviceToken','deviceToken','userToken','from','to','when','historical','all'])
+ interfaces.update(FieldTypes.field_types)
+ _flags = set(['serviceToken','deviceToken','userToken','from','to','when','historical','all'])
+ _flags.update(FieldTypes.field_types)
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._nodes = set()
+ self._fields = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._nodes = set([node['nodeId'] for node in self['nodes']])
+ self._fields = set([field['name'] for field in self['fields']])
+
+ def _get_flags(self):
+ """
+ Helper function for getting of flags. Returns all flags in
+ dictionary format: { "flag name": "flag value" ... }
+ """
+ flags = {}
+ for f in self._flags:
+ if not self[f] == "":
+ flags[f] = self[f]
+ return flags
+
+ def _set_flags(self, flags):
+ """
+ Helper function for setting of flags.
+
+ Arguments:
+ flags -- Flags in dictionary format: { "flag name": "flag value" ... }
+ """
+ for f in self._flags:
+ if flags is not None and f in flags:
+ self[f] = flags[f]
+ else:
+ self[f] = None
+
+ def add_node(self, nodeId, sourceId=None, cacheType=None):
+ """
+ Add a new node element. Each item is required to have a
+ nodeId, but may also specify a sourceId value and cacheType.
+
+ Arguments:
+ nodeId -- The ID for the node.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ if nodeId not in self._nodes:
+ self._nodes.add((nodeId))
+ node = RequestNode(parent=self)
+ node['nodeId'] = nodeId
+ node['sourceId'] = sourceId
+ node['cacheType'] = cacheType
+ self.iterables.append(node)
+ return node
+ return None
+
+ def del_node(self, nodeId):
+ """
+ Remove a single node.
+
+ Arguments:
+ nodeId -- Node ID of the item to remove.
+ """
+ if nodeId in self._nodes:
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ if node['nodeId'] == nodeId:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+ return True
+ return False
+
+ def get_nodes(self):
+ """Return all nodes."""
+ nodes = []
+ for node in self['substanzas']:
+ if isinstance(node, RequestNode):
+ nodes.append(node)
+ return nodes
+
+ def set_nodes(self, nodes):
+ """
+ Set or replace all nodes. The given nodes must be in a
+ list or set where each item is a tuple of the form:
+ (nodeId, sourceId, cacheType)
+
+ Arguments:
+ nodes -- A series of nodes in tuple format.
+ """
+ self.del_nodes()
+ for node in nodes:
+ if isinstance(node, RequestNode):
+ self.add_node(node['nodeId'], node['sourceId'], node['cacheType'])
+ else:
+ nodeId, sourceId, cacheType = node
+ self.add_node(nodeId, sourceId, cacheType)
+
+ def del_nodes(self):
+ """Remove all nodes."""
+ self._nodes = set()
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+
+
+ def add_field(self, name):
+ """
+ Add a new field element. Each item is required to have a
+ name.
+
+ Arguments:
+ name -- The name of the field.
+ """
+ if name not in self._fields:
+ self._fields.add((name))
+ field = RequestField(parent=self)
+ field['name'] = name
+ self.iterables.append(field)
+ return field
+ return None
+
+ def del_field(self, name):
+ """
+ Remove a single field.
+
+ Arguments:
+ name -- name of field to remove.
+ """
+ if name in self._fields:
+ fields = [i for i in self.iterables if isinstance(i, RequestField)]
+ for field in fields:
+ if field['name'] == name:
+ self.xml.remove(field.xml)
+ self.iterables.remove(field)
+ return True
+ return False
+
+ def get_fields(self):
+ """Return all fields."""
+ fields = []
+ for field in self['substanzas']:
+ if isinstance(field, RequestField):
+ fields.append(field)
+ return fields
+
+ def set_fields(self, fields):
+ """
+ Set or replace all fields. The given fields must be in a
+ list or set where each item is RequestField or string
+
+ Arguments:
+ fields -- A series of fields in RequestField or string format.
+ """
+ self.del_fields()
+ for field in fields:
+ if isinstance(field, RequestField):
+ self.add_field(field['name'])
+ else:
+ self.add_field(field)
+
+ def del_fields(self):
+ """Remove all fields."""
+ self._fields = set()
+ fields = [i for i in self.iterables if isinstance(i, RequestField)]
+ for field in fields:
+ self.xml.remove(field.xml)
+ self.iterables.remove(field)
+
+
+class RequestNode(ElementBase):
+ """ Node element in a request """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'node'
+ plugin_attrib = name
+ interfaces = set(['nodeId','sourceId','cacheType'])
+
+class RequestField(ElementBase):
+ """ Field element in a request """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'field'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+class Accepted(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'accepted'
+ plugin_attrib = name
+ interfaces = set(['seqnr','queued'])
+
+class Started(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'started'
+ plugin_attrib = name
+ interfaces = set(['seqnr'])
+
+class Failure(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'failure'
+ plugin_attrib = name
+ interfaces = set(['seqnr','done'])
+
+class Error(ElementBase):
+ """ Error element in a request failure """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'error'
+ plugin_attrib = name
+ interfaces = set(['nodeId','timestamp','sourceId','cacheType','text'])
+
+ def get_text(self):
+ """Return then contents inside the XML tag."""
+ return self.xml.text
+
+ def set_text(self, value):
+ """Set then contents inside the XML tag.
+
+ :param value: string
+ """
+
+ self.xml.text = value
+ return self
+
+ def del_text(self):
+ """Remove the contents inside the XML tag."""
+ self.xml.text = ""
+ return self
+
+class Rejected(ElementBase):
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'rejected'
+ plugin_attrib = name
+ interfaces = set(['seqnr','error'])
+ sub_interfaces = set(['error'])
+
+class Fields(ElementBase):
+ """ Fields element, top level in a response message with data """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'fields'
+ plugin_attrib = name
+ interfaces = set(['seqnr','done','nodes'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._nodes = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._nodes = set([node['nodeId'] for node in self['nodes']])
+
+
+ def add_node(self, nodeId, sourceId=None, cacheType=None, substanzas=None):
+ """
+ Add a new node element. Each item is required to have a
+ nodeId, but may also specify a sourceId value and cacheType.
+
+ Arguments:
+ nodeId -- The ID for the node.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ if nodeId not in self._nodes:
+ self._nodes.add((nodeId))
+ node = FieldsNode(parent=self)
+ node['nodeId'] = nodeId
+ node['sourceId'] = sourceId
+ node['cacheType'] = cacheType
+ if substanzas is not None:
+ node.set_timestamps(substanzas)
+
+ self.iterables.append(node)
+ return node
+ return None
+
+ def del_node(self, nodeId):
+ """
+ Remove a single node.
+
+ Arguments:
+ nodeId -- Node ID of the item to remove.
+ """
+ if nodeId in self._nodes:
+ nodes = [i for i in self.iterables if isinstance(i, FieldsNode)]
+ for node in nodes:
+ if node['nodeId'] == nodeId:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+ return True
+ return False
+
+ def get_nodes(self):
+ """Return all nodes."""
+ nodes = []
+ for node in self['substanzas']:
+ if isinstance(node, FieldsNode):
+ nodes.append(node)
+ return nodes
+
+ def set_nodes(self, nodes):
+ """
+ Set or replace all nodes. The given nodes must be in a
+ list or set where each item is a tuple of the form:
+ (nodeId, sourceId, cacheType)
+
+ Arguments:
+ nodes -- A series of nodes in tuple format.
+ """
+ #print(str(id(self)) + " set_nodes: got " + str(nodes))
+ self.del_nodes()
+ for node in nodes:
+ if isinstance(node, FieldsNode):
+ self.add_node(node['nodeId'], node['sourceId'], node['cacheType'], substanzas=node['substanzas'])
+ else:
+ nodeId, sourceId, cacheType = node
+ self.add_node(nodeId, sourceId, cacheType)
+
+ def del_nodes(self):
+ """Remove all nodes."""
+ self._nodes = set()
+ nodes = [i for i in self.iterables if isinstance(i, FieldsNode)]
+ for node in nodes:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+
+
+class FieldsNode(ElementBase):
+ """ Node element in response fields """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'node'
+ plugin_attrib = name
+ interfaces = set(['nodeId','sourceId','cacheType','timestamps'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._timestamps = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._timestamps = set([ts['value'] for ts in self['timestamps']])
+
+ def add_timestamp(self, timestamp, substanzas=None):
+ """
+ Add a new timestamp element.
+
+ Arguments:
+ timestamp -- The timestamp in ISO format.
+ """
+ #print(str(id(self)) + " add_timestamp: " + str(timestamp))
+
+ if timestamp not in self._timestamps:
+ self._timestamps.add((timestamp))
+ ts = Timestamp(parent=self)
+ ts['value'] = timestamp
+ if not substanzas is None:
+ ts.set_datas(substanzas)
+ #print("add_timestamp with substanzas: " + str(substanzas))
+ self.iterables.append(ts)
+ #print(str(id(self)) + " added_timestamp: " + str(id(ts)))
+ return ts
+ return None
+
+ def del_timestamp(self, timestamp):
+ """
+ Remove a single timestamp.
+
+ Arguments:
+ timestamp -- timestamp (in ISO format) of the item to remove.
+ """
+ #print("del_timestamp: ")
+ if timestamp in self._timestamps:
+ timestamps = [i for i in self.iterables if isinstance(i, Timestamp)]
+ for ts in timestamps:
+ if ts['value'] == timestamp:
+ self.xml.remove(ts.xml)
+ self.iterables.remove(ts)
+ return True
+ return False
+
+ def get_timestamps(self):
+ """Return all timestamps."""
+ #print(str(id(self)) + " get_timestamps: ")
+ timestamps = []
+ for timestamp in self['substanzas']:
+ if isinstance(timestamp, Timestamp):
+ timestamps.append(timestamp)
+ return timestamps
+
+ def set_timestamps(self, timestamps):
+ """
+ Set or replace all timestamps. The given timestamps must be in a
+ list or set where each item is a timestamp
+
+ Arguments:
+ timestamps -- A series of timestamps.
+ """
+ #print(str(id(self)) + " set_timestamps: got " + str(timestamps))
+ self.del_timestamps()
+ for timestamp in timestamps:
+ #print("set_timestamps: subset " + str(timestamp))
+ #print("set_timestamps: subset.substanzas " + str(timestamp['substanzas']))
+ if isinstance(timestamp, Timestamp):
+ self.add_timestamp(timestamp['value'], substanzas=timestamp['substanzas'])
+ else:
+ #print("set_timestamps: got " + str(timestamp))
+ self.add_timestamp(timestamp)
+
+ def del_timestamps(self):
+ """Remove all timestamps."""
+ #print(str(id(self)) + " del_timestamps: ")
+ self._timestamps = set()
+ timestamps = [i for i in self.iterables if isinstance(i, Timestamp)]
+ for timestamp in timestamps:
+ self.xml.remove(timestamp.xml)
+ self.iterables.remove(timestamp)
+
+class Field(ElementBase):
+ """
+ Field element in response Timestamp. This is a base class,
+ all instances of fields added to Timestamp must be of types:
+ DataNumeric
+ DataString
+ DataBoolean
+ DataDateTime
+ DataTimeSpan
+ DataEnum
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'field'
+ plugin_attrib = name
+ interfaces = set(['name','module','stringIds'])
+ interfaces.update(FieldTypes.field_types)
+ interfaces.update(FieldStatus.field_status)
+
+ _flags = set()
+ _flags.update(FieldTypes.field_types)
+ _flags.update(FieldStatus.field_status)
+
+ def set_stringIds(self, value):
+ """Verifies stringIds according to regexp from specification XMPP-0323.
+
+ :param value: string
+ """
+
+ pattern = re.compile("^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$")
+ if pattern.match(value) is not None:
+ self.xml.stringIds = value
+ else:
+ # Bad content, add nothing
+ pass
+
+ return self
+
+ def _get_flags(self):
+ """
+ Helper function for getting of flags. Returns all flags in
+ dictionary format: { "flag name": "flag value" ... }
+ """
+ flags = {}
+ for f in self._flags:
+ if not self[f] == "":
+ flags[f] = self[f]
+ return flags
+
+ def _set_flags(self, flags):
+ """
+ Helper function for setting of flags.
+
+ Arguments:
+ flags -- Flags in dictionary format: { "flag name": "flag value" ... }
+ """
+ for f in self._flags:
+ if flags is not None and f in flags:
+ self[f] = flags[f]
+ else:
+ self[f] = None
+
+ def _get_typename(self):
+ return "invalid type, use subclasses!"
+
+
+class Timestamp(ElementBase):
+ """ Timestamp element in response Node """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'timestamp'
+ plugin_attrib = name
+ interfaces = set(['value','datas'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._datas = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._datas = set([data['name'] for data in self['datas']])
+
+ def add_data(self, typename, name, value, module=None, stringIds=None, unit=None, dataType=None, flags=None):
+ """
+ Add a new data element.
+
+ Arguments:
+ typename -- The type of data element (numeric, string, boolean, dateTime, timeSpan or enum)
+ value -- The value of the data element
+ module -- [optional] language module to use for the data element
+ stringIds -- [optional] The stringIds used to find associated text in the language module
+ unit -- [optional] The unit. Only applicable for type numeric
+ dataType -- [optional] The dataType. Only applicable for type enum
+ """
+ if name not in self._datas:
+ dataObj = None
+ if typename == "numeric":
+ dataObj = DataNumeric(parent=self)
+ dataObj['unit'] = unit
+ elif typename == "string":
+ dataObj = DataString(parent=self)
+ elif typename == "boolean":
+ dataObj = DataBoolean(parent=self)
+ elif typename == "dateTime":
+ dataObj = DataDateTime(parent=self)
+ elif typename == "timeSpan":
+ dataObj = DataTimeSpan(parent=self)
+ elif typename == "enum":
+ dataObj = DataEnum(parent=self)
+ dataObj['dataType'] = dataType
+
+ dataObj['name'] = name
+ dataObj['value'] = value
+ dataObj['module'] = module
+ dataObj['stringIds'] = stringIds
+
+ if flags is not None:
+ dataObj._set_flags(flags)
+
+ self._datas.add(name)
+ self.iterables.append(dataObj)
+ return dataObj
+ return None
+
+ def del_data(self, name):
+ """
+ Remove a single data element.
+
+ Arguments:
+ data_name -- The data element name to remove.
+ """
+ if name in self._datas:
+ datas = [i for i in self.iterables if isinstance(i, Field)]
+ for data in datas:
+ if data['name'] == name:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+ return True
+ return False
+
+ def get_datas(self):
+ """ Return all data elements. """
+ datas = []
+ for data in self['substanzas']:
+ if isinstance(data, Field):
+ datas.append(data)
+ return datas
+
+ def set_datas(self, datas):
+ """
+ Set or replace all data elements. The given elements must be in a
+ list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum)
+
+ Arguments:
+ datas -- A series of data elements.
+ """
+ self.del_datas()
+ for data in datas:
+ self.add_data(typename=data._get_typename(), name=data['name'], value=data['value'], module=data['module'], stringIds=data['stringIds'], unit=data['unit'], dataType=data['dataType'], flags=data._get_flags())
+
+ def del_datas(self):
+ """Remove all data elements."""
+ self._datas = set()
+ datas = [i for i in self.iterables if isinstance(i, Field)]
+ for data in datas:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+
+class DataNumeric(Field):
+ """
+ Field data of type numeric.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'numeric'
+ plugin_attrib = name
+ interfaces = set(['value', 'unit'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "numeric"
+
+class DataString(Field):
+ """
+ Field data of type string
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'string'
+ plugin_attrib = name
+ interfaces = set(['value'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "string"
+
+class DataBoolean(Field):
+ """
+ Field data of type boolean.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'boolean'
+ plugin_attrib = name
+ interfaces = set(['value'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "boolean"
+
+class DataDateTime(Field):
+ """
+ Field data of type dateTime.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'dateTime'
+ plugin_attrib = name
+ interfaces = set(['value'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "dateTime"
+
+class DataTimeSpan(Field):
+ """
+ Field data of type timeSpan.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'timeSpan'
+ plugin_attrib = name
+ interfaces = set(['value'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "timeSpan"
+
+class DataEnum(Field):
+ """
+ Field data of type enum.
+ Note that the value is expressed as a string.
+ """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'enum'
+ plugin_attrib = name
+ interfaces = set(['value', 'dataType'])
+ interfaces.update(Field.interfaces)
+
+ def _get_typename(self):
+ return "enum"
+
+class Done(ElementBase):
+ """ Done element used to signal that all data has been transferred """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'done'
+ plugin_attrib = name
+ interfaces = set(['seqnr'])
+
+class Cancel(ElementBase):
+ """ Cancel element used to signal that a request shall be cancelled """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'cancel'
+ plugin_attrib = name
+ interfaces = set(['seqnr'])
+
+class Cancelled(ElementBase):
+ """ Cancelled element used to signal that cancellation is confirmed """
+ namespace = 'urn:xmpp:iot:sensordata'
+ name = 'cancelled'
+ plugin_attrib = name
+ interfaces = set(['seqnr'])
+
+
+register_stanza_plugin(Iq, Request)
+register_stanza_plugin(Request, RequestNode, iterable=True)
+register_stanza_plugin(Request, RequestField, iterable=True)
+
+register_stanza_plugin(Iq, Accepted)
+register_stanza_plugin(Message, Failure)
+register_stanza_plugin(Failure, Error)
+
+register_stanza_plugin(Iq, Rejected)
+
+register_stanza_plugin(Message, Fields)
+register_stanza_plugin(Fields, FieldsNode, iterable=True)
+register_stanza_plugin(FieldsNode, Timestamp, iterable=True)
+register_stanza_plugin(Timestamp, Field, iterable=True)
+register_stanza_plugin(Timestamp, DataNumeric, iterable=True)
+register_stanza_plugin(Timestamp, DataString, iterable=True)
+register_stanza_plugin(Timestamp, DataBoolean, iterable=True)
+register_stanza_plugin(Timestamp, DataDateTime, iterable=True)
+register_stanza_plugin(Timestamp, DataTimeSpan, iterable=True)
+register_stanza_plugin(Timestamp, DataEnum, iterable=True)
+
+register_stanza_plugin(Message, Started)
+
+register_stanza_plugin(Iq, Cancel)
+register_stanza_plugin(Iq, Cancelled)
diff --git a/sleekxmpp/plugins/xep_0323/timerreset.py b/sleekxmpp/plugins/xep_0323/timerreset.py
new file mode 100644
index 00000000..398b47c1
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0323/timerreset.py
@@ -0,0 +1,69 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+from threading import Thread, Event, Timer
+import time
+
+def TimerReset(*args, **kwargs):
+ """ Global function for Timer """
+ return _TimerReset(*args, **kwargs)
+
+
+class _TimerReset(Thread):
+ """Call a function after a specified number of seconds:
+
+ t = TimerReset(30.0, f, args=[], kwargs={})
+ t.start()
+ t.cancel() # stop the timer's action if it's still waiting
+ """
+
+ def __init__(self, interval, function, args=None, kwargs=None):
+ if not kwargs:
+ kwargs = {}
+ if not args:
+ args = []
+
+ Thread.__init__(self)
+ self.interval = interval
+ self.function = function
+ self.args = args
+ self.kwargs = kwargs
+ self.finished = Event()
+ self.resetted = True
+
+ def cancel(self):
+ """Stop the timer if it hasn't finished yet"""
+ self.finished.set()
+
+ def run(self):
+ #print "Time: %s - timer running..." % time.asctime()
+
+ while self.resetted:
+ #print "Time: %s - timer waiting for timeout in %.2f..." % (time.asctime(), self.interval)
+ self.resetted = False
+ self.finished.wait(self.interval)
+
+ if not self.finished.isSet():
+ self.function(*self.args, **self.kwargs)
+ self.finished.set()
+ #print "Time: %s - timer finished!" % time.asctime()
+
+ def reset(self, interval=None):
+ """ Reset the timer """
+
+ if interval:
+ #print "Time: %s - timer resetting to %.2f..." % (time.asctime(), interval)
+ self.interval = interval
+ else:
+ #print "Time: %s - timer resetting..." % time.asctime()
+ pass
+
+ self.resetted = True
+ self.finished.set()
+ self.finished.clear()
diff --git a/sleekxmpp/plugins/xep_0325/__init__.py b/sleekxmpp/plugins/xep_0325/__init__.py
new file mode 100644
index 00000000..01c38dce
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0325/__init__.py
@@ -0,0 +1,18 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0325.control import XEP_0325
+from sleekxmpp.plugins.xep_0325 import stanza
+
+register_plugin(XEP_0325)
+
+xep_0325=XEP_0325
diff --git a/sleekxmpp/plugins/xep_0325/control.py b/sleekxmpp/plugins/xep_0325/control.py
new file mode 100644
index 00000000..11e7a045
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0325/control.py
@@ -0,0 +1,569 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import logging
+import time
+from threading import Thread, Timer, Lock
+
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.plugins.base import BasePlugin
+from sleekxmpp.plugins.xep_0325 import stanza
+from sleekxmpp.plugins.xep_0325.stanza import Control
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0325(BasePlugin):
+
+ """
+ XEP-0325: IoT Control
+
+
+ Actuators are devices in sensor networks that can be controlled through
+ the network and act with the outside world. In sensor networks and
+ Internet of Things applications, actuators make it possible to automate
+ real-world processes.
+ This plugin implements a mechanism whereby actuators can be controlled
+ in XMPP-based sensor networks, making it possible to integrate sensors
+ and actuators of different brands, makes and models into larger
+ Internet of Things applications.
+
+ Also see <http://xmpp.org/extensions/xep-0325.html>
+
+ Configuration Values:
+ threaded -- Indicates if communication with sensors should be threaded.
+ Defaults to True.
+
+ Events:
+ Sensor side
+ -----------
+ Control Event:DirectSet -- Received a control message
+ Control Event:SetReq -- Received a control request
+
+ Client side
+ -----------
+ Control Event:SetResponse -- Received a response to a
+ control request, type result
+ Control Event:SetResponseError -- Received a response to a
+ control request, type error
+
+ Attributes:
+ threaded -- Indicates if command events should be threaded.
+ Defaults to True.
+ sessions -- A dictionary or equivalent backend mapping
+ session IDs to dictionaries containing data
+ relevant to a request's session. This dictionary is used
+ both by the client and sensor side. On client side, seqnr
+ is used as key, while on sensor side, a session_id is used
+ as key. This ensures that the two will not collide, so
+ one instance can be both client and sensor.
+ Sensor side
+ -----------
+ nodes -- A dictionary mapping sensor nodes that are serviced through
+ this XMPP instance to their device handlers ("drivers").
+ Client side
+ -----------
+ last_seqnr -- The last used sequence number (integer). One sequence of
+ communication (e.g. -->request, <--accept, <--fields)
+ between client and sensor is identified by a unique
+ sequence number (unique between the client/sensor pair)
+
+ Methods:
+ plugin_init -- Overrides base_plugin.plugin_init
+ post_init -- Overrides base_plugin.post_init
+ plugin_end -- Overrides base_plugin.plugin_end
+
+ Sensor side
+ -----------
+ register_node -- Register a sensor as available from this XMPP
+ instance.
+
+ Client side
+ -----------
+ set_request -- Initiates a control request to modify data in
+ sensor(s). Non-blocking, a callback function will
+ be called when the sensor has responded.
+ set_command -- Initiates a control command to modify data in
+ sensor(s). Non-blocking. The sensor(s) will not
+ respond regardless of the result of the command,
+ so no callback is made.
+
+ """
+
+ name = 'xep_0325'
+ description = 'XEP-0325 Internet of Things - Control'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+
+ default_config = {
+ 'threaded': True
+# 'session_db': None
+ }
+
+ def plugin_init(self):
+ """ Start the XEP-0325 plugin """
+
+ self.xmpp.register_handler(
+ Callback('Control Event:DirectSet',
+ StanzaPath('message/set'),
+ self._handle_direct_set))
+
+ self.xmpp.register_handler(
+ Callback('Control Event:SetReq',
+ StanzaPath('iq@type=set/set'),
+ self._handle_set_req))
+
+ self.xmpp.register_handler(
+ Callback('Control Event:SetResponse',
+ StanzaPath('iq@type=result/setResponse'),
+ self._handle_set_response))
+
+ self.xmpp.register_handler(
+ Callback('Control Event:SetResponseError',
+ StanzaPath('iq@type=error/setResponse'),
+ self._handle_set_response))
+
+ # Server side dicts
+ self.nodes = {}
+ self.sessions = {}
+
+ self.last_seqnr = 0
+ self.seqnr_lock = Lock()
+
+ ## For testning only
+ self.test_authenticated_from = ""
+
+ def post_init(self):
+ """ Init complete. Register our features in Serivce discovery. """
+ BasePlugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature(Control.namespace)
+ self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple())
+
+ def _new_session(self):
+ """ Return a new session ID. """
+ return str(time.time()) + '-' + self.xmpp.new_id()
+
+ def plugin_end(self):
+ """ Stop the XEP-0325 plugin """
+ self.sessions.clear()
+ self.xmpp.remove_handler('Control Event:DirectSet')
+ self.xmpp.remove_handler('Control Event:SetReq')
+ self.xmpp.remove_handler('Control Event:SetResponse')
+ self.xmpp.remove_handler('Control Event:SetResponseError')
+ self.xmpp['xep_0030'].del_feature(feature=Control.namespace)
+ self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple())
+
+
+ # =================================================================
+ # Sensor side (data provider) API
+
+ def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None):
+ """
+ Register a sensor/device as available for control requests/commands
+ through this XMPP instance.
+
+ The device object may by any custom implementation to support
+ specific devices, but it must implement the functions:
+ has_control_field
+ set_control_fields
+ according to the interfaces shown in the example device.py file.
+
+ Arguments:
+ nodeId -- The identifier for the device
+ device -- The device object
+ commTimeout -- Time in seconds to wait between each callback from device during
+ a data readout. Float.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ self.nodes[nodeId] = {"device": device,
+ "commTimeout": commTimeout,
+ "sourceId": sourceId,
+ "cacheType": cacheType}
+
+ def _set_authenticated(self, auth=''):
+ """ Internal testing function """
+ self.test_authenticated_from = auth
+
+ def _get_new_seqnr(self):
+ """ Returns a unique sequence number (unique across threads) """
+ self.seqnr_lock.acquire()
+ self.last_seqnr += 1
+ self.seqnr_lock.release()
+ return str(self.last_seqnr)
+
+ def _handle_set_req(self, iq):
+ """
+ Event handler for reception of an Iq with set req - this is a
+ control request.
+
+ Verifies that
+ - all the requested nodes are available
+ (if no nodes are specified in the request, assume all nodes)
+ - all the control fields are available from all requested nodes
+ (if no nodes are specified in the request, assume all nodes)
+
+ If the request passes verification, the control request is passed
+ to the devices (in a separate thread).
+ If the verification fails, a setResponse with error indication
+ is sent.
+ """
+
+ error_msg = ''
+ req_ok = True
+ missing_node = None
+ missing_field = None
+
+ # Authentication
+ if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from:
+ # Invalid authentication
+ req_ok = False
+ error_msg = "Access denied"
+
+ # Nodes
+ if len(iq['set']['nodes']) > 0:
+ for n in iq['set']['nodes']:
+ if not n['nodeId'] in self.nodes:
+ req_ok = False
+ missing_node = n['nodeId']
+ error_msg = "Invalid nodeId " + n['nodeId']
+ process_nodes = [n['nodeId'] for n in iq['set']['nodes']]
+ else:
+ process_nodes = self.nodes.keys()
+
+ # Fields - for control we need to find all in all devices, otherwise we reject
+ process_fields = []
+ if len(iq['set']['datas']) > 0:
+ for f in iq['set']['datas']:
+ for node in self.nodes:
+ if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()):
+ req_ok = False
+ missing_field = f['name']
+ error_msg = "Invalid field " + f['name']
+ break
+ process_fields = [(f['name'], f._get_typename(), f['value']) for f in iq['set']['datas']]
+
+ if req_ok:
+ session = self._new_session()
+ self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": iq['id']}
+ self.sessions[session]["commTimers"] = {}
+ self.sessions[session]["nodeDone"] = {}
+ # Flag that a reply is exected when we are done
+ self.sessions[session]["reply"] = True
+
+ self.sessions[session]["node_list"] = process_nodes
+ if self.threaded:
+ #print("starting thread")
+ tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields))
+ tr_req.start()
+ #print("started thread")
+ else:
+ self._threaded_node_request(session, process_fields)
+
+ else:
+ iq.reply()
+ iq['type'] = 'error'
+ iq['setResponse']['responseCode'] = "NotFound"
+ if missing_node is not None:
+ iq['setResponse'].add_node(missing_node)
+ if missing_field is not None:
+ iq['setResponse'].add_data(missing_field)
+ iq['setResponse']['error']['var'] = "Output"
+ iq['setResponse']['error']['text'] = error_msg
+ iq.send(block=False)
+
+ def _handle_direct_set(self, msg):
+ """
+ Event handler for reception of a Message with set command - this is a
+ direct control command.
+
+ Verifies that
+ - all the requested nodes are available
+ (if no nodes are specified in the request, assume all nodes)
+ - all the control fields are available from all requested nodes
+ (if no nodes are specified in the request, assume all nodes)
+
+ If the request passes verification, the control request is passed
+ to the devices (in a separate thread).
+ If the verification fails, do nothing.
+ """
+ req_ok = True
+
+ # Nodes
+ if len(msg['set']['nodes']) > 0:
+ for n in msg['set']['nodes']:
+ if not n['nodeId'] in self.nodes:
+ req_ok = False
+ error_msg = "Invalid nodeId " + n['nodeId']
+ process_nodes = [n['nodeId'] for n in msg['set']['nodes']]
+ else:
+ process_nodes = self.nodes.keys()
+
+ # Fields - for control we need to find all in all devices, otherwise we reject
+ process_fields = []
+ if len(msg['set']['datas']) > 0:
+ for f in msg['set']['datas']:
+ for node in self.nodes:
+ if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()):
+ req_ok = False
+ missing_field = f['name']
+ error_msg = "Invalid field " + f['name']
+ break
+ process_fields = [(f['name'], f._get_typename(), f['value']) for f in msg['set']['datas']]
+
+ if req_ok:
+ session = self._new_session()
+ self.sessions[session] = {"from": msg['from'], "to": msg['to']}
+ self.sessions[session]["commTimers"] = {}
+ self.sessions[session]["nodeDone"] = {}
+ self.sessions[session]["reply"] = False
+
+ self.sessions[session]["node_list"] = process_nodes
+ if self.threaded:
+ #print("starting thread")
+ tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields))
+ tr_req.start()
+ #print("started thread")
+ else:
+ self._threaded_node_request(session, process_fields)
+
+
+ def _threaded_node_request(self, session, process_fields):
+ """
+ Helper function to handle the device control in a separate thread.
+
+ Arguments:
+ session -- The request session id
+ process_fields -- The fields to set in the devices. List of tuple format:
+ (name, datatype, value)
+ """
+ for node in self.sessions[session]["node_list"]:
+ self.sessions[session]["nodeDone"][node] = False
+
+ for node in self.sessions[session]["node_list"]:
+ timer = Timer(self.nodes[node]['commTimeout'], self._event_comm_timeout, args=(session, node))
+ self.sessions[session]["commTimers"][node] = timer
+ timer.start()
+ self.nodes[node]['device'].set_control_fields(process_fields, session=session, callback=self._device_set_command_callback)
+
+ def _event_comm_timeout(self, session, nodeId):
+ """
+ Triggered if any of the control operations timeout.
+ Stop communicating with the failing device.
+ If the control command was an Iq request, sends a failure
+ message back to the client.
+
+ Arguments:
+ session -- The request session id
+ nodeId -- The id of the device which timed out
+ """
+
+ if self.sessions[session]["reply"]:
+ # Reply is exected when we are done
+ iq = self.xmpp.Iq()
+ iq['from'] = self.sessions[session]['to']
+ iq['to'] = self.sessions[session]['from']
+ iq['type'] = "error"
+ iq['id'] = self.sessions[session]['seqnr']
+ iq['setResponse']['responseCode'] = "OtherError"
+ iq['setResponse'].add_node(nodeId)
+ iq['setResponse']['error']['var'] = "Output"
+ iq['setResponse']['error']['text'] = "Timeout."
+ iq.send(block=False)
+
+ ## TODO - should we send one timeout per node??
+
+ # Drop communication with this device and check if we are done
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ # The session is complete, delete it
+ del self.sessions[session]
+
+ def _all_nodes_done(self, session):
+ """
+ Checks wheter all devices are done replying to the control command.
+
+ Arguments:
+ session -- The request session id
+ """
+ for n in self.sessions[session]["nodeDone"]:
+ if not self.sessions[session]["nodeDone"][n]:
+ return False
+ return True
+
+ def _device_set_command_callback(self, session, nodeId, result, error_field=None, error_msg=None):
+ """
+ Callback function called by the devices when the control command is
+ complete or failed.
+ If needed, composes a message with the result and sends it back to the
+ client.
+
+ Arguments:
+ session -- The request session id
+ nodeId -- The device id which initiated the callback
+ result -- The current result status of the control command. Valid values are:
+ "error" - Set fields failed.
+ "ok" - All fields were set.
+ error_field -- [optional] Only applies when result == "error"
+ The field name that failed (usually means it is missing)
+ error_msg -- [optional] Only applies when result == "error".
+ Error details when a request failed.
+ """
+
+ if not session in self.sessions:
+ # This can happend if a session was deleted, like in a timeout. Just drop the data.
+ return
+
+ if result == "error":
+ self.sessions[session]["commTimers"][nodeId].cancel()
+
+ if self.sessions[session]["reply"]:
+ # Reply is exected when we are done
+ iq = self.xmpp.Iq()
+ iq['from'] = self.sessions[session]['to']
+ iq['to'] = self.sessions[session]['from']
+ iq['type'] = "error"
+ iq['id'] = self.sessions[session]['seqnr']
+ iq['setResponse']['responseCode'] = "OtherError"
+ iq['setResponse'].add_node(nodeId)
+ if error_field is not None:
+ iq['setResponse'].add_data(error_field)
+ iq['setResponse']['error']['var'] = error_field
+ iq['setResponse']['error']['text'] = error_msg
+ iq.send(block=False)
+
+ # Drop communication with this device and check if we are done
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ # The session is complete, delete it
+ del self.sessions[session]
+ else:
+ self.sessions[session]["commTimers"][nodeId].cancel()
+
+ self.sessions[session]["nodeDone"][nodeId] = True
+ if (self._all_nodes_done(session)):
+ if self.sessions[session]["reply"]:
+ # Reply is exected when we are done
+ iq = self.xmpp.Iq()
+ iq['from'] = self.sessions[session]['to']
+ iq['to'] = self.sessions[session]['from']
+ iq['type'] = "result"
+ iq['id'] = self.sessions[session]['seqnr']
+ iq['setResponse']['responseCode'] = "OK"
+ iq.send(block=False)
+
+ # The session is complete, delete it
+ del self.sessions[session]
+
+
+ # =================================================================
+ # Client side (data controller) API
+
+ def set_request(self, from_jid, to_jid, callback, fields, nodeIds=None):
+ """
+ Called on the client side to initiade a control request.
+ Composes a message with the request and sends it to the device(s).
+ Does not block, the callback will be called when the device(s)
+ has responded.
+
+ Arguments:
+ from_jid -- The jid of the requester
+ to_jid -- The jid of the device(s)
+ callback -- The callback function to call when data is availble.
+
+ The callback function must support the following arguments:
+
+ from_jid -- The jid of the responding device(s)
+ result -- The result of the control request. Valid values are:
+ "OK" - Control request completed successfully
+ "NotFound" - One or more nodes or fields are missing
+ "InsufficientPrivileges" - Not authorized.
+ "Locked" - Field(s) is locked and cannot
+ be changed at the moment.
+ "NotImplemented" - Request feature not implemented.
+ "FormError" - Error while setting with
+ a form (not implemented).
+ "OtherError" - Indicates other types of
+ errors, such as timeout.
+ Details in the error_msg.
+
+
+ nodeId -- [optional] Only applicable when result == "error"
+ List of node Ids of failing device(s).
+
+ fields -- [optional] Only applicable when result == "error"
+ List of fields that failed.[optional] Mandatory when result == "rejected" or "failure".
+
+ error_msg -- Details about why the request failed.
+
+ fields -- Fields to set. List of tuple format: (name, typename, value).
+ nodeIds -- [optional] Limits the request to the node Ids in this list.
+ """
+ iq = self.xmpp.Iq()
+ iq['from'] = from_jid
+ iq['to'] = to_jid
+ seqnr = self._get_new_seqnr()
+ iq['id'] = seqnr
+ iq['type'] = "set"
+ if nodeIds is not None:
+ for nodeId in nodeIds:
+ iq['set'].add_node(nodeId)
+ if fields is not None:
+ for name, typename, value in fields:
+ iq['set'].add_data(name=name, typename=typename, value=value)
+
+ self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "callback": callback}
+ iq.send(block=False)
+
+ def set_command(self, from_jid, to_jid, fields, nodeIds=None):
+ """
+ Called on the client side to initiade a control command.
+ Composes a message with the set commandand sends it to the device(s).
+ Does not block. Device(s) will not respond, regardless of result.
+
+ Arguments:
+ from_jid -- The jid of the requester
+ to_jid -- The jid of the device(s)
+
+ fields -- Fields to set. List of tuple format: (name, typename, value).
+ nodeIds -- [optional] Limits the request to the node Ids in this list.
+ """
+ msg = self.xmpp.Message()
+ msg['from'] = from_jid
+ msg['to'] = to_jid
+ msg['type'] = "set"
+ if nodeIds is not None:
+ for nodeId in nodeIds:
+ msg['set'].add_node(nodeId)
+ if fields is not None:
+ for name, typename, value in fields:
+ msg['set'].add_data(name, typename, value)
+
+ # We won't get any reply, so don't create a session
+ msg.send()
+
+ def _handle_set_response(self, iq):
+ """ Received response from device(s) """
+ #print("ooh")
+ seqnr = iq['id']
+ from_jid = str(iq['from'])
+ result = iq['setResponse']['responseCode']
+ nodeIds = [n['name'] for n in iq['setResponse']['nodes']]
+ fields = [f['name'] for f in iq['setResponse']['datas']]
+ error_msg = None
+
+ if not iq['setResponse'].find('error') is None and not iq['setResponse']['error']['text'] == "":
+ error_msg = iq['setResponse']['error']['text']
+
+ callback = self.sessions[seqnr]["callback"]
+ callback(from_jid=from_jid, result=result, nodeIds=nodeIds, fields=fields, error_msg=error_msg)
diff --git a/sleekxmpp/plugins/xep_0325/device.py b/sleekxmpp/plugins/xep_0325/device.py
new file mode 100644
index 00000000..f1ed0733
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0325/device.py
@@ -0,0 +1,125 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+import datetime
+
+class Device(object):
+ """
+ Example implementation of a device control object.
+
+ The device object may by any custom implementation to support
+ specific devices, but it must implement the functions:
+ has_control_field
+ set_control_fields
+ """
+
+ def __init__(self, nodeId):
+ self.nodeId = nodeId
+ self.control_fields = {}
+
+ def has_control_field(self, field, typename):
+ """
+ Returns true if the supplied field name exists
+ and the type matches for control in this device.
+
+ Arguments:
+ field -- The field name
+ typename -- The expected type
+ """
+ if field in self.control_fields and self.control_fields[field]["type"] == typename:
+ return True
+ return False
+
+ def set_control_fields(self, fields, session, callback):
+ """
+ Starts a control setting procedure. Verifies the fields,
+ sets the data and (if needed) and calls the callback.
+
+ Arguments:
+ fields -- List of control fields in tuple format:
+ (name, typename, value)
+ session -- Session id, only used in the callback as identifier
+ callback -- Callback function to call when control set is complete.
+
+ The callback function must support the following arguments:
+
+ session -- Session id, as supplied in the
+ request_fields call
+ nodeId -- Identifier for this device
+ result -- The current result status of the readout.
+ Valid values are:
+ "error" - Set fields failed.
+ "ok" - All fields were set.
+ error_field -- [optional] Only applies when result == "error"
+ The field name that failed
+ (usually means it is missing)
+ error_msg -- [optional] Only applies when result == "error".
+ Error details when a request failed.
+ """
+
+ if len(fields) > 0:
+ # Check availiability
+ for name, typename, value in fields:
+ if not self.has_control_field(name, typename):
+ self._send_control_reject(session, name, "NotFound", callback)
+ return False
+
+ for name, typename, value in fields:
+ self._set_field_value(name, value)
+
+ callback(session, result="ok", nodeId=self.nodeId)
+ return True
+
+ def _send_control_reject(self, session, field, message, callback):
+ """
+ Sends a reject to the caller
+
+ Arguments:
+ session -- Session id, see definition in
+ set_control_fields function
+ callback -- Callback function, see definition in
+ set_control_fields function
+ """
+ callback(session, result="error", nodeId=self.nodeId, error_field=field, error_msg=message)
+
+ def _add_control_field(self, name, typename, value):
+ """
+ Adds a control field to the device
+
+ Arguments:
+ name -- Name of the field
+ typename -- Type of the field, one of:
+ (boolean, color, string, date, dateTime,
+ double, duration, int, long, time)
+ value -- Field value
+ """
+ self.control_fields[name] = {"type": typename, "value": value}
+
+ def _set_field_value(self, name, value):
+ """
+ Set the value of a control field
+
+ Arguments:
+ name -- Name of the field
+ value -- New value for the field
+ """
+ if name in self.control_fields:
+ self.control_fields[name]["value"] = value
+
+ def _get_field_value(self, name):
+ """
+ Get the value of a control field. Only used for unit testing.
+
+ Arguments:
+ name -- Name of the field
+ """
+ if name in self.control_fields:
+ return self.control_fields[name]["value"]
+ return None
diff --git a/sleekxmpp/plugins/xep_0325/stanza/__init__.py b/sleekxmpp/plugins/xep_0325/stanza/__init__.py
new file mode 100644
index 00000000..746c2033
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0325/stanza/__init__.py
@@ -0,0 +1,12 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.plugins.xep_0325.stanza.control import *
+
diff --git a/sleekxmpp/plugins/xep_0325/stanza/base.py b/sleekxmpp/plugins/xep_0325/stanza/base.py
new file mode 100644
index 00000000..1dadcf46
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0325/stanza/base.py
@@ -0,0 +1,13 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp.xmlstream import ET
+
+pass
diff --git a/sleekxmpp/plugins/xep_0325/stanza/control.py b/sleekxmpp/plugins/xep_0325/stanza/control.py
new file mode 100644
index 00000000..1fd5c35d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0325/stanza/control.py
@@ -0,0 +1,527 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Implementation of xeps for Internet of Things
+ http://wiki.xmpp.org/web/Tech_pages/IoT_systems
+ Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
+
+from sleekxmpp import Iq, Message
+from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
+from re import match
+
+class Control(ElementBase):
+ """ Placeholder for the namespace, not used as a stanza """
+ namespace = 'urn:xmpp:iot:control'
+ name = 'control'
+ plugin_attrib = name
+ interfaces = set(tuple())
+
+class ControlSet(ElementBase):
+ namespace = 'urn:xmpp:iot:control'
+ name = 'set'
+ plugin_attrib = name
+ interfaces = set(['nodes','datas'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._nodes = set()
+ self._datas = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._nodes = set([node['nodeId'] for node in self['nodes']])
+ self._datas = set([data['name'] for data in self['datas']])
+
+ def add_node(self, nodeId, sourceId=None, cacheType=None):
+ """
+ Add a new node element. Each item is required to have a
+ nodeId, but may also specify a sourceId value and cacheType.
+
+ Arguments:
+ nodeId -- The ID for the node.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ if nodeId not in self._nodes:
+ self._nodes.add((nodeId))
+ node = RequestNode(parent=self)
+ node['nodeId'] = nodeId
+ node['sourceId'] = sourceId
+ node['cacheType'] = cacheType
+ self.iterables.append(node)
+ return node
+ return None
+
+ def del_node(self, nodeId):
+ """
+ Remove a single node.
+
+ Arguments:
+ nodeId -- Node ID of the item to remove.
+ """
+ if nodeId in self._nodes:
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ if node['nodeId'] == nodeId:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+ return True
+ return False
+
+ def get_nodes(self):
+ """Return all nodes."""
+ nodes = []
+ for node in self['substanzas']:
+ if isinstance(node, RequestNode):
+ nodes.append(node)
+ return nodes
+
+ def set_nodes(self, nodes):
+ """
+ Set or replace all nodes. The given nodes must be in a
+ list or set where each item is a tuple of the form:
+ (nodeId, sourceId, cacheType)
+
+ Arguments:
+ nodes -- A series of nodes in tuple format.
+ """
+ self.del_nodes()
+ for node in nodes:
+ if isinstance(node, RequestNode):
+ self.add_node(node['nodeId'], node['sourceId'], node['cacheType'])
+ else:
+ nodeId, sourceId, cacheType = node
+ self.add_node(nodeId, sourceId, cacheType)
+
+ def del_nodes(self):
+ """Remove all nodes."""
+ self._nodes = set()
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+
+
+ def add_data(self, name, typename, value):
+ """
+ Add a new data element.
+
+ Arguments:
+ name -- The name of the data element
+ typename -- The type of data element
+ (boolean, color, string, date, dateTime,
+ double, duration, int, long, time)
+ value -- The value of the data element
+ """
+ if name not in self._datas:
+ dataObj = None
+ if typename == "boolean":
+ dataObj = BooleanParameter(parent=self)
+ elif typename == "color":
+ dataObj = ColorParameter(parent=self)
+ elif typename == "string":
+ dataObj = StringParameter(parent=self)
+ elif typename == "date":
+ dataObj = DateParameter(parent=self)
+ elif typename == "dateTime":
+ dataObj = DateTimeParameter(parent=self)
+ elif typename == "double":
+ dataObj = DoubleParameter(parent=self)
+ elif typename == "duration":
+ dataObj = DurationParameter(parent=self)
+ elif typename == "int":
+ dataObj = IntParameter(parent=self)
+ elif typename == "long":
+ dataObj = LongParameter(parent=self)
+ elif typename == "time":
+ dataObj = TimeParameter(parent=self)
+
+ dataObj['name'] = name
+ dataObj['value'] = value
+
+ self._datas.add(name)
+ self.iterables.append(dataObj)
+ return dataObj
+ return None
+
+ def del_data(self, name):
+ """
+ Remove a single data element.
+
+ Arguments:
+ data_name -- The data element name to remove.
+ """
+ if name in self._datas:
+ datas = [i for i in self.iterables if isinstance(i, BaseParameter)]
+ for data in datas:
+ if data['name'] == name:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+ return True
+ return False
+
+ def get_datas(self):
+ """ Return all data elements. """
+ datas = []
+ for data in self['substanzas']:
+ if isinstance(data, BaseParameter):
+ datas.append(data)
+ return datas
+
+ def set_datas(self, datas):
+ """
+ Set or replace all data elements. The given elements must be in a
+ list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum)
+
+ Arguments:
+ datas -- A series of data elements.
+ """
+ self.del_datas()
+ for data in datas:
+ self.add_data(name=data['name'], typename=data._get_typename(), value=data['value'])
+
+ def del_datas(self):
+ """Remove all data elements."""
+ self._datas = set()
+ datas = [i for i in self.iterables if isinstance(i, BaseParameter)]
+ for data in datas:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+
+
+class RequestNode(ElementBase):
+ """ Node element in a request """
+ namespace = 'urn:xmpp:iot:control'
+ name = 'node'
+ plugin_attrib = name
+ interfaces = set(['nodeId','sourceId','cacheType'])
+
+
+class ControlSetResponse(ElementBase):
+ namespace = 'urn:xmpp:iot:control'
+ name = 'setResponse'
+ plugin_attrib = name
+ interfaces = set(['responseCode'])
+
+ def __init__(self, xml=None, parent=None):
+ ElementBase.__init__(self, xml, parent)
+ self._nodes = set()
+ self._datas = set()
+
+ def setup(self, xml=None):
+ """
+ Populate the stanza object using an optional XML object.
+
+ Overrides ElementBase.setup
+
+ Caches item information.
+
+ Arguments:
+ xml -- Use an existing XML object for the stanza's values.
+ """
+ ElementBase.setup(self, xml)
+ self._nodes = set([node['nodeId'] for node in self['nodes']])
+ self._datas = set([data['name'] for data in self['datas']])
+
+ def add_node(self, nodeId, sourceId=None, cacheType=None):
+ """
+ Add a new node element. Each item is required to have a
+ nodeId, but may also specify a sourceId value and cacheType.
+
+ Arguments:
+ nodeId -- The ID for the node.
+ sourceId -- [optional] identifying the data source controlling the device
+ cacheType -- [optional] narrowing down the search to a specific kind of node
+ """
+ if nodeId not in self._nodes:
+ self._nodes.add(nodeId)
+ node = RequestNode(parent=self)
+ node['nodeId'] = nodeId
+ node['sourceId'] = sourceId
+ node['cacheType'] = cacheType
+ self.iterables.append(node)
+ return node
+ return None
+
+ def del_node(self, nodeId):
+ """
+ Remove a single node.
+
+ Arguments:
+ nodeId -- Node ID of the item to remove.
+ """
+ if nodeId in self._nodes:
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ if node['nodeId'] == nodeId:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+ return True
+ return False
+
+ def get_nodes(self):
+ """Return all nodes."""
+ nodes = []
+ for node in self['substanzas']:
+ if isinstance(node, RequestNode):
+ nodes.append(node)
+ return nodes
+
+ def set_nodes(self, nodes):
+ """
+ Set or replace all nodes. The given nodes must be in a
+ list or set where each item is a tuple of the form:
+ (nodeId, sourceId, cacheType)
+
+ Arguments:
+ nodes -- A series of nodes in tuple format.
+ """
+ self.del_nodes()
+ for node in nodes:
+ if isinstance(node, RequestNode):
+ self.add_node(node['nodeId'], node['sourceId'], node['cacheType'])
+ else:
+ nodeId, sourceId, cacheType = node
+ self.add_node(nodeId, sourceId, cacheType)
+
+ def del_nodes(self):
+ """Remove all nodes."""
+ self._nodes = set()
+ nodes = [i for i in self.iterables if isinstance(i, RequestNode)]
+ for node in nodes:
+ self.xml.remove(node.xml)
+ self.iterables.remove(node)
+
+
+ def add_data(self, name):
+ """
+ Add a new ResponseParameter element.
+
+ Arguments:
+ name -- Name of the parameter
+ """
+ if name not in self._datas:
+ self._datas.add(name)
+ data = ResponseParameter(parent=self)
+ data['name'] = name
+ self.iterables.append(data)
+ return data
+ return None
+
+ def del_data(self, name):
+ """
+ Remove a single ResponseParameter element.
+
+ Arguments:
+ name -- The data element name to remove.
+ """
+ if name in self._datas:
+ datas = [i for i in self.iterables if isinstance(i, ResponseParameter)]
+ for data in datas:
+ if data['name'] == name:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+ return True
+ return False
+
+ def get_datas(self):
+ """ Return all ResponseParameter elements. """
+ datas = set()
+ for data in self['substanzas']:
+ if isinstance(data, ResponseParameter):
+ datas.add(data)
+ return datas
+
+ def set_datas(self, datas):
+ """
+ Set or replace all data elements. The given elements must be in a
+ list or set of ResponseParameter elements
+
+ Arguments:
+ datas -- A series of data element names.
+ """
+ self.del_datas()
+ for data in datas:
+ self.add_data(name=data['name'])
+
+ def del_datas(self):
+ """Remove all ResponseParameter elements."""
+ self._datas = set()
+ datas = [i for i in self.iterables if isinstance(i, ResponseParameter)]
+ for data in datas:
+ self.xml.remove(data.xml)
+ self.iterables.remove(data)
+
+
+class Error(ElementBase):
+ namespace = 'urn:xmpp:iot:control'
+ name = 'error'
+ plugin_attrib = name
+ interfaces = set(['var','text'])
+
+ def get_text(self):
+ """Return then contents inside the XML tag."""
+ return self.xml.text
+
+ def set_text(self, value):
+ """Set then contents inside the XML tag.
+
+ Arguments:
+ value -- string
+ """
+
+ self.xml.text = value
+ return self
+
+ def del_text(self):
+ """Remove the contents inside the XML tag."""
+ self.xml.text = ""
+ return self
+
+class ResponseParameter(ElementBase):
+ """
+ Parameter element in ControlSetResponse.
+ """
+ namespace = 'urn:xmpp:iot:control'
+ name = 'parameter'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+
+class BaseParameter(ElementBase):
+ """
+ Parameter element in SetCommand. This is a base class,
+ all instances of parameters added to SetCommand must be of types:
+ BooleanParameter
+ ColorParameter
+ StringParameter
+ DateParameter
+ DateTimeParameter
+ DoubleParameter
+ DurationParameter
+ IntParameter
+ LongParameter
+ TimeParameter
+ """
+ namespace = 'urn:xmpp:iot:control'
+ name = 'baseParameter'
+ plugin_attrib = name
+ interfaces = set(['name','value'])
+
+ def _get_typename(self):
+ return self.name
+
+
+class BooleanParameter(BaseParameter):
+ """
+ Field data of type boolean.
+ Note that the value is expressed as a string.
+ """
+ name = 'boolean'
+ plugin_attrib = name
+
+class ColorParameter(BaseParameter):
+ """
+ Field data of type color.
+ Note that the value is expressed as a string.
+ """
+ name = 'color'
+ plugin_attrib = name
+
+class StringParameter(BaseParameter):
+ """
+ Field data of type string.
+ """
+ name = 'string'
+ plugin_attrib = name
+
+class DateParameter(BaseParameter):
+ """
+ Field data of type date.
+ Note that the value is expressed as a string.
+ """
+ name = 'date'
+ plugin_attrib = name
+
+class DateTimeParameter(BaseParameter):
+ """
+ Field data of type dateTime.
+ Note that the value is expressed as a string.
+ """
+ name = 'dateTime'
+ plugin_attrib = name
+
+class DoubleParameter(BaseParameter):
+ """
+ Field data of type double.
+ Note that the value is expressed as a string.
+ """
+ name = 'double'
+ plugin_attrib = name
+
+class DurationParameter(BaseParameter):
+ """
+ Field data of type duration.
+ Note that the value is expressed as a string.
+ """
+ name = 'duration'
+ plugin_attrib = name
+
+class IntParameter(BaseParameter):
+ """
+ Field data of type int.
+ Note that the value is expressed as a string.
+ """
+ name = 'int'
+ plugin_attrib = name
+
+class LongParameter(BaseParameter):
+ """
+ Field data of type long (64-bit int).
+ Note that the value is expressed as a string.
+ """
+ name = 'long'
+ plugin_attrib = name
+
+class TimeParameter(BaseParameter):
+ """
+ Field data of type time.
+ Note that the value is expressed as a string.
+ """
+ name = 'time'
+ plugin_attrib = name
+
+register_stanza_plugin(Iq, ControlSet)
+register_stanza_plugin(Message, ControlSet)
+
+register_stanza_plugin(ControlSet, RequestNode, iterable=True)
+
+register_stanza_plugin(ControlSet, BooleanParameter, iterable=True)
+register_stanza_plugin(ControlSet, ColorParameter, iterable=True)
+register_stanza_plugin(ControlSet, StringParameter, iterable=True)
+register_stanza_plugin(ControlSet, DateParameter, iterable=True)
+register_stanza_plugin(ControlSet, DateTimeParameter, iterable=True)
+register_stanza_plugin(ControlSet, DoubleParameter, iterable=True)
+register_stanza_plugin(ControlSet, DurationParameter, iterable=True)
+register_stanza_plugin(ControlSet, IntParameter, iterable=True)
+register_stanza_plugin(ControlSet, LongParameter, iterable=True)
+register_stanza_plugin(ControlSet, TimeParameter, iterable=True)
+
+register_stanza_plugin(Iq, ControlSetResponse)
+register_stanza_plugin(ControlSetResponse, Error)
+register_stanza_plugin(ControlSetResponse, RequestNode, iterable=True)
+register_stanza_plugin(ControlSetResponse, ResponseParameter, iterable=True)
+
diff --git a/sleekxmpp/roster/__init__.py b/sleekxmpp/roster/__init__.py
index 4335d367..18b380c9 100644
--- a/sleekxmpp/roster/__init__.py
+++ b/sleekxmpp/roster/__init__.py
@@ -6,7 +6,6 @@
See the file LICENSE for copying permission.
"""
-from sleekxmpp.xmlstream import JID
from sleekxmpp.roster.item import RosterItem
from sleekxmpp.roster.single import RosterNode
from sleekxmpp.roster.multi import Roster
diff --git a/sleekxmpp/roster/single.py b/sleekxmpp/roster/single.py
index f080ae8a..e9ce4f21 100644
--- a/sleekxmpp/roster/single.py
+++ b/sleekxmpp/roster/single.py
@@ -237,8 +237,7 @@ class RosterNode(object):
if not self.xmpp.is_component:
return self.update(jid, subscription='remove')
- def update(self, jid, name=None, subscription=None, groups=[],
- block=True, timeout=None, callback=None):
+ def update(self, jid, name=None, subscription=None, groups=None, block=True, timeout=None, callback=None):
"""
Update a JID's subscription information.
@@ -258,6 +257,9 @@ class RosterNode(object):
Will be executed when the roster is received.
Implies block=False.
"""
+ if not groups:
+ groups = []
+
self[jid]['name'] = name
self[jid]['groups'] = groups
self[jid].save()
diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py
index d21a74e1..c43178f2 100644
--- a/sleekxmpp/stanza/htmlim.py
+++ b/sleekxmpp/stanza/htmlim.py
@@ -7,78 +7,13 @@
"""
from sleekxmpp.stanza import Message
-from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
-
-
-class HTMLIM(ElementBase):
-
- """
- XEP-0071: XHTML-IM defines a method for embedding XHTML content
- within a <message> stanza so that lightweight markup can be used
- to format the message contents and to create links.
-
- Only a subset of XHTML is recommended for use with XHTML-IM.
- See the full spec at 'http://xmpp.org/extensions/xep-0071.html'
- for more information.
-
- Example stanza:
- <message to="user@example.com">
- <body>Non-html message content.</body>
- <html xmlns="http://jabber.org/protocol/xhtml-im">
- <body xmlns="http://www.w3.org/1999/xhtml">
- <p><b>HTML!</b></p>
- </body>
- </html>
- </message>
-
- Stanza Interface:
- body -- The contents of the HTML body tag.
-
- Methods:
- setup -- Overrides ElementBase.setup.
- get_body -- Return the HTML body contents.
- set_body -- Set the HTML body contents.
- del_body -- Remove the HTML body contents.
- """
-
- namespace = 'http://jabber.org/protocol/xhtml-im'
- name = 'html'
- interfaces = set(('body',))
- plugin_attrib = name
-
- def set_body(self, html):
- """
- Set the contents of the HTML body.
-
- Arguments:
- html -- Either a string or XML object. If the top level
- element is not <body> with a namespace of
- 'http://www.w3.org/1999/xhtml', it will be wrapped.
- """
- if isinstance(html, str):
- html = ET.XML(html)
- if html.tag != '{http://www.w3.org/1999/xhtml}body':
- body = ET.Element('{http://www.w3.org/1999/xhtml}body')
- body.append(html)
- self.xml.append(body)
- else:
- self.xml.append(html)
-
- def get_body(self):
- """Return the contents of the HTML body."""
- html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
- if html is None:
- return ''
- return html
-
- def del_body(self):
- """Remove the HTML body contents."""
- if self.parent is not None:
- self.parent().xml.remove(self.xml)
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0071 import XHTML_IM as HTMLIM
register_stanza_plugin(Message, HTMLIM)
+
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
HTMLIM.setBody = HTMLIM.set_body
diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py
index 71c0444d..088de4c0 100644
--- a/sleekxmpp/stanza/iq.py
+++ b/sleekxmpp/stanza/iq.py
@@ -9,7 +9,7 @@
from sleekxmpp.stanza.rootstanza import RootStanza
from sleekxmpp.xmlstream import StanzaBase, ET
from sleekxmpp.xmlstream.handler import Waiter, Callback
-from sleekxmpp.xmlstream.matcher import MatcherId
+from sleekxmpp.xmlstream.matcher import MatchIDSender, MatcherId
from sleekxmpp.exceptions import IqTimeout, IqError
@@ -115,9 +115,13 @@ class Iq(RootStanza):
"""
query = self.xml.find("{%s}query" % value)
if query is None and value:
- self.clear()
- query = ET.Element("{%s}query" % value)
- self.xml.append(query)
+ plugin = self.plugin_tag_map.get('{%s}query' % value, None)
+ if plugin:
+ self.enable(plugin.plugin_attrib)
+ else:
+ self.clear()
+ query = ET.Element("{%s}query" % value)
+ self.xml.append(query)
return self
def get_query(self):
@@ -182,36 +186,46 @@ class Iq(RootStanza):
the stanza immediately. Used during stream
initialization. Defaults to False.
timeout_callback -- Optional reference to a stream handler function.
- Will be executed when the timeout expires before a
- response has been received with the originally-sent IQ
+ Will be executed when the timeout expires before a
+ response has been received with the originally-sent IQ
stanza. Only called if there is a callback parameter
(and therefore are in async mode).
"""
if timeout is None:
timeout = self.stream.response_timeout
+
+ if self.stream.session_bind_event.is_set():
+ matcher = MatchIDSender({
+ 'id': self['id'],
+ 'self': self.stream.boundjid,
+ 'peer': self['to']
+ })
+ else:
+ matcher = MatcherId(self['id'])
+
if callback is not None and self['type'] in ('get', 'set'):
handler_name = 'IqCallback_%s' % self['id']
if timeout_callback:
self.callback = callback
self.timeout_callback = timeout_callback
- self.stream.schedule('IqTimeout_%s' % self['id'],
- timeout,
- self._fire_timeout,
- repeat=False)
+ self.stream.schedule('IqTimeout_%s' % self['id'],
+ timeout,
+ self._fire_timeout,
+ repeat=False)
handler = Callback(handler_name,
- MatcherId(self['id']),
+ matcher,
self._handle_result,
once=True)
else:
handler = Callback(handler_name,
- MatcherId(self['id']),
+ matcher,
callback,
once=True)
self.stream.register_handler(handler)
StanzaBase.send(self, now=now)
return handler_name
elif block and self['type'] in ('get', 'set'):
- waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
+ waitfor = Waiter('IqWait_%s' % self['id'], matcher)
self.stream.register_handler(waitfor)
StanzaBase.send(self, now=now)
result = waitfor.wait(timeout)
diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py
index a7c2b218..52b807e5 100644
--- a/sleekxmpp/stanza/rootstanza.py
+++ b/sleekxmpp/stanza/rootstanza.py
@@ -60,7 +60,9 @@ class RootStanza(StanzaBase):
self.send()
elif isinstance(e, XMPPError):
# We raised this deliberately
+ keep_id = self['id']
self.reply(clear=e.clear)
+ self['id'] = keep_id
self['error']['condition'] = e.condition
self['error']['text'] = e.text
self['error']['type'] = e.etype
@@ -72,7 +74,9 @@ class RootStanza(StanzaBase):
self.send()
else:
# We probably didn't raise this on purpose, so send an error stanza
+ keep_id = self['id']
self.reply()
+ self['id'] = keep_id
self['error']['condition'] = 'undefined-condition'
self['error']['text'] = "SleekXMPP got into trouble."
self['error']['type'] = 'cancel'
diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py
index 901c3a56..e26f99ce 100644
--- a/sleekxmpp/test/sleektest.py
+++ b/sleekxmpp/test/sleektest.py
@@ -9,16 +9,14 @@
import unittest
from xml.parsers.expat import ExpatError
-import sleekxmpp
from sleekxmpp import ClientXMPP, ComponentXMPP
from sleekxmpp.util import Queue
from sleekxmpp.stanza import Message, Iq, Presence
from sleekxmpp.test import TestSocket, TestLiveSocket
-from sleekxmpp.exceptions import XMPPError, IqTimeout, IqError
-from sleekxmpp.xmlstream import ET, register_stanza_plugin
-from sleekxmpp.xmlstream import ElementBase, StanzaBase
+from sleekxmpp.xmlstream import ET
+from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.xmlstream.tostring import tostring
-from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId
+from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId, MatchIDSender
from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath
@@ -214,6 +212,7 @@ class SleekTest(unittest.TestCase):
matchers = {'stanzapath': StanzaPath,
'xpath': MatchXPath,
'mask': MatchXMLMask,
+ 'idsender': MatchIDSender,
'id': MatcherId}
Matcher = matchers.get(method, None)
if Matcher is None:
@@ -289,11 +288,8 @@ class SleekTest(unittest.TestCase):
if self.xmpp:
self.xmpp.socket.disconnect_error()
- def stream_start(self, mode='client', skip=True, header=None,
- socket='mock', jid='tester@localhost',
- password='test', server='localhost',
- port=5222, sasl_mech=None,
- plugins=None, plugin_config={}):
+ def stream_start(self, mode='client', skip=True, header=None, socket='mock', jid='tester@localhost',
+ password='test', server='localhost', port=5222, sasl_mech=None, plugins=None, plugin_config=None):
"""
Initialize an XMPP client or component using a dummy XML stream.
@@ -316,6 +312,9 @@ class SleekTest(unittest.TestCase):
plugins -- List of plugins to register. By default, all plugins
are loaded.
"""
+ if not plugin_config:
+ plugin_config = {}
+
if mode == 'client':
self.xmpp = ClientXMPP(jid, password,
sasl_mech=sasl_mech,
@@ -377,6 +376,7 @@ class SleekTest(unittest.TestCase):
if skip:
if socket != 'live':
# Mark send queue as usable
+ self.xmpp.session_bind_event.set()
self.xmpp.session_started_event.set()
# Clear startup stanzas
self.xmpp.socket.next_sent(timeout=1)
@@ -425,8 +425,7 @@ class SleekTest(unittest.TestCase):
parts.append('xmlns="%s"' % default_ns)
return header % ' '.join(parts)
- def recv(self, data, defaults=[], method='exact',
- use_values=True, timeout=1):
+ def recv(self, data, defaults=None, method='exact', use_values=True, timeout=1):
"""
Pass data to the dummy XMPP client as if it came from an XMPP server.
@@ -447,6 +446,9 @@ class SleekTest(unittest.TestCase):
timeout -- Time to wait in seconds for data to be received by
a live connection.
"""
+ if not defaults:
+ defaults = []
+
if self.xmpp.socket.is_live:
# we are working with a live connection, so we should
# verify what has been received instead of simulating
diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py
index 93f26312..e751a448 100644
--- a/sleekxmpp/thirdparty/mini_dateutil.py
+++ b/sleekxmpp/thirdparty/mini_dateutil.py
@@ -108,7 +108,7 @@ except:
def __init__(self, name, offset):
self._name = name
- self._offset = datetime.timedelta(seconds=offset)
+ self._offset = datetime.timedelta(minutes=offset)
def utcoffset(self, dt):
return self._offset
@@ -154,7 +154,7 @@ except:
absoff = offsetmins
name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60)
- inst = tzoffset(offsetmins, name)
+ inst = tzoffset(name,offsetmins)
_fixed_offset_tzs[offsetmins] = inst
return _fixed_offset_tzs[offsetmins]
diff --git a/sleekxmpp/thirdparty/socks.py b/sleekxmpp/thirdparty/socks.py
index a6c0d70e..34090d51 100644
--- a/sleekxmpp/thirdparty/socks.py
+++ b/sleekxmpp/thirdparty/socks.py
@@ -13,7 +13,7 @@ are permitted provided that the following conditions are met:
3. Neither the name of Dan Haim nor the names of his contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
-
+
THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
@@ -38,6 +38,8 @@ for use in PyLoris (http://pyloris.sourceforge.net/)
Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/)
mainly to merge bug fixes found in Sourceforge
+Minor modifications made by Eugene Dementiev (http://www.dementiev.eu/)
+
"""
import socket
@@ -212,12 +214,12 @@ class socksocket(socket.socket):
if self.__proxy[3]:
# Resolve remotely
ipaddr = None
- req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr
+ req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr.encode()
else:
# Resolve locally
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
req = req + chr(0x01).encode() + ipaddr
- req = req + struct.pack(">H", destport)
+ req += struct.pack(">H", destport)
self.sendall(req)
# Get the response
resp = self.__recvall(4)
@@ -286,7 +288,7 @@ class socksocket(socket.socket):
# The username parameter is considered userid for SOCKS4
if self.__proxy[4] != None:
req = req + self.__proxy[4]
- req = req + chr(0x00).encode()
+ req += chr(0x00).encode()
# DNS name if remote resolving is required
# NOTE: This is actually an extension to the SOCKS4 protocol
# called SOCKS4A and may not be supported in all cases.
@@ -327,7 +329,10 @@ class socksocket(socket.socket):
# We read the response until we get the string "\r\n\r\n"
resp = self.recv(1)
while resp.find("\r\n\r\n".encode()) == -1:
- resp = resp + self.recv(1)
+ recv = self.recv(1)
+ if not recv:
+ raise GeneralProxyError((1, _generalerrors[1]))
+ resp = resp + recv
# We just need the first line to check if the connection
# was successful
statusline = resp.splitlines()[0].split(" ".encode(), 2)
diff --git a/sleekxmpp/thirdparty/statemachine.py b/sleekxmpp/thirdparty/statemachine.py
index 4b5ecd6b..6c504dce 100644
--- a/sleekxmpp/thirdparty/statemachine.py
+++ b/sleekxmpp/thirdparty/statemachine.py
@@ -15,7 +15,8 @@ log = logging.getLogger(__name__)
class StateMachine(object):
- def __init__(self, states=[]):
+ def __init__(self, states=None):
+ if not states: states = []
self.lock = threading.Condition()
self.__states = []
self.addStates(states)
@@ -33,7 +34,7 @@ class StateMachine(object):
self.lock.release()
- def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}):
+ def transition(self, from_state, to_state, wait=0.0, func=None, args=None, kwargs=None):
'''
Transition from the given `from_state` to the given `to_state`.
This method will return `True` if the state machine is now in `to_state`. It
@@ -64,15 +65,23 @@ class StateMachine(object):
values for `args` and `kwargs` are provided, they are expanded and passed like so:
`func( *args, **kwargs )`.
'''
+ if not args:
+ args = []
+ if not kwargs:
+ kwargs = {}
return self.transition_any((from_state,), to_state, wait=wait,
func=func, args=args, kwargs=kwargs)
- def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}):
+ def transition_any(self, from_states, to_state, wait=0.0, func=None, args=None, kwargs=None):
'''
Transition from any of the given `from_states` to the given `to_state`.
'''
+ if not args:
+ args = []
+ if not kwargs:
+ kwargs = {}
if not isinstance(from_states, (tuple, list, set)):
raise ValueError("from_states should be a list, tuple, or set")
diff --git a/sleekxmpp/util/__init__.py b/sleekxmpp/util/__init__.py
index 1e4af02d..47a935af 100644
--- a/sleekxmpp/util/__init__.py
+++ b/sleekxmpp/util/__init__.py
@@ -11,21 +11,38 @@
from sleekxmpp.util.misc_ops import bytes, unicode, hashes, hash, \
- num_to_bytes, bytes_to_num, quote, XOR
+ num_to_bytes, bytes_to_num, quote, \
+ XOR, safedict
# =====================================================================
# Standardize import of Queue class:
import sys
-if 'gevent' in sys.modules:
+
+def _gevent_threads_enabled():
+ if not 'gevent' in sys.modules:
+ return False
+ try:
+ from gevent import thread as green_thread
+ thread = __import__('thread')
+ return thread.LockType is green_thread.LockType
+ except ImportError:
+ return False
+
+if _gevent_threads_enabled():
import gevent.queue as queue
- Queue = queue.JoinableQueue
+ _queue = queue.JoinableQueue
else:
try:
import queue
except ImportError:
import Queue as queue
- Queue = queue.Queue
+ _queue = queue.Queue
+class Queue(_queue):
+ def put(self, item, block=True, timeout=None):
+ if _queue.full(self):
+ _queue.get(self)
+ return _queue.put(self, item, block, timeout)
QueueEmpty = queue.Empty
diff --git a/sleekxmpp/util/misc_ops.py b/sleekxmpp/util/misc_ops.py
index 3b246625..18c919a8 100644
--- a/sleekxmpp/util/misc_ops.py
+++ b/sleekxmpp/util/misc_ops.py
@@ -8,7 +8,10 @@ def unicode(text):
text = text.decode('utf-8')
import __builtin__
return __builtin__.unicode(text)
- return str(text)
+ elif not isinstance(text, str):
+ return text.decode('utf-8')
+ else:
+ return text
def bytes(text):
@@ -126,6 +129,7 @@ def hashes():
hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')]
return t + hashes
+
def setdefaultencoding(encoding):
"""
Set the current default string encoding used by the Unicode implementation.
@@ -148,4 +152,14 @@ def setdefaultencoding(encoding):
if func is None:
raise RuntimeError("Could not find setdefaultencoding")
sys.setdefaultencoding = func
- return func(encoding) \ No newline at end of file
+ return func(encoding)
+
+
+def safedict(data):
+ if sys.version_info < (2, 7):
+ safe = {}
+ for key in data:
+ safe[key.encode('utf8')] = data[key]
+ return safe
+ else:
+ return data
diff --git a/sleekxmpp/util/sasl/__init__.py b/sleekxmpp/util/sasl/__init__.py
index d054ce09..2d344e9b 100644
--- a/sleekxmpp/util/sasl/__init__.py
+++ b/sleekxmpp/util/sasl/__init__.py
@@ -7,7 +7,9 @@
Part of SleekXMPP: The Sleek XMPP Library
- :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :copryight: (c) 2004-2013 David Alan Cridland
+ :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout
+
:license: MIT, see LICENSE for more details
"""
diff --git a/sleekxmpp/util/sasl/client.py b/sleekxmpp/util/sasl/client.py
index 0bfb63f8..fd685547 100644
--- a/sleekxmpp/util/sasl/client.py
+++ b/sleekxmpp/util/sasl/client.py
@@ -7,7 +7,9 @@
Part of SleekXMPP: The Sleek XMPP Library
- :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :copryight: (c) 2004-2013 David Alan Cridland
+ :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout
+
:license: MIT, see LICENSE for more details
"""
diff --git a/sleekxmpp/util/sasl/mechanisms.py b/sleekxmpp/util/sasl/mechanisms.py
index 55ae44dd..7a7ebf7b 100644
--- a/sleekxmpp/util/sasl/mechanisms.py
+++ b/sleekxmpp/util/sasl/mechanisms.py
@@ -9,7 +9,9 @@
Part of SleekXMPP: The Sleek XMPP Library
- :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout
+ :copryight: (c) 2004-2013 David Alan Cridland
+ :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout
+
:license: MIT, see LICENSE for more details
"""
@@ -21,7 +23,8 @@ from base64 import b64encode, b64decode
from sleekxmpp.util import bytes, hash, XOR, quote, num_to_bytes
from sleekxmpp.util.sasl.client import sasl_mech, Mech, \
- SASLCancelled, SASLFailed
+ SASLCancelled, SASLFailed, \
+ SASLMutualAuthFailed
@sasl_mech(0)
@@ -86,7 +89,7 @@ class EXTERNAL(Mech):
return self.credentials['authzid']
-@sasl_mech(30)
+@sasl_mech(31)
class X_FACEBOOK_PLATFORM(Mech):
name = 'X-FACEBOOK-PLATFORM'
@@ -108,7 +111,7 @@ class X_FACEBOOK_PLATFORM(Mech):
b'api_key': self.credentials['api_key']
}
- resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()])
+ resp = '&'.join(['%s=%s' % (k.decode("utf-8"), v.decode("utf-8")) for k, v in resp_data.items()])
return bytes(resp)
return b''
@@ -220,17 +223,16 @@ class SCRAM(Mech):
return self.hash(text).digest()
def saslname(self, value):
- escaped = b''
- for char in bytes(value):
- if char == b',':
- escaped += b'=2C'
- elif char == b'=':
- escaped += b'=3D'
+ value = value.decode("utf-8")
+ escaped = []
+ for char in value:
+ if char == ',':
+ escaped += '=2C'
+ elif char == '=':
+ escaped += '=3D'
else:
- if isinstance(char, int):
- char = chr(char)
- escaped += bytes(char)
- return escaped
+ escaped += char
+ return "".join(escaped).encode("utf-8")
def parse(self, challenge):
items = {}
@@ -284,7 +286,9 @@ class SCRAM(Mech):
if nonce[:len(self.cnonce)] != self.cnonce:
raise SASLCancelled('Invalid nonce')
- cbind_data = self.credentials['channel_binding']
+ cbind_data = b''
+ if self.use_channel_binding:
+ cbind_data = self.credentials['channel_binding']
cbind_input = self.gs2_header + cbind_data
channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'')
@@ -467,7 +471,8 @@ class DIGEST(Mech):
'qop': self.qop,
'digest-uri': quote(self.digest_uri()),
'response': self.response(b'AUTHENTICATE'),
- 'maxbuf': self.maxbuf
+ 'maxbuf': self.maxbuf,
+ 'charset': 'utf-8'
}
resp = b''
for key, value in data.items():
@@ -480,7 +485,7 @@ class DIGEST(Mech):
if self.cnonce and self.nonce and self.nonce_count and self.qop:
self.nonce_count += 1
return self.respond()
- return b''
+ return None
data = self.parse(challenge)
if 'rspauth' in data:
@@ -526,6 +531,9 @@ else:
result = kerberos.authGSSClientStep(self.gss, b64_challenge)
if result != kerberos.AUTH_GSS_CONTINUE:
self.step = 1
+ elif not challenge:
+ kerberos.authGSSClientClean(self.gss)
+ return b''
elif self.step == 1:
username = self.credentials['username']
@@ -535,7 +543,7 @@ else:
resp = kerberos.authGSSClientResponse(self.gss)
except kerberos.GSSError as e:
- raise SASLCancelled('Kerberos error: %s' % e.message)
+ raise SASLCancelled('Kerberos error: %s' % e)
if not resp:
return b''
else:
diff --git a/sleekxmpp/util/stringprep_profiles.py b/sleekxmpp/util/stringprep_profiles.py
index ad89d4cc..84326bc3 100644
--- a/sleekxmpp/util/stringprep_profiles.py
+++ b/sleekxmpp/util/stringprep_profiles.py
@@ -16,9 +16,8 @@
from __future__ import unicode_literals
-import sys
import stringprep
-import unicodedata
+from unicodedata import ucd_3_2_0 as unicodedata
from sleekxmpp.util import unicode
diff --git a/sleekxmpp/version.py b/sleekxmpp/version.py
index 010f425b..acea9334 100644
--- a/sleekxmpp/version.py
+++ b/sleekxmpp/version.py
@@ -9,5 +9,5 @@
# We don't want to have to import the entire library
# just to get the version info for setup.py
-__version__ = '1.1.10'
-__version_info__ = (1, 1, 10, '', 0)
+__version__ = '1.4.0'
+__version_info__ = (1, 4, 0, '', 0)
diff --git a/sleekxmpp/xmlstream/cert.py b/sleekxmpp/xmlstream/cert.py
index fa12f794..71146f36 100644
--- a/sleekxmpp/xmlstream/cert.py
+++ b/sleekxmpp/xmlstream/cert.py
@@ -1,6 +1,10 @@
import logging
from datetime import datetime, timedelta
+# Make a call to strptime before starting threads to
+# prevent thread safety issues.
+datetime.strptime('1970-01-01 12:00:00', "%Y-%m-%d %H:%M:%S")
+
try:
from pyasn1.codec.der import decoder, encoder
diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py
index d4537998..53b83bc7 100644
--- a/sleekxmpp/xmlstream/filesocket.py
+++ b/sleekxmpp/xmlstream/filesocket.py
@@ -13,6 +13,7 @@
"""
from socket import _fileobject
+import errno
import socket
@@ -29,7 +30,13 @@ class FileSocket(_fileobject):
"""Read data from the socket as if it were a file."""
if self._sock is None:
return None
- data = self._sock.recv(size)
+ while True:
+ try:
+ data = self._sock.recv(size)
+ break
+ except socket.error as serr:
+ if serr.errno != errno.EINTR:
+ raise
if data is not None:
return data
diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py
index 1038d1bd..aa74c434 100644
--- a/sleekxmpp/xmlstream/matcher/__init__.py
+++ b/sleekxmpp/xmlstream/matcher/__init__.py
@@ -7,6 +7,7 @@
"""
from sleekxmpp.xmlstream.matcher.id import MatcherId
+from sleekxmpp.xmlstream.matcher.idsender import MatchIDSender
from sleekxmpp.xmlstream.matcher.many import MatchMany
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask
diff --git a/sleekxmpp/xmlstream/matcher/idsender.py b/sleekxmpp/xmlstream/matcher/idsender.py
new file mode 100644
index 00000000..5c2c1f51
--- /dev/null
+++ b/sleekxmpp/xmlstream/matcher/idsender.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+ sleekxmpp.xmlstream.matcher.id
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Part of SleekXMPP: The Sleek XMPP Library
+
+ :copyright: (c) 2011 Nathanael C. Fritz
+ :license: MIT, see LICENSE for more details
+"""
+
+from sleekxmpp.xmlstream.matcher.base import MatcherBase
+
+
+class MatchIDSender(MatcherBase):
+
+ """
+ The IDSender matcher selects stanzas that have the same stanza 'id'
+ interface value as the desired ID, and that the 'from' value is one
+ of a set of approved entities that can respond to a request.
+ """
+
+ def match(self, xml):
+ """Compare the given stanza's ``'id'`` attribute to the stored
+ ``id`` value, and verify the sender's JID.
+
+ :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase`
+ stanza to compare against.
+ """
+
+ selfjid = self._criteria['self']
+ peerjid = self._criteria['peer']
+
+ allowed = {}
+ allowed[''] = True
+ allowed[selfjid.bare] = True
+ allowed[selfjid.host] = True
+ allowed[peerjid.full] = True
+ allowed[peerjid.bare] = True
+ allowed[peerjid.host] = True
+
+ _from = xml['from']
+
+ try:
+ return xml['id'] == self._criteria['id'] and allowed[_from]
+ except KeyError:
+ return False
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
index a0568f08..56f728e1 100644
--- a/sleekxmpp/xmlstream/matcher/xmlmask.py
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -14,12 +14,6 @@ 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
-
-
log = logging.getLogger(__name__)
@@ -39,19 +33,15 @@ class MatchXMLMask(MatcherBase):
:class:`~sleekxmpp.xmlstream.matcher.stanzapath.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.
-
:param criteria: Either an :class:`~xml.etree.ElementTree.Element` XML
object or XML string to use as a mask.
"""
- def __init__(self, criteria):
+ def __init__(self, criteria, default_ns='jabber:client'):
MatcherBase.__init__(self, criteria)
if isinstance(criteria, str):
self._criteria = ET.fromstring(self._criteria)
- self.default_ns = 'jabber:client'
+ self.default_ns = default_ns
def setDefaultNS(self, ns):
"""Set the default namespace to use during comparisons.
@@ -84,8 +74,6 @@ class MatchXMLMask(MatcherBase):
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
@@ -96,17 +84,10 @@ class MatchXMLMask(MatcherBase):
mask = ET.fromstring(mask)
except ExpatError:
log.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
+
+ 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 and \
@@ -122,37 +103,15 @@ class MatchXMLMask(MatcherBase):
# Recursively check subelements.
matched_elements = {}
for subelement in mask:
- if use_ns:
- matched = False
- for other in source.findall(subelement.tag):
- matched_elements[other] = False
- if self._mask_cmp(other, subelement, use_ns):
- if not matched_elements.get(other, False):
- matched_elements[other] = True
- matched = True
- if not matched:
- return False
- else:
- if not self._mask_cmp(self._get_child(source, subelement.tag),
- subelement, use_ns):
- return False
+ matched = False
+ for other in source.findall(subelement.tag):
+ matched_elements[other] = False
+ if self._mask_cmp(other, subelement, use_ns):
+ if not matched_elements.get(other, False):
+ matched_elements[other] = True
+ matched = True
+ if not matched:
+ 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.
-
- :param xml: The :class:`~xml.etree.ElementTree.Element` XML object
- to search for the given child tag.
- :param tag: The name of the subelement to find.
- """
- tag = tag.split('}')[-1]
- try:
- children = [c.tag.split('}')[-1] for c in xml]
- index = children.index(tag)
- except ValueError:
- return None
- return list(xml)[index]
diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py
index 3f03e68e..f3d28429 100644
--- a/sleekxmpp/xmlstream/matcher/xpath.py
+++ b/sleekxmpp/xmlstream/matcher/xpath.py
@@ -9,16 +9,10 @@
:license: MIT, see LICENSE for more details
"""
-from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.stanzabase import ET, fix_ns
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):
"""
@@ -38,6 +32,9 @@ class MatchXPath(MatcherBase):
expressions will be matched without using namespaces.
"""
+ def __init__(self, criteria):
+ self._criteria = fix_ns(criteria)
+
def match(self, xml):
"""
Compare a stanza's XML contents to an XPath expression.
@@ -59,28 +56,4 @@ class MatchXPath(MatcherBase):
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]
- try:
- index = children.index(tag)
- except ValueError:
- return False
- xml = list(xml)[index]
- return True
+ return x.find(self._criteria) is not None
diff --git a/sleekxmpp/xmlstream/resolver.py b/sleekxmpp/xmlstream/resolver.py
index 394daa64..188e5ac7 100644
--- a/sleekxmpp/xmlstream/resolver.py
+++ b/sleekxmpp/xmlstream/resolver.py
@@ -32,10 +32,10 @@ log = logging.getLogger(__name__)
#: cd dnspython
#: git checkout python3
#: python3 setup.py install
-USE_DNSPYTHON = False
+DNSPYTHON_AVAILABLE = False
try:
import dns.resolver
- USE_DNSPYTHON = True
+ DNSPYTHON_AVAILABLE = True
except ImportError as e:
log.debug("Could not find dnspython package. " + \
"Not all features will be available")
@@ -47,13 +47,13 @@ def default_resolver():
:returns: A :class:`dns.resolver.Resolver` object if dnspython
is available. Otherwise, ``None``.
"""
- if USE_DNSPYTHON:
+ if DNSPYTHON_AVAILABLE:
return dns.resolver.get_default_resolver()
return None
def resolve(host, port=None, service=None, proto='tcp',
- resolver=None, use_ipv6=True):
+ resolver=None, use_ipv6=True, use_dnspython=True):
"""Peform DNS resolution for a given hostname.
Resolution may perform SRV record lookups if a service and protocol
@@ -77,6 +77,9 @@ def resolve(host, port=None, service=None, proto='tcp',
:param use_ipv6: Optionally control the use of IPv6 in situations
where it is either not available, or performance
is degraded. Defaults to ``True``.
+ :param use_dnspython: Optionally control if dnspython is used to make
+ the DNS queries instead of the built-in DNS
+ library.
:type host: string
:type port: int
@@ -84,14 +87,22 @@ def resolve(host, port=None, service=None, proto='tcp',
:type proto: string
:type resolver: :class:`dns.resolver.Resolver`
:type use_ipv6: bool
+ :type use_dnspython: bool
:return: An iterable of IP address, port pairs in the order
dictated by SRV priorities and weights, if applicable.
"""
+
+ if not use_dnspython:
+ if DNSPYTHON_AVAILABLE:
+ log.debug("DNS: Not using dnspython, but dnspython is installed.")
+ else:
+ log.debug("DNS: Not using dnspython.")
+
if not use_ipv6:
log.debug("DNS: Use of IPv6 has been disabled.")
- if resolver is None and USE_DNSPYTHON:
+ if resolver is None and DNSPYTHON_AVAILABLE and use_dnspython:
resolver = dns.resolver.get_default_resolver()
# An IPv6 literal is allowed to be enclosed in square brackets, but
@@ -113,7 +124,7 @@ def resolve(host, port=None, service=None, proto='tcp',
if hasattr(socket, 'inet_pton'):
ipv6 = socket.inet_pton(socket.AF_INET6, host)
yield (host, host, port)
- except socket.error:
+ except (socket.error, ValueError):
pass
# If no service was provided, then we can just do A/AAAA lookups on the
@@ -122,7 +133,9 @@ def resolve(host, port=None, service=None, proto='tcp',
if not service:
hosts = [(host, port)]
else:
- hosts = get_SRV(host, port, service, proto, resolver=resolver)
+ hosts = get_SRV(host, port, service, proto,
+ resolver=resolver,
+ use_dnspython=use_dnspython)
for host, port in hosts:
results = []
@@ -131,16 +144,18 @@ def resolve(host, port=None, service=None, proto='tcp',
results.append((host, '::1', port))
results.append((host, '127.0.0.1', port))
if use_ipv6:
- for address in get_AAAA(host, resolver=resolver):
+ for address in get_AAAA(host, resolver=resolver,
+ use_dnspython=use_dnspython):
results.append((host, address, port))
- for address in get_A(host, resolver=resolver):
+ for address in get_A(host, resolver=resolver,
+ use_dnspython=use_dnspython):
results.append((host, address, port))
for host, address, port in results:
yield host, address, port
-def get_A(host, resolver=None):
+def get_A(host, resolver=None, use_dnspython=True):
"""Lookup DNS A records for a given host.
If ``resolver`` is not provided, or is ``None``, then resolution will
@@ -148,9 +163,13 @@ def get_A(host, resolver=None):
:param host: The hostname to resolve for A record IPv4 addresses.
:param resolver: Optional DNS resolver object to use for the query.
+ :param use_dnspython: Optionally control if dnspython is used to make
+ the DNS queries instead of the built-in DNS
+ library.
:type host: string
:type resolver: :class:`dns.resolver.Resolver` or ``None``
+ :type use_dnspython: bool
:return: A list of IPv4 literals.
"""
@@ -158,7 +177,7 @@ def get_A(host, resolver=None):
# If not using dnspython, attempt lookup using the OS level
# getaddrinfo() method.
- if resolver is None:
+ if resolver is None or not use_dnspython:
try:
recs = socket.getaddrinfo(host, None, socket.AF_INET,
socket.SOCK_STREAM)
@@ -183,7 +202,7 @@ def get_A(host, resolver=None):
return []
-def get_AAAA(host, resolver=None):
+def get_AAAA(host, resolver=None, use_dnspython=True):
"""Lookup DNS AAAA records for a given host.
If ``resolver`` is not provided, or is ``None``, then resolution will
@@ -191,9 +210,13 @@ def get_AAAA(host, resolver=None):
:param host: The hostname to resolve for AAAA record IPv6 addresses.
:param resolver: Optional DNS resolver object to use for the query.
+ :param use_dnspython: Optionally control if dnspython is used to make
+ the DNS queries instead of the built-in DNS
+ library.
:type host: string
:type resolver: :class:`dns.resolver.Resolver` or ``None``
+ :type use_dnspython: bool
:return: A list of IPv6 literals.
"""
@@ -201,12 +224,15 @@ def get_AAAA(host, resolver=None):
# If not using dnspython, attempt lookup using the OS level
# getaddrinfo() method.
- if resolver is None:
+ if resolver is None or not use_dnspython:
+ if not socket.has_ipv6:
+ log.debug("Unable to query %s for AAAA records: IPv6 is not supported", host)
+ return []
try:
recs = socket.getaddrinfo(host, None, socket.AF_INET6,
socket.SOCK_STREAM)
return [rec[4][0] for rec in recs]
- except socket.gaierror:
+ except (OSError, socket.gaierror):
log.debug("DNS: Error retreiving AAAA address " + \
"info for %s." % host)
return []
@@ -227,7 +253,7 @@ def get_AAAA(host, resolver=None):
return []
-def get_SRV(host, port, service, proto='tcp', resolver=None):
+def get_SRV(host, port, service, proto='tcp', resolver=None, use_dnspython=True):
"""Perform SRV record resolution for a given host.
.. note::
@@ -253,7 +279,7 @@ def get_SRV(host, port, service, proto='tcp', resolver=None):
:return: A list of hostname, port pairs in the order dictacted
by SRV priorities and weights.
"""
- if resolver is None:
+ if resolver is None or not use_dnspython:
log.warning("DNS: dnspython not found. Can not use SRV lookup.")
return [(host, port)]
diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py
index b3e50983..e6fae37a 100644
--- a/sleekxmpp/xmlstream/scheduler.py
+++ b/sleekxmpp/xmlstream/scheduler.py
@@ -20,6 +20,11 @@ import itertools
from sleekxmpp.util import Queue, QueueEmpty
+#: The time in seconds to wait for events from the event queue, and also the
+#: time between checks for the process stop signal.
+WAIT_TIMEOUT = 1.0
+
+
log = logging.getLogger(__name__)
@@ -76,7 +81,7 @@ class Task(object):
"""
if self.qpointer is not None:
self.qpointer.put(('schedule', self.callback,
- self.args, self.name))
+ self.args, self.kwargs, self.name))
else:
self.callback(*self.args, **self.kwargs)
self.reset()
@@ -120,6 +125,10 @@ class Scheduler(object):
#: Lock for accessing the task queue.
self.schedule_lock = threading.RLock()
+ #: The time in seconds to wait for events from the event queue,
+ #: and also the time between checks for the process stop signal.
+ self.wait_timeout = WAIT_TIMEOUT
+
def process(self, threaded=True, daemon=False):
"""Begin accepting and processing scheduled tasks.
@@ -139,24 +148,25 @@ class Scheduler(object):
self.run = True
try:
while self.run and not self.stop.is_set():
- wait = 0.1
updated = False
if self.schedule:
wait = self.schedule[0].next - time.time()
+ else:
+ wait = self.wait_timeout
try:
if wait <= 0.0:
newtask = self.addq.get(False)
else:
- if wait >= 3.0:
- wait = 3.0
newtask = None
- elapsed = 0
- while not self.stop.is_set() and \
+ while self.run and \
+ not self.stop.is_set() and \
newtask is None and \
- elapsed < wait:
- newtask = self.addq.get(True, 0.1)
- elapsed += 0.1
- except QueueEmpty:
+ wait > 0:
+ try:
+ newtask = self.addq.get(True, min(wait, self.wait_timeout))
+ except QueueEmpty: # Nothing to add, nothing to do. Check run flags and continue waiting.
+ wait -= self.wait_timeout
+ except QueueEmpty: # Time to run some tasks, and no new tasks to add.
self.schedule_lock.acquire()
# select only those tasks which are to be executed now
relevant = itertools.takewhile(
@@ -174,11 +184,11 @@ class Scheduler(object):
# only need to resort tasks if a repeated task has
# been kept in the list.
updated = True
- else:
- updated = True
+ else: # Add new task
self.schedule_lock.acquire()
if newtask is not None:
self.schedule.append(newtask)
+ updated = True
finally:
if updated:
self.schedule.sort(key=lambda task: task.next)
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index 122d7eb4..11c8dd67 100644
--- a/sleekxmpp/xmlstream/stanzabase.py
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -3,7 +3,7 @@
sleekxmpp.xmlstream.stanzabase
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- This module implements a wrapper layer for XML objects
+ module implements a wrapper layer for XML objects
that allows them to be treated like dictionaries.
Part of SleekXMPP: The Sleek XMPP Library
@@ -19,6 +19,7 @@ import logging
import weakref
from xml.etree import cElementTree as ET
+from sleekxmpp.util import safedict
from sleekxmpp.xmlstream import JID
from sleekxmpp.xmlstream.tostring import tostring
from sleekxmpp.thirdparty import OrderedDict
@@ -141,7 +142,7 @@ def multifactory(stanza, plugin_attrib):
parent.loaded_plugins.remove(plugin_attrib)
try:
parent.xml.remove(self.xml)
- except:
+ except ValueError:
pass
else:
for stanza in list(res):
@@ -192,7 +193,7 @@ def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''):
for element in elements:
if element:
# Skip empty entry artifacts from splitting.
- if propagate_ns:
+ if propagate_ns and element[0] != '*':
tag = '{%s}%s' % (namespace, element)
else:
tag = element
@@ -565,7 +566,10 @@ class ElementBase(object):
values = {}
values['lang'] = self['lang']
for interface in self.interfaces:
- values[interface] = self[interface]
+ if isinstance(self[interface], JID):
+ values[interface] = self[interface].jid
+ else:
+ values[interface] = self[interface]
if interface in self.lang_interfaces:
values['%s|*' % interface] = self['%s|*' % interface]
for plugin, stanza in self.plugins.items():
@@ -596,31 +600,39 @@ class ElementBase(object):
iterable_interfaces = [p.plugin_attrib for \
p in self.plugin_iterables]
+ if 'lang' in values:
+ self['lang'] = values['lang']
+
+ if 'substanzas' in values:
+ # Remove existing substanzas
+ for stanza in self.iterables:
+ try:
+ self.xml.remove(stanza.xml)
+ except ValueError:
+ pass
+ self.iterables = []
+
+ # Add new substanzas
+ for subdict in values['substanzas']:
+ if '__childtag__' in subdict:
+ for subclass in self.plugin_iterables:
+ child_tag = "{%s}%s" % (subclass.namespace,
+ subclass.name)
+ if subdict['__childtag__'] == child_tag:
+ sub = subclass(parent=self)
+ sub.values = subdict
+ self.iterables.append(sub)
+
for interface, value in values.items():
full_interface = interface
interface_lang = ('%s|' % interface).split('|')
interface = interface_lang[0]
lang = interface_lang[1] or self.get_lang()
- if interface == 'substanzas':
- # Remove existing substanzas
- for stanza in self.iterables:
- self.xml.remove(stanza.xml)
- self.iterables = []
-
- # Add new substanzas
- for subdict in value:
- if '__childtag__' in subdict:
- for subclass in self.plugin_iterables:
- child_tag = "{%s}%s" % (subclass.namespace,
- subclass.name)
- if subdict['__childtag__'] == child_tag:
- sub = subclass(parent=self)
- sub.values = subdict
- self.iterables.append(sub)
- break
- elif interface == 'lang':
- self[interface] = value
+ if interface == 'lang':
+ continue
+ elif interface == 'substanzas':
+ continue
elif interface in self.interfaces:
self[full_interface] = value
elif interface in self.plugin_attrib_map:
@@ -668,6 +680,8 @@ class ElementBase(object):
if lang and attrib in self.lang_interfaces:
kwargs['lang'] = lang
+ kwargs = safedict(kwargs)
+
if attrib == 'substanzas':
return self.iterables
elif attrib in self.interfaces or attrib == 'lang':
@@ -744,6 +758,8 @@ class ElementBase(object):
if lang and attrib in self.lang_interfaces:
kwargs['lang'] = lang
+ kwargs = safedict(kwargs)
+
if attrib in self.interfaces or attrib == 'lang':
if value is not None:
set_method = "set_%s" % attrib.lower()
@@ -830,6 +846,8 @@ class ElementBase(object):
if lang and attrib in self.lang_interfaces:
kwargs['lang'] = lang
+ kwargs = safedict(kwargs)
+
if attrib in self.interfaces or attrib == 'lang':
del_method = "del_%s" % attrib.lower()
del_method2 = "del%s" % attrib.title()
@@ -866,7 +884,7 @@ class ElementBase(object):
self.loaded_plugins.remove(attrib)
try:
self.xml.remove(plugin.xml)
- except:
+ except ValueError:
pass
return self
diff --git a/sleekxmpp/xmlstream/tostring.py b/sleekxmpp/xmlstream/tostring.py
index 08d7ad02..c49abd3e 100644
--- a/sleekxmpp/xmlstream/tostring.py
+++ b/sleekxmpp/xmlstream/tostring.py
@@ -24,8 +24,8 @@ if sys.version_info < (3, 0):
XML_NS = 'http://www.w3.org/XML/1998/namespace'
-def tostring(xml=None, xmlns='', stream=None,
- outbuffer='', top_level=False, open_only=False):
+def tostring(xml=None, xmlns='', stream=None, outbuffer='',
+ top_level=False, open_only=False, namespaces=None):
"""Serialize an XML object to a Unicode string.
If an outer xmlns is provided using ``xmlns``, then the current element's
@@ -41,7 +41,8 @@ def tostring(xml=None, xmlns='', stream=None,
during recursive calls.
:param bool top_level: Indicates that the element is the outermost
element.
-
+ :param set namespaces: Track which namespaces are in active use so
+ that new ones can be declared when needed.
:type xml: :py:class:`~xml.etree.ElementTree.Element`
:type stream: :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream`
@@ -63,6 +64,7 @@ def tostring(xml=None, xmlns='', stream=None,
default_ns = ''
stream_ns = ''
use_cdata = False
+
if stream:
default_ns = stream.default_ns
stream_ns = stream.stream_ns
@@ -82,6 +84,7 @@ def tostring(xml=None, xmlns='', stream=None,
output.append(namespace)
# Output escaped attribute values.
+ new_namespaces = set()
for attrib, value in xml.attrib.items():
value = escape(value, use_cdata)
if '}' not in attrib:
@@ -89,14 +92,20 @@ def tostring(xml=None, xmlns='', stream=None,
else:
attrib_ns = attrib.split('}')[0][1:]
attrib = attrib.split('}')[1]
- if stream and attrib_ns in stream.namespace_map:
+ if attrib_ns == XML_NS:
+ output.append(' xml:%s="%s"' % (attrib, value))
+ elif stream and attrib_ns in stream.namespace_map:
mapped_ns = stream.namespace_map[attrib_ns]
if mapped_ns:
- output.append(' %s:%s="%s"' % (mapped_ns,
- attrib,
- value))
- elif attrib_ns == XML_NS:
- output.append(' xml:%s="%s"' % (attrib, value))
+ if namespaces is None:
+ namespaces = set()
+ if attrib_ns not in namespaces:
+ namespaces.add(attrib_ns)
+ new_namespaces.add(attrib_ns)
+ output.append(' xmlns:%s="%s"' % (
+ mapped_ns, attrib_ns))
+ output.append(' %s:%s="%s"' % (
+ mapped_ns, attrib, value))
if open_only:
# Only output the opening tag, regardless of content.
@@ -110,7 +119,8 @@ def tostring(xml=None, xmlns='', stream=None,
output.append(escape(xml.text, use_cdata))
if len(xml):
for child in xml:
- output.append(tostring(child, tag_xmlns, stream))
+ output.append(tostring(child, tag_xmlns, stream,
+ namespaces=namespaces))
output.append("</%s>" % tag_name)
elif xml.text:
# If we only have text content.
@@ -121,6 +131,11 @@ def tostring(xml=None, xmlns='', stream=None,
if xml.tail:
# If there is additional text after the element.
output.append(escape(xml.tail, use_cdata))
+ for ns in new_namespaces:
+ # Remove namespaces introduced in this context. This is necessary
+ # because the namespaces object continues to be shared with other
+ # contexts.
+ namespaces.remove(ns)
return ''.join(output)
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index bea6e88f..f9ec4947 100644
--- a/sleekxmpp/xmlstream/xmlstream.py
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -26,11 +26,12 @@ import time
import random
import weakref
import uuid
+import errno
from xml.parsers.expat import ExpatError
import sleekxmpp
-from sleekxmpp.util import Queue, QueueEmpty
+from sleekxmpp.util import Queue, QueueEmpty, safedict
from sleekxmpp.thirdparty.statemachine import StateMachine
from sleekxmpp.xmlstream import Scheduler, tostring, cert
from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET, ElementBase
@@ -49,7 +50,7 @@ RESPONSE_TIMEOUT = 30
#: The time in seconds to wait for events from the event queue, and also the
#: time between checks for the process stop signal.
-WAIT_TIMEOUT = 0.1
+WAIT_TIMEOUT = 1.0
#: The number of threads to use to handle XML stream events. This is not the
#: same as the number of custom event handling threads.
@@ -122,6 +123,11 @@ class XMLStream(object):
#: xmpp.ssl_version = ssl.PROTOCOL_SSLv23
self.ssl_version = ssl.PROTOCOL_TLSv1
+ #: The list of accepted ciphers, in OpenSSL Format.
+ #: It might be useful to override it for improved security
+ #: over the python defaults.
+ self.ciphers = None
+
#: Path to a file containing certificates for verifying the
#: server SSL certificate. A non-``None`` value will trigger
#: certificate checking.
@@ -218,6 +224,11 @@ class XMLStream(object):
#: If set to ``True``, attempt to use IPv6.
self.use_ipv6 = True
+ #: If set to ``True``, allow using the ``dnspython`` DNS library
+ #: if available. If set to ``False``, the builtin DNS resolver
+ #: will be used, even if ``dnspython`` is installed.
+ self.use_dnspython = True
+
#: Use CDATA for escaping instead of XML entities. Defaults
#: to ``False``.
self.use_cdata = False
@@ -280,7 +291,7 @@ class XMLStream(object):
self.event_queue = Queue()
#: A queue of string data to be sent over the stream.
- self.send_queue = Queue()
+ self.send_queue = Queue(maxsize=256)
self.send_queue_lock = threading.Lock()
self.send_lock = threading.RLock()
@@ -449,9 +460,11 @@ class XMLStream(object):
def _connect(self, reattempt=True):
self.scheduler.remove('Session timeout check')
- if self.reconnect_delay is None or not reattempt:
+ if self.reconnect_delay is None:
delay = 1.0
- else:
+ self.reconnect_delay = delay
+
+ if reattempt:
delay = min(self.reconnect_delay * 2, self.reconnect_max_delay)
delay = random.normalvariate(delay, delay * 0.1)
log.debug('Waiting %s seconds before connecting.', delay)
@@ -461,10 +474,10 @@ class XMLStream(object):
time.sleep(0.1)
elapsed += 0.1
except KeyboardInterrupt:
- self.stop.set()
+ self.set_stop()
return False
except SystemExit:
- self.stop.set()
+ self.set_stop()
return False
if self.default_domain:
@@ -507,12 +520,19 @@ class XMLStream(object):
else:
cert_policy = ssl.CERT_REQUIRED
- ssl_socket = ssl.wrap_socket(self.socket,
- certfile=self.certfile,
- keyfile=self.keyfile,
- ca_certs=self.ca_certs,
- cert_reqs=cert_policy,
- do_handshake_on_connect=False)
+ ssl_args = safedict({
+ 'certfile': self.certfile,
+ 'keyfile': self.keyfile,
+ 'ca_certs': self.ca_certs,
+ 'cert_reqs': cert_policy,
+ 'do_handshake_on_connect': False,
+ "ssl_version": self.ssl_version
+ })
+
+ if sys.version_info >= (2, 7):
+ ssl_args['ciphers'] = self.ciphers
+
+ ssl_socket = ssl.wrap_socket(self.socket, **ssl_args)
if hasattr(self.socket, 'socket'):
# We are using a testing socket, so preserve the top
@@ -550,7 +570,7 @@ class XMLStream(object):
cert.verify(self._expected_server_name, self._der_cert)
except cert.CertificateError as err:
if not self.event_handled('ssl_invalid_cert'):
- log.error(err.message)
+ log.error(err)
self.disconnect(send_close=False)
else:
self.event('ssl_invalid_cert',
@@ -559,8 +579,7 @@ class XMLStream(object):
self.set_socket(self.socket, ignore=True)
#this event is where you should set your application state
- self.event("connected", direct=True)
- self.reconnect_delay = 1.0
+ self.event('connected', direct=True)
return True
except (Socket.error, ssl.SSLError) as serr:
error_msg = "Could not connect to %s:%s. Socket Error #%s: %s"
@@ -600,7 +619,7 @@ class XMLStream(object):
headers = '\r\n'.join(headers) + '\r\n\r\n'
try:
- log.debug("Connecting to proxy: %s:%s", address)
+ log.debug("Connecting to proxy: %s:%s", *address)
self.socket.connect(address)
self.send_raw(headers, now=True)
resp = ''
@@ -611,6 +630,7 @@ class XMLStream(object):
lines = resp.split('\r\n')
if '200' not in lines[0]:
self.event('proxy_error', resp)
+ self.event('connection_failed', direct=True)
log.error('Proxy Error: %s', lines[0])
return False
@@ -706,7 +726,7 @@ class XMLStream(object):
self.stream_end_event.set()
if not self.auto_reconnect:
- self.stop.set()
+ self.set_stop()
if self._disconnect_wait_for_threads:
self._wait_for_threads()
@@ -718,12 +738,12 @@ class XMLStream(object):
self.event('socket_error', serr, direct=True)
finally:
#clear your application state
- self.event("disconnected", direct=True)
+ self.event('disconnected', direct=True)
return True
def abort(self):
self.session_started_event.clear()
- self.stop.set()
+ self.set_stop()
if self._disconnect_wait_for_threads:
self._wait_for_threads()
try:
@@ -818,19 +838,26 @@ class XMLStream(object):
to be restarted.
"""
log.info("Negotiating TLS")
- log.info("Using SSL version: %s", str(self.ssl_version))
+ ssl_versions = {3: 'TLS 1.0', 1: 'SSL 3', 2: 'SSL 2/3'}
+ log.info("Using SSL version: %s", ssl_versions[self.ssl_version])
if self.ca_certs is None:
cert_policy = ssl.CERT_NONE
else:
cert_policy = ssl.CERT_REQUIRED
- ssl_socket = ssl.wrap_socket(self.socket,
- certfile=self.certfile,
- keyfile=self.keyfile,
- ssl_version=self.ssl_version,
- do_handshake_on_connect=False,
- ca_certs=self.ca_certs,
- cert_reqs=cert_policy)
+ ssl_args = safedict({
+ 'certfile': self.certfile,
+ 'keyfile': self.keyfile,
+ 'ca_certs': self.ca_certs,
+ 'cert_reqs': cert_policy,
+ 'do_handshake_on_connect': False,
+ "ssl_version": self.ssl_version
+ })
+
+ if sys.version_info >= (2, 7):
+ ssl_args['ciphers'] = self.ciphers
+
+ ssl_socket = ssl.wrap_socket(self.socket, **ssl_args)
if hasattr(self.socket, 'socket'):
# We are using a testing socket, so preserve the top
@@ -859,7 +886,7 @@ class XMLStream(object):
cert.verify(self._expected_server_name, self._der_cert)
except cert.CertificateError as err:
if not self.event_handled('ssl_invalid_cert'):
- log.error(err.message)
+ log.error(err)
self.disconnect(self.auto_reconnect, send_close=False)
else:
self.event('ssl_invalid_cert', pem_cert, direct=True)
@@ -915,12 +942,13 @@ class XMLStream(object):
self.whitespace_keepalive_interval = 300
"""
- self.schedule('Whitespace Keepalive',
- self.whitespace_keepalive_interval,
- self.send_raw,
- args=(' ',),
- kwargs={'now': True},
- repeat=True)
+ if self.whitespace_keepalive:
+ self.schedule('Whitespace Keepalive',
+ self.whitespace_keepalive_interval,
+ self.send_raw,
+ args=(' ',),
+ kwargs={'now': True},
+ repeat=True)
def _remove_schedules(self, event):
"""Remove whitespace keepalive and certificate expiration schedules."""
@@ -1016,9 +1044,13 @@ class XMLStream(object):
# and handler classes here.
if name is None:
- name = 'add_handler_%s' % self.getNewId()
- self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer,
- once=disposable, instream=instream))
+ name = 'add_handler_%s' % self.new_id()
+ self.register_handler(
+ XMLCallback(name,
+ MatchXMLMask(mask, self.default_ns),
+ 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
@@ -1059,7 +1091,8 @@ class XMLStream(object):
return resolve(domain, port, service=self.dns_service,
resolver=resolver,
- use_ipv6=self.use_ipv6)
+ use_ipv6=self.use_ipv6,
+ use_dnspython=self.use_dnspython)
def pick_dns_answer(self, domain, port=None):
"""Pick a server and port from DNS answers.
@@ -1120,7 +1153,7 @@ class XMLStream(object):
"""
return len(self.__event_handlers.get(name, []))
- def event(self, name, data={}, direct=False):
+ def event(self, name, data=None, direct=False):
"""Manually trigger a custom event.
:param name: The name of the event to trigger.
@@ -1131,6 +1164,11 @@ class XMLStream(object):
event queue. All event handlers will run in the
same thread.
"""
+ if not data:
+ data = {}
+
+ log.debug("Event triggered: " + name)
+
handlers = self.__event_handlers.get(name, [])
for handler in handlers:
#TODO: Data should not be copied, but should be read only,
@@ -1302,6 +1340,9 @@ class XMLStream(object):
if not self.stop.is_set():
time.sleep(self.ssl_retry_delay)
tries += 1
+ except Socket.error as serr:
+ if serr.errno != errno.EINTR:
+ raise
if count > 1:
log.debug('SENT: %d chunks', count)
except (Socket.error, ssl.SSLError) as serr:
@@ -1316,12 +1357,12 @@ class XMLStream(object):
return True
def _start_thread(self, name, target, track=True):
- self.__active_threads.add(name)
self.__thread[name] = threading.Thread(name=name, target=target)
self.__thread[name].daemon = self._use_daemons
self.__thread[name].start()
if track:
+ self.__active_threads.add(name)
with self.__thread_cond:
self.__thread_count += 1
@@ -1350,6 +1391,13 @@ class XMLStream(object):
if self.__thread_count == 0:
self.__thread_cond.notify()
+ def set_stop(self):
+ self.stop.set()
+
+ # Unlock queues
+ self.event_queue.put(None)
+ self.send_queue.put(None)
+
def _wait_for_threads(self):
with self.__thread_cond:
if self.__thread_count != 0:
@@ -1493,6 +1541,10 @@ class XMLStream(object):
# as handshakes.
self.stream_end_event.clear()
self.start_stream_handler(root)
+
+ # We have a successful stream connection, so reset
+ # exponential backoff for new reconnect attempts.
+ self.reconnect_delay = 1.0
depth += 1
if event == b'end':
depth -= 1
@@ -1618,11 +1670,7 @@ class XMLStream(object):
log.debug("Loading event runner")
try:
while not self.stop.is_set():
- try:
- wait = self.wait_timeout
- event = self.event_queue.get(True, timeout=wait)
- except QueueEmpty:
- event = None
+ event = self.event_queue.get()
if event is None:
continue
@@ -1638,10 +1686,10 @@ class XMLStream(object):
log.exception(error_msg, handler.name)
orig.exception(e)
elif etype == 'schedule':
- name = args[1]
+ name = args[2]
try:
log.debug('Scheduled event: %s: %s', name, args[0])
- handler(*args[0])
+ handler(*args[0], **args[1])
except Exception as e:
log.exception('Error processing scheduled task')
self.exception(e)
@@ -1683,14 +1731,13 @@ class XMLStream(object):
while not self.stop.is_set():
while not self.stop.is_set() and \
not self.session_started_event.is_set():
- self.session_started_event.wait(timeout=0.1)
+ self.session_started_event.wait(timeout=0.1) # Wait for session start
if self.__failed_send_stanza is not None:
data = self.__failed_send_stanza
self.__failed_send_stanza = None
else:
- try:
- data = self.send_queue.get(True, 1)
- except QueueEmpty:
+ data = self.send_queue.get() # Wait for data to send
+ if data is None:
continue
log.debug("SEND: %s", data)
enc_data = data.encode('utf-8')
@@ -1717,6 +1764,9 @@ class XMLStream(object):
if not self.stop.is_set():
time.sleep(self.ssl_retry_delay)
tries += 1
+ except Socket.error as serr:
+ if serr.errno != errno.EINTR:
+ raise
if count > 1:
log.debug('SENT: %d chunks', count)
self.send_queue.task_done()