summaryrefslogtreecommitdiff
path: root/sleekxmpp/plugins
diff options
context:
space:
mode:
Diffstat (limited to 'sleekxmpp/plugins')
-rw-r--r--sleekxmpp/plugins/__init__.py29
-rw-r--r--sleekxmpp/plugins/base.py36
-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.py25
-rw-r--r--sleekxmpp/plugins/xep_0009/remote.py61
-rw-r--r--sleekxmpp/plugins/xep_0009/rpc.py10
-rw-r--r--sleekxmpp/plugins/xep_0013/__init__.py15
-rw-r--r--sleekxmpp/plugins/xep_0013/offline.py134
-rw-r--r--sleekxmpp/plugins/xep_0013/stanza.py53
-rw-r--r--sleekxmpp/plugins/xep_0016/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0016/privacy.py110
-rw-r--r--sleekxmpp/plugins/xep_0016/stanza.py103
-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.py15
-rw-r--r--sleekxmpp/plugins/xep_0027/stanza.py2
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py49
-rw-r--r--sleekxmpp/plugins/xep_0030/stanza/items.py7
-rw-r--r--sleekxmpp/plugins/xep_0045.py77
-rw-r--r--sleekxmpp/plugins/xep_0047/ibb.py126
-rw-r--r--sleekxmpp/plugins/xep_0047/stanza.py4
-rw-r--r--sleekxmpp/plugins/xep_0047/stream.py67
-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_0049/__init__.py15
-rw-r--r--sleekxmpp/plugins/xep_0049/private_storage.py53
-rw-r--r--sleekxmpp/plugins/xep_0049/stanza.py17
-rw-r--r--sleekxmpp/plugins/xep_0050/adhoc.py140
-rw-r--r--sleekxmpp/plugins/xep_0054/stanza.py4
-rw-r--r--sleekxmpp/plugins/xep_0054/vcard_temp.py20
-rw-r--r--sleekxmpp/plugins/xep_0059/rsm.py14
-rw-r--r--sleekxmpp/plugins/xep_0060/pubsub.py6
-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.py31
-rw-r--r--sleekxmpp/plugins/xep_0078/legacyauth.py49
-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.py19
-rw-r--r--sleekxmpp/plugins/xep_0084/stanza.py11
-rw-r--r--sleekxmpp/plugins/xep_0085/chat_states.py1
-rw-r--r--sleekxmpp/plugins/xep_0086/legacy_error.py5
-rw-r--r--sleekxmpp/plugins/xep_0091/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0091/legacy_delay.py29
-rw-r--r--sleekxmpp/plugins/xep_0091/stanza.py47
-rw-r--r--sleekxmpp/plugins/xep_0092/version.py25
-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_0106.py26
-rw-r--r--sleekxmpp/plugins/xep_0115/caps.py125
-rw-r--r--sleekxmpp/plugins/xep_0131/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0131/headers.py41
-rw-r--r--sleekxmpp/plugins/xep_0131/stanza.py51
-rw-r--r--sleekxmpp/plugins/xep_0133.py54
-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.py76
-rw-r--r--sleekxmpp/plugins/xep_0163.py2
-rw-r--r--sleekxmpp/plugins/xep_0184/receipt.py12
-rw-r--r--sleekxmpp/plugins/xep_0191/blocking.py4
-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_0198/stream_management.py53
-rw-r--r--sleekxmpp/plugins/xep_0199/ping.py147
-rw-r--r--sleekxmpp/plugins/xep_0202/time.py23
-rw-r--r--sleekxmpp/plugins/xep_0203/stanza.py13
-rw-r--r--sleekxmpp/plugins/xep_0222.py7
-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_0235/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0235/oauth.py32
-rw-r--r--sleekxmpp/plugins/xep_0235/stanza.py80
-rw-r--r--sleekxmpp/plugins/xep_0242.py21
-rw-r--r--sleekxmpp/plugins/xep_0256.py5
-rw-r--r--sleekxmpp/plugins/xep_0257/__init__.py17
-rw-r--r--sleekxmpp/plugins/xep_0257/client_cert_management.py65
-rw-r--r--sleekxmpp/plugins/xep_0257/stanza.py87
-rw-r--r--sleekxmpp/plugins/xep_0258/stanza.py3
-rw-r--r--sleekxmpp/plugins/xep_0279/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0279/ipcheck.py39
-rw-r--r--sleekxmpp/plugins/xep_0279/stanza.py30
-rw-r--r--sleekxmpp/plugins/xep_0280/__init__.py17
-rw-r--r--sleekxmpp/plugins/xep_0280/carbons.py81
-rw-r--r--sleekxmpp/plugins/xep_0280/stanza.py64
-rw-r--r--sleekxmpp/plugins/xep_0297/__init__.py16
-rw-r--r--sleekxmpp/plugins/xep_0297/forwarded.py64
-rw-r--r--sleekxmpp/plugins/xep_0297/stanza.py36
-rw-r--r--sleekxmpp/plugins/xep_0308/__init__.py15
-rw-r--r--sleekxmpp/plugins/xep_0308/correction.py52
-rw-r--r--sleekxmpp/plugins/xep_0308/stanza.py16
-rw-r--r--sleekxmpp/plugins/xep_0313/__init__.py15
-rw-r--r--sleekxmpp/plugins/xep_0313/mam.py94
-rw-r--r--sleekxmpp/plugins/xep_0313/stanza.py139
-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
144 files changed, 8081 insertions, 2005 deletions
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
index 1613ac4d..951f31eb 100644
--- a/sleekxmpp/plugins/__init__.py
+++ b/sleekxmpp/plugins/__init__.py
@@ -11,43 +11,53 @@ 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
'xep_0059', # Result Set Management
'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
'xep_0085', # Chat State Notifications
'xep_0086', # Legacy Error Codes
+ 'xep_0091', # Legacy Delayed Delivery
'xep_0092', # Software Version
+ 'xep_0106', # JID Escaping
'xep_0107', # User Mood
'xep_0108', # User Activity
'xep_0115', # Entity Capabilities
'xep_0118', # User Tune
'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', # Simple Communications Blocking
+ 'xep_0191', # Blocking Command
+ 'xep_0196', # User Gaming
'xep_0198', # Stream Management
'xep_0199', # Ping
'xep_0202', # Entity Time
@@ -57,9 +67,20 @@ __all__ = [
'xep_0223', # Persistent Storage of Private Data via Pubsub
'xep_0224', # Attention
'xep_0231', # Bits of Binary
+ 'xep_0235', # OAuth Over XMPP
+ 'xep_0242', # XMPP Client Compliance 2009
'xep_0249', # Direct MUC Invitations
'xep_0256', # Last Activity in Presence
+ 'xep_0257', # Client Certificate Management for SASL EXTERNAL
'xep_0258', # Security Labels in XMPP
'xep_0270', # XMPP Compliance Suites 2010
+ 'xep_0279', # Server IP Check
+ 'xep_0280', # Message Carbons
+ 'xep_0297', # Stanza Forwarding
'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/base.py b/sleekxmpp/plugins/base.py
index 26f0c827..67675908 100644
--- a/sleekxmpp/plugins/base.py
+++ b/sleekxmpp/plugins/base.py
@@ -14,6 +14,7 @@
"""
import sys
+import copy
import logging
import threading
@@ -272,6 +273,14 @@ class BasePlugin(object):
#: be initialized as needed if this plugin is enabled.
dependencies = set()
+ #: The basic, standard configuration for the plugin, which may
+ #: be overridden when initializing the plugin. The configuration
+ #: fields included here may be accessed directly as attributes of
+ #: the plugin. For example, including the configuration field 'foo'
+ #: would mean accessing `plugin.foo` returns the current value of
+ #: `plugin.config['foo']`.
+ default_config = {}
+
def __init__(self, xmpp, config=None):
self.xmpp = xmpp
if self.xmpp:
@@ -279,7 +288,32 @@ class BasePlugin(object):
#: A plugin's behaviour may be configurable, in which case those
#: configuration settings will be provided as a dictionary.
- self.config = config if config is not None else {}
+ self.config = copy.copy(self.default_config)
+ if config:
+ self.config.update(config)
+
+ def __getattr__(self, key):
+ """Provide direct access to configuration fields.
+
+ If the standard configuration includes the option `'foo'`, then
+ accessing `self.foo` should be the same as `self.config['foo']`.
+ """
+ if key in self.default_config:
+ return self.config.get(key, None)
+ else:
+ return object.__getattribute__(self, key)
+
+ def __setattr__(self, key, value):
+ """Provide direct assignment to configuration fields.
+
+ If the standard configuration includes the option `'foo'`, then
+ assigning to `self.foo` should be the same as assigning to
+ `self.config['foo']`.
+ """
+ if key in self.default_config:
+ self.config[key] = value
+ else:
+ super(BasePlugin, self).__setattr__(key, value)
def _init(self):
"""Initialize plugin state, such as registering event handlers.
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 bbf0ee7d..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,13 +194,21 @@ 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']
if instructions in [None, '']:
return
- instructions = instructions.split('\n')
+ if not isinstance(instructions, list):
+ instructions = instructions.split('\n')
for instruction in instructions:
inst = ET.Element('{%s}instructions' % self.namespace)
inst.text = instruction
@@ -220,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_0013/__init__.py b/sleekxmpp/plugins/xep_0013/__init__.py
new file mode 100644
index 00000000..ad400949
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0013/__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_0013.stanza import Offline
+from sleekxmpp.plugins.xep_0013.offline import XEP_0013
+
+
+register_plugin(XEP_0013)
diff --git a/sleekxmpp/plugins/xep_0013/offline.py b/sleekxmpp/plugins/xep_0013/offline.py
new file mode 100644
index 00000000..a0d992a7
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0013/offline.py
@@ -0,0 +1,134 @@
+"""
+ 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
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp.stanza import Message, Iq
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.xmlstream.handler import Collector
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0013 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0013(BasePlugin):
+
+ """
+ XEP-0013 Flexible Offline Message Retrieval
+ """
+
+ name = 'xep_0013'
+ description = 'XEP-0013: Flexible Offline Message Retrieval'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, stanza.Offline)
+ register_stanza_plugin(Message, stanza.Offline)
+
+ def get_count(self, **kwargs):
+ return self.xmpp['xep_0030'].get_info(
+ node='http://jabber.org/protocol/offline',
+ local=False,
+ **kwargs)
+
+ def get_headers(self, **kwargs):
+ return self.xmpp['xep_0030'].get_items(
+ node='http://jabber.org/protocol/offline',
+ local=False,
+ **kwargs)
+
+ def view(self, nodes, ifrom=None, block=True, timeout=None, callback=None):
+ if not isinstance(nodes, (list, set)):
+ nodes = [nodes]
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ offline = iq['offline']
+ for node in nodes:
+ item = stanza.Item()
+ item['node'] = node
+ item['action'] = 'view'
+ offline.append(item)
+
+ collector = Collector(
+ 'Offline_Results_%s' % iq['id'],
+ StanzaPath('message/offline'))
+ self.xmpp.register_handler(collector)
+
+ if not block and callback is not None:
+ def wrapped_cb(iq):
+ results = collector.stop()
+ if iq['type'] == 'result':
+ iq['offline']['results'] = results
+ callback(iq)
+ return iq.send(block=block, timeout=timeout, callback=wrapped_cb)
+ else:
+ try:
+ resp = iq.send(block=block, timeout=timeout, callback=callback)
+ resp['offline']['results'] = collector.stop()
+ return resp
+ except XMPPError as e:
+ collector.stop()
+ raise e
+
+ def remove(self, nodes, ifrom=None, block=True, timeout=None, callback=None):
+ if not isinstance(nodes, (list, set)):
+ nodes = [nodes]
+
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ offline = iq['offline']
+ for node in nodes:
+ item = stanza.Item()
+ item['node'] = node
+ item['action'] = 'remove'
+ offline.append(item)
+
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def fetch(self, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['offline']['fetch'] = True
+
+ collector = Collector(
+ 'Offline_Results_%s' % iq['id'],
+ StanzaPath('message/offline'))
+ self.xmpp.register_handler(collector)
+
+ if not block and callback is not None:
+ def wrapped_cb(iq):
+ results = collector.stop()
+ if iq['type'] == 'result':
+ iq['offline']['results'] = results
+ callback(iq)
+ return iq.send(block=block, timeout=timeout, callback=wrapped_cb)
+ else:
+ try:
+ resp = iq.send(block=block, timeout=timeout, callback=callback)
+ resp['offline']['results'] = collector.stop()
+ return resp
+ except XMPPError as e:
+ collector.stop()
+ raise e
+
+ def purge(self, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['offline']['purge'] = True
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0013/stanza.py b/sleekxmpp/plugins/xep_0013/stanza.py
new file mode 100644
index 00000000..c9c69786
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0013/stanza.py
@@ -0,0 +1,53 @@
+"""
+ 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.jid import JID
+from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin
+
+
+class Offline(ElementBase):
+ name = 'offline'
+ namespace = 'http://jabber.org/protocol/offline'
+ plugin_attrib = 'offline'
+ interfaces = set(['fetch', 'purge', 'results'])
+ bool_interfaces = interfaces
+
+ def setup(self, xml=None):
+ ElementBase.setup(self, xml)
+ self._results = []
+
+ # The results interface is meant only as an easy
+ # way to access the set of collected message responses
+ # from the query.
+
+ def get_results(self):
+ return self._results
+
+ def set_results(self, values):
+ self._results = values
+
+ def del_results(self):
+ self._results = []
+
+
+class Item(ElementBase):
+ name = 'item'
+ namespace = 'http://jabber.org/protocol/offline'
+ plugin_attrib = 'item'
+ interfaces = set(['action', 'node', 'jid'])
+
+ actions = set(['view', 'remove'])
+
+ def get_jid(self):
+ return JID(self._get_attr('jid'))
+
+ def set_jid(self, value):
+ self._set_attr('jid', str(value))
+
+
+register_stanza_plugin(Offline, Item, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0016/__init__.py b/sleekxmpp/plugins/xep_0016/__init__.py
new file mode 100644
index 00000000..06704d26
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0016/__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_0016 import stanza
+from sleekxmpp.plugins.xep_0016.stanza import Privacy
+from sleekxmpp.plugins.xep_0016.privacy import XEP_0016
+
+
+register_plugin(XEP_0016)
diff --git a/sleekxmpp/plugins/xep_0016/privacy.py b/sleekxmpp/plugins/xep_0016/privacy.py
new file mode 100644
index 00000000..79fd68f0
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0016/privacy.py
@@ -0,0 +1,110 @@
+"""
+ 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.
+"""
+
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0016 import stanza
+from sleekxmpp.plugins.xep_0016.stanza import Privacy, Item
+
+
+class XEP_0016(BasePlugin):
+
+ name = 'xep_0016'
+ description = 'XEP-0016: Privacy Lists'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, Privacy)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Privacy.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Privacy.namespace)
+
+ def get_privacy_lists(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq.enable('privacy')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_list(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy']['list']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_active(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy'].enable('active')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_default(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['privacy'].enable('default')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def activate(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['active']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def deactivate(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy'].enable('active')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def make_default(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['default']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def remove_default(self, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy'].enable('default')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def edit_list(self, name, rules, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['list']['name'] = name
+ priv_list = iq['privacy']['list']
+
+ if not rules:
+ rules = []
+
+ for rule in rules:
+ if isinstance(rule, Item):
+ priv_list.append(rule)
+ continue
+
+ priv_list.add_item(
+ rule['value'],
+ rule['action'],
+ rule['order'],
+ itype=rule.get('type', None),
+ iq=rule.get('iq', None),
+ message=rule.get('message', None),
+ presence_in=rule.get('presence_in',
+ rule.get('presence-in', None)),
+ presence_out=rule.get('presence_out',
+ rule.get('presence-out', None)))
+
+ def remove_list(self, name, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['privacy']['list']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0016/stanza.py b/sleekxmpp/plugins/xep_0016/stanza.py
new file mode 100644
index 00000000..3f9977fc
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0016/stanza.py
@@ -0,0 +1,103 @@
+from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin
+
+
+class Privacy(ElementBase):
+ name = 'query'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = 'privacy'
+ interfaces = set()
+
+ def add_list(self, name):
+ priv_list = List()
+ priv_list['name'] = name
+ self.append(priv_list)
+ return priv_list
+
+
+class Active(ElementBase):
+ name = 'active'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+
+class Default(ElementBase):
+ name = 'default'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ interfaces = set(['name'])
+
+
+class List(ElementBase):
+ name = 'list'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ plugin_multi_attrib = 'lists'
+ interfaces = set(['name'])
+
+ def add_item(self, value, action, order, itype=None, iq=False,
+ message=False, presence_in=False, presence_out=False):
+ item = Item()
+ item.values = {'type': itype,
+ 'value': value,
+ 'action': action,
+ 'order': order,
+ 'message': message,
+ 'iq': iq,
+ 'presence_in': presence_in,
+ 'presence_out': presence_out}
+ self.append(item)
+ return item
+
+
+class Item(ElementBase):
+ name = 'item'
+ namespace = 'jabber:iq:privacy'
+ plugin_attrib = name
+ plugin_multi_attrib = 'items'
+ interfaces = set(['type', 'value', 'action', 'order', 'iq',
+ 'message', 'presence_in', 'presence_out'])
+ bool_interfaces = set(['message', 'iq', 'presence_in', 'presence_out'])
+
+ type_values = ('', 'jid', 'group', 'subscription')
+ action_values = ('allow', 'deny')
+
+ def set_type(self, value):
+ if value and value not in self.type_values:
+ raise ValueError('Unknown type value: %s' % value)
+ else:
+ self._set_attr('type', value)
+
+ def set_action(self, value):
+ if value not in self.action_values:
+ raise ValueError('Unknown action value: %s' % value)
+ else:
+ self._set_attr('action', value)
+
+ def set_presence_in(self, value):
+ keep = True if value else False
+ self._set_sub_text('presence-in', '', keep=keep)
+
+ def get_presence_in(self):
+ pres = self.xml.find('{%s}presence-in' % self.namespace)
+ return pres is not None
+
+ def del_presence_in(self):
+ self._del_sub('{%s}presence-in' % self.namespace)
+
+ def set_presence_out(self, value):
+ keep = True if value else False
+ self._set_sub_text('presence-in', '', keep=keep)
+
+ def get_presence_out(self):
+ pres = self.xml.find('{%s}presence-in' % self.namespace)
+ return pres is not None
+
+ def del_presence_out(self):
+ self._del_sub('{%s}presence-in' % self.namespace)
+
+
+register_stanza_plugin(Privacy, Active)
+register_stanza_plugin(Privacy, Default)
+register_stanza_plugin(Privacy, List, iterable=True)
+register_stanza_plugin(List, Item, iterable=True)
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 9c6ca078..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 == '':
+ if begin_headers and line.strip() == '':
begin_data = True
continue
if 'END PGP %s' % kind in line:
@@ -40,14 +40,15 @@ class XEP_0027(BasePlugin):
description = 'XEP-0027: Current Jabber OpenPGP Usage'
dependencies = set()
stanza = stanza
+ default_config = {
+ 'gpg_binary': 'gpg',
+ 'gpg_home': '',
+ 'use_agent': True,
+ 'keyring': None,
+ 'key_server': 'pgp.mit.edu'
+ }
def plugin_init(self):
- self.gpg_binary = self.config.get('gpg_binary', 'gpg')
- self.gpg_home = self.config.get('gpg_home', '')
- self.use_agent = self.config.get('use_agent', True)
- self.keyring = self.config.get('keyring', None)
- self.key_server = self.config.get('key_server', 'pgp.mit.edu')
-
self.gpg = GPG(gnupghome=self.gpg_home,
gpgbinary=self.gpg_binary,
use_agent=self.use_agent,
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 eeb977b1..721f73f6 100644
--- a/sleekxmpp/plugins/xep_0030/disco.py
+++ b/sleekxmpp/plugins/xep_0030/disco.py
@@ -88,6 +88,10 @@ class XEP_0030(BasePlugin):
description = 'XEP-0030: Service Discovery'
dependencies = set()
stanza = stanza
+ default_config = {
+ 'use_cache': True,
+ 'wrap_results': False
+ }
def plugin_init(self):
"""
@@ -108,9 +112,6 @@ class XEP_0030(BasePlugin):
self.static = StaticDisco(self.xmpp, self)
- self.use_cache = self.config.get('use_cache', True)
- self.wrap_results = self.config.get('wrap_results', False)
-
self._disco_ops = [
'get_info', 'set_info', 'set_identities', 'set_features',
'get_items', 'set_items', 'del_items', 'add_identity',
@@ -287,7 +288,7 @@ class XEP_0030(BasePlugin):
'cached': cached}
return self.api['has_identity'](jid, node, ifrom, data)
- def get_info(self, jid=None, node=None, local=False,
+ def get_info(self, jid=None, node=None, local=None,
cached=None, **kwargs):
"""
Retrieve the disco#info results from a given JID/node combination.
@@ -323,18 +324,21 @@ 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
+ has been received in timeout seconds.
"""
- 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 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:
log.debug("Looking up local disco#info data " + \
@@ -362,7 +366,8 @@ class XEP_0030(BasePlugin):
iq['disco_info']['node'] = node if node else ''
return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
- callback=kwargs.get('callback', None))
+ callback=kwargs.get('callback', None),
+ timeout_callback=kwargs.get('timeout_callback', None))
def set_info(self, jid=None, node=None, info=None):
"""
@@ -403,8 +408,10 @@ 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
+ has been received in timeout seconds.
"""
- if local or jid is None:
+ if local or local is None and jid is None:
items = self.api['get_items'](jid, node,
kwargs.get('ifrom', None),
kwargs)
@@ -421,7 +428,8 @@ class XEP_0030(BasePlugin):
else:
return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
- callback=kwargs.get('callback', None))
+ callback=kwargs.get('callback', None),
+ timeout_callback=kwargs.get('timeout_callback', None))
def set_items(self, jid=None, node=None, **kwargs):
"""
@@ -596,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.
@@ -607,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 7fbb3d43..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))
@@ -137,7 +138,7 @@ class XEP_0045(BasePlugin):
def handle_groupchat_invite(self, inv):
""" Handle an invite into a muc.
"""
- logging.debug("MUC invite to %s from %s: %s", inv['from'], inv["from"], inv)
+ logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv)
if inv['from'] not in self.rooms.keys():
self.xmpp.event("groupchat_invite", inv)
@@ -156,6 +157,7 @@ class XEP_0045(BasePlugin):
entry = pr['muc'].getStanzaValues()
entry['show'] = pr['show']
entry['status'] = pr['status']
+ entry['alt_nick'] = pr['nick']
if pr['type'] == 'unavailable':
if entry['nick'] in self.rooms[entry['room']]:
del self.rooms[entry['room']][entry['nick']]
@@ -178,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)
@@ -197,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:
@@ -244,11 +233,11 @@ class XEP_0045(BasePlugin):
stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom)
x = ET.Element('{http://jabber.org/protocol/muc}x')
if password:
- passelement = ET.Element('password')
+ passelement = ET.Element('{http://jabber.org/protocol/muc}password')
passelement.text = password
x.append(passelement)
if maxhistory:
- history = ET.Element('history')
+ history = ET.Element('{http://jabber.org/protocol/muc}history')
if maxhistory == "0":
history.attrib['maxchars'] = maxhistory
else:
@@ -270,10 +259,10 @@ class XEP_0045(BasePlugin):
iq['from'] = ifrom
iq['to'] = room
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
- destroy = ET.Element('destroy')
+ destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
if altroom:
destroy.attrib['jid'] = altroom
- xreason = ET.Element('reason')
+ xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
xreason.text = reason
destroy.append(xreason)
query.append(destroy)
@@ -293,9 +282,9 @@ class XEP_0045(BasePlugin):
raise TypeError
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
if nick is not None:
- item = ET.Element('item', {'affiliation':affiliation, 'nick':nick})
+ item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
else:
- item = ET.Element('item', {'affiliation':affiliation, 'jid':jid})
+ item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid})
query.append(item)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
@@ -309,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)
@@ -316,7 +323,7 @@ class XEP_0045(BasePlugin):
x = ET.Element('{http://jabber.org/protocol/muc#user}x')
invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
if reason:
- rxml = ET.Element('reason')
+ rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
rxml.text = reason
invite.append(rxml)
x.append(invite)
diff --git a/sleekxmpp/plugins/xep_0047/ibb.py b/sleekxmpp/plugins/xep_0047/ibb.py
index 2b8c57d4..62dddac2 100644
--- a/sleekxmpp/plugins/xep_0047/ibb.py
+++ b/sleekxmpp/plugins/xep_0047/ibb.py
@@ -20,21 +20,26 @@ class XEP_0047(BasePlugin):
description = 'XEP-0047: In-band Bytestreams'
dependencies = set(['xep_0030'])
stanza = stanza
+ default_config = {
+ 'block_size': 4096,
+ 'max_block_size': 8192,
+ 'window_size': 1,
+ 'auto_accept': False,
+ }
def plugin_init(self):
- self.streams = {}
- self.pending_streams = {3: 5}
- self.pending_close_streams = {}
+ self._streams = {}
+ self._pending_streams = {}
+ self._pending_lock = threading.Lock()
self._stream_lock = threading.Lock()
- self.max_block_size = self.config.get('max_block_size', 8192)
- self.window_size = self.config.get('window_size', 1)
- self.auto_accept = self.config.get('auto_accept', True)
- self.accept_stream = self.config.get('accept_stream', None)
+ 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',
@@ -51,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'
@@ -82,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)
@@ -107,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/stanza.py b/sleekxmpp/plugins/xep_0047/stanza.py
index afba07a8..7e5d2fed 100644
--- a/sleekxmpp/plugins/xep_0047/stanza.py
+++ b/sleekxmpp/plugins/xep_0047/stanza.py
@@ -1,9 +1,9 @@
import re
import base64
+from sleekxmpp.util import bytes
from sleekxmpp.exceptions import XMPPError
from sleekxmpp.xmlstream import ElementBase
-from sleekxmpp.thirdparty.suelta.util import bytes
VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
@@ -14,7 +14,7 @@ def to_b64(data):
def from_b64(data):
- return bytes(base64.b64decode(bytes(data))).decode('utf-8')
+ return bytes(base64.b64decode(bytes(data)))
class Open(ElementBase):
diff --git a/sleekxmpp/plugins/xep_0047/stream.py b/sleekxmpp/plugins/xep_0047/stream.py
index 49f56f36..9651edf8 100644
--- a/sleekxmpp/plugins/xep_0047/stream.py
+++ b/sleekxmpp/plugins/xep_0047/stream.py
@@ -1,11 +1,9 @@
import socket
import threading
import logging
-try:
- import queue
-except ImportError:
- import Queue as queue
+from sleekxmpp.stanza import Iq
+from sleekxmpp.util import Queue
from sleekxmpp.exceptions import XMPPError
@@ -14,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
@@ -33,7 +34,7 @@ class IBBytestream(object):
self.stream_in_closed = threading.Event()
self.stream_out_closed = threading.Event()
- self.recv_queue = queue.Queue()
+ self.recv_queue = Queue()
self.send_window = threading.BoundedSemaphore(value=self.window_size)
self.window_ids = set()
@@ -49,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):
@@ -74,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)
@@ -109,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,
@@ -120,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_0049/__init__.py b/sleekxmpp/plugins/xep_0049/__init__.py
new file mode 100644
index 00000000..b0c4f904
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0049/__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 permission.
+"""
+
+from sleekxmpp.plugins.base import register_plugin
+
+from sleekxmpp.plugins.xep_0049.stanza import PrivateXML
+from sleekxmpp.plugins.xep_0049.private_storage import XEP_0049
+
+
+register_plugin(XEP_0049)
diff --git a/sleekxmpp/plugins/xep_0049/private_storage.py b/sleekxmpp/plugins/xep_0049/private_storage.py
new file mode 100644
index 00000000..ef6cbdde
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0049/private_storage.py
@@ -0,0 +1,53 @@
+"""
+ 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.
+"""
+
+import logging
+
+from sleekxmpp import Iq
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0049 import stanza, PrivateXML
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0049(BasePlugin):
+
+ name = 'xep_0049'
+ description = 'XEP-0049: Private XML Storage'
+ dependencies = set([])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, PrivateXML)
+
+ def register(self, stanza):
+ register_stanza_plugin(PrivateXML, stanza, iterable=True)
+
+ def store(self, data, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+
+ if not isinstance(data, list):
+ data = [data]
+
+ for elem in data:
+ iq['private'].append(elem)
+
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def retrieve(self, name, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ iq['private'].enable(name)
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0049/stanza.py b/sleekxmpp/plugins/xep_0049/stanza.py
new file mode 100644
index 00000000..d424e2f0
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0049/stanza.py
@@ -0,0 +1,17 @@
+"""
+ 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
+
+
+class PrivateXML(ElementBase):
+
+ name = 'query'
+ namespace = 'jabber:iq:private'
+ plugin_attrib = 'private'
+ interfaces = set()
diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py
index a833221a..e5594c3f 100644
--- a/sleekxmpp/plugins/xep_0050/adhoc.py
+++ b/sleekxmpp/plugins/xep_0050/adhoc.py
@@ -82,12 +82,18 @@ class XEP_0050(BasePlugin):
description = 'XEP-0050: Ad-Hoc Commands'
dependencies = set(['xep_0030', 'xep_0004'])
stanza = stanza
+ default_config = {
+ 'threaded': True,
+ 'session_db': None
+ }
def plugin_init(self):
"""Start the XEP-0050 plugin."""
- self.threaded = self.config.get('threaded', True)
+ self.sessions = self.session_db
+ if self.sessions is None:
+ self.sessions = {}
+
self.commands = {}
- self.sessions = self.config.get('session_db', {})
self.xmpp.register_handler(
Callback("Ad-Hoc Execute",
@@ -181,12 +187,6 @@ class XEP_0050(BasePlugin):
jid = JID(jid)
item_jid = jid.full
- # Client disco uses only the bare JID
- if self.xmpp.is_component:
- jid = jid.full
- else:
- jid = jid.bare
-
self.xmpp['xep_0030'].add_identity(category='automation',
itype='command-list',
name='Ad-Hoc commands',
@@ -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/stanza.py b/sleekxmpp/plugins/xep_0054/stanza.py
index 75b69d3e..72da0b51 100644
--- a/sleekxmpp/plugins/xep_0054/stanza.py
+++ b/sleekxmpp/plugins/xep_0054/stanza.py
@@ -1,8 +1,7 @@
import base64
import datetime as dt
-from sleekxmpp.thirdparty.suelta.util import bytes
-
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID
from sleekxmpp.plugins import xep_0082
@@ -542,6 +541,7 @@ register_stanza_plugin(VCardTemp, Logo, iterable=True)
register_stanza_plugin(VCardTemp, Mailer, iterable=True)
register_stanza_plugin(VCardTemp, Note, iterable=True)
register_stanza_plugin(VCardTemp, Nickname, iterable=True)
+register_stanza_plugin(VCardTemp, Org, iterable=True)
register_stanza_plugin(VCardTemp, Photo, iterable=True)
register_stanza_plugin(VCardTemp, ProdID, iterable=True)
register_stanza_plugin(VCardTemp, Rev, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0054/vcard_temp.py b/sleekxmpp/plugins/xep_0054/vcard_temp.py
index 83cbccf8..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)
@@ -97,8 +107,8 @@ class XEP_0054(BasePlugin):
def publish_vcard(self, vcard=None, jid=None, block=True, ifrom=None,
callback=None, timeout=None):
+ self.api['set_vcard'](jid, None, ifrom, vcard)
if self.xmpp.is_component:
- self.api['set_vcard'](jid, None, ifrom, vcard)
return
iq = self.xmpp.Iq()
diff --git a/sleekxmpp/plugins/xep_0059/rsm.py b/sleekxmpp/plugins/xep_0059/rsm.py
index 59cfc10b..d73b45bc 100644
--- a/sleekxmpp/plugins/xep_0059/rsm.py
+++ b/sleekxmpp/plugins/xep_0059/rsm.py
@@ -25,11 +25,14 @@ class ResultIterator():
An iterator for Result Set Managment
"""
- def __init__(self, query, interface, amount=10, start=None, reverse=False):
+ def __init__(self, query, interface, results='substanzas', amount=10,
+ start=None, reverse=False):
"""
Arguments:
query -- The template query
interface -- The substanza of the query, for example disco_items
+ results -- The query stanza's interface which provides a
+ countable list of query results.
amount -- The max amounts of items to request per iteration
start -- From which item id to start
reverse -- If True, page backwards through the results
@@ -46,6 +49,7 @@ class ResultIterator():
self.amount = amount
self.start = start
self.interface = interface
+ self.results = results
self.reverse = reverse
self._stop = False
@@ -85,7 +89,7 @@ class ResultIterator():
r[self.interface]['rsm']['first_index']:
count = int(r[self.interface]['rsm']['count'])
first = int(r[self.interface]['rsm']['first_index'])
- num_items = len(r[self.interface]['substanzas'])
+ num_items = len(r[self.interface][self.results])
if first + num_items == count:
self._stop = True
@@ -123,7 +127,7 @@ class XEP_0059(BasePlugin):
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Set.namespace)
- def iterate(self, stanza, interface):
+ def iterate(self, stanza, interface, results='substanzas'):
"""
Create a new result set iterator for a given stanza query.
@@ -135,5 +139,7 @@ class XEP_0059(BasePlugin):
result set management stanza should be
appended. For example, for disco#items queries
the interface 'disco_items' should be used.
+ results -- The name of the interface containing the
+ query results (typically just 'substanzas').
"""
- return ResultIterator(stanza, interface)
+ return ResultIterator(stanza, interface, results)
diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py
index 387c5a0f..bec5f565 100644
--- a/sleekxmpp/plugins/xep_0060/pubsub.py
+++ b/sleekxmpp/plugins/xep_0060/pubsub.py
@@ -26,7 +26,7 @@ class XEP_0060(BasePlugin):
name = 'xep_0060'
description = 'XEP-0060: Publish-Subscribe'
- dependencies = set(['xep_0030', 'xep_0004'])
+ dependencies = set(['xep_0030', 'xep_0004', 'xep_0082', 'xep_0131'])
stanza = stanza
def plugin_init(self):
@@ -53,6 +53,8 @@ class XEP_0060(BasePlugin):
StanzaPath('message/pubsub_event/subscription'),
self._handle_event_subscription))
+ self.xmpp['xep_0131'].supported_headers.add('SubID')
+
def plugin_end(self):
self.xmpp.remove_handler('Pubsub Event: Items')
self.xmpp.remove_handler('Pubsub Event: Purge')
@@ -421,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 7f00354b..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
@@ -27,10 +28,13 @@ class XEP_0077(BasePlugin):
description = 'XEP-0077: In-Band Registration'
dependencies = set(['xep_0004', 'xep_0066'])
stanza = stanza
+ default_config = {
+ 'create_account': True,
+ 'force_registration': False,
+ 'order': 50
+ }
def plugin_init(self):
- self.create_account = self.config.get('create_account', True)
-
register_stanza_plugin(StreamFeatures, RegisterFeature)
register_stanza_plugin(Iq, Register)
@@ -38,14 +42,33 @@ class XEP_0077(BasePlugin):
self.xmpp.register_feature('register',
self._handle_register_feature,
restart=False,
- order=self.config.get('order', 50))
+ order=self.order)
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.config.get('order', 50))
+ 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:
diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py
index 8ea78fba..da6bfa2c 100644
--- a/sleekxmpp/plugins/xep_0078/legacyauth.py
+++ b/sleekxmpp/plugins/xep_0078/legacyauth.py
@@ -6,11 +6,13 @@
See the file LICENSE for copying permission.
"""
+import uuid
import logging
import hashlib
import random
import sys
+from sleekxmpp.jid import JID
from sleekxmpp.exceptions import IqError, IqTimeout
from sleekxmpp.stanza import Iq, StreamFeatures
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
@@ -34,23 +36,37 @@ class XEP_0078(BasePlugin):
description = 'XEP-0078: Non-SASL Authentication'
dependencies = set()
stanza = stanza
+ default_config = {
+ 'order': 15
+ }
def plugin_init(self):
self.xmpp.register_feature('auth',
self._handle_auth,
restart=False,
- order=self.config.get('order', 15))
+ order=self.order)
+
+ self.xmpp.add_event_handler('legacy_protocol',
+ self._handle_legacy_protocol)
register_stanza_plugin(Iq, stanza.IqAuth)
register_stanza_plugin(StreamFeatures, stanza.AuthFeature)
def plugin_end(self):
- self.xmpp.unregister_feature('auth', self.config.get('order', 15))
+ self.xmpp.del_event_handler('legacy_protocol',
+ self._handle_legacy_protocol)
+ self.xmpp.unregister_feature('auth', self.order)
def _handle_auth(self, features):
# If we can or have already authenticated with SASL, do nothing.
if 'mechanisms' in features['features']:
return False
+ return self.authenticate()
+
+ def _handle_legacy_protocol(self, event):
+ self.authenticate()
+
+ def authenticate(self):
if self.xmpp.authenticated:
return False
@@ -59,13 +75,13 @@ class XEP_0078(BasePlugin):
# Step 1: Request the auth form
iq = self.xmpp.Iq()
iq['type'] = 'get'
- iq['to'] = self.xmpp.boundjid.host
- iq['auth']['username'] = self.xmpp.boundjid.user
+ iq['to'] = self.xmpp.requested_jid.host
+ iq['auth']['username'] = self.xmpp.requested_jid.user
try:
resp = iq.send(now=True)
- except IqError:
- log.info("Authentication failed: %s", resp['error']['condition'])
+ except IqError as err:
+ log.info("Authentication failed: %s", err.iq['error']['condition'])
self.xmpp.event('failed_auth', direct=True)
self.xmpp.disconnect()
return True
@@ -78,13 +94,14 @@ class XEP_0078(BasePlugin):
# Step 2: Fill out auth form for either password or digest auth
iq = self.xmpp.Iq()
iq['type'] = 'set'
- iq['auth']['username'] = self.xmpp.boundjid.user
+ iq['auth']['username'] = self.xmpp.requested_jid.user
# A resource is required, so create a random one if necessary
- if self.xmpp.boundjid.resource:
- iq['auth']['resource'] = self.xmpp.boundjid.resource
- else:
- iq['auth']['resource'] = '%s' % random.random()
+ resource = self.xmpp.requested_jid.resource
+ if not resource:
+ resource = str(uuid.uuid4())
+
+ iq['auth']['resource'] = resource
if 'digest' in resp['auth']['fields']:
log.debug('Authenticating via jabber:iq:auth Digest')
@@ -106,16 +123,22 @@ class XEP_0078(BasePlugin):
result = iq.send(now=True)
except IqError as err:
log.info("Authentication failed")
- self.xmpp.disconnect()
self.xmpp.event("failed_auth", direct=True)
+ self.xmpp.disconnect()
except IqTimeout:
log.info("Authentication failed")
- self.xmpp.disconnect()
self.xmpp.event("failed_auth", direct=True)
+ self.xmpp.disconnect()
self.xmpp.features.add('auth')
self.xmpp.authenticated = True
+
+ self.xmpp.boundjid = JID(self.xmpp.requested_jid,
+ resource=resource,
+ cache_lock=True)
+ self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
+
log.debug("Established Session")
self.xmpp.sessionstarted = True
self.xmpp.session_started_event.set()
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 bbac330a..677a888d 100644
--- a/sleekxmpp/plugins/xep_0084/avatar.py
+++ b/sleekxmpp/plugins/xep_0084/avatar.py
@@ -41,6 +41,9 @@ class XEP_0084(BasePlugin):
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData)
+ def generate_id(self, data):
+ return hashlib.sha1(data).hexdigest()
+
def retrieve_avatar(self, jid, id, url=None, ifrom=None, block=True,
callback=None, timeout=None):
return self.xmpp['xep_0060'].get_item(jid, Data.namespace, id,
@@ -54,8 +57,7 @@ class XEP_0084(BasePlugin):
payload = Data()
payload['value'] = data
return self.xmpp['xep_0163'].publish(payload,
- node=Data.namespace,
- id=hashlib.sha1(data).hexdigest(),
+ id=self.generate_id(data),
ifrom=ifrom,
block=block,
callback=callback,
@@ -67,17 +69,20 @@ class XEP_0084(BasePlugin):
metadata = MetaData()
if items is None:
items = []
+ if not isinstance(items, (list, set)):
+ items = [items]
for info in items:
metadata.add_info(info['id'], info['type'], info['bytes'],
height=info.get('height', ''),
width=info.get('width', ''),
url=info.get('url', ''))
- for pointer in pointers:
- metadata.add_pointer(pointer)
- return self.xmpp['xep_0163'].publish(payload,
- node=Data.namespace,
- id=hashlib.sha1(data).hexdigest(),
+ if pointers is not None:
+ for pointer in pointers:
+ 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 1b204471..fd21e6f1 100644
--- a/sleekxmpp/plugins/xep_0084/stanza.py
+++ b/sleekxmpp/plugins/xep_0084/stanza.py
@@ -7,8 +7,8 @@
"""
from base64 import b64encode, b64decode
-from sleekxmpp.thirdparty.suelta.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 = ''
@@ -43,7 +46,7 @@ class MetaData(ElementBase):
info = Info()
info.values = {'id': id,
'type': itype,
- 'bytes': ibytes,
+ 'bytes': '%s' % ibytes,
'height': height,
'width': width,
'url': url}
diff --git a/sleekxmpp/plugins/xep_0085/chat_states.py b/sleekxmpp/plugins/xep_0085/chat_states.py
index 17e19d35..17f82afd 100644
--- a/sleekxmpp/plugins/xep_0085/chat_states.py
+++ b/sleekxmpp/plugins/xep_0085/chat_states.py
@@ -52,4 +52,5 @@ class XEP_0085(BasePlugin):
def _handle_chat_state(self, msg):
state = msg['chat_state']
log.debug("Chat State: %s, %s", state, msg['from'].jid)
+ self.xmpp.event('chatstate', msg)
self.xmpp.event('chatstate_%s' % state, msg)
diff --git a/sleekxmpp/plugins/xep_0086/legacy_error.py b/sleekxmpp/plugins/xep_0086/legacy_error.py
index bed22ee2..f7d0ac9c 100644
--- a/sleekxmpp/plugins/xep_0086/legacy_error.py
+++ b/sleekxmpp/plugins/xep_0086/legacy_error.py
@@ -37,7 +37,10 @@ class XEP_0086(BasePlugin):
description = 'XEP-0086: Error Condition Mappings'
dependencies = set()
stanza = stanza
+ default_config = {
+ 'override': True
+ }
def plugin_init(self):
register_stanza_plugin(Error, LegacyError,
- overrides=self.config.get('override', True))
+ overrides=self.override)
diff --git a/sleekxmpp/plugins/xep_0091/__init__.py b/sleekxmpp/plugins/xep_0091/__init__.py
new file mode 100644
index 00000000..04f21ef5
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0091/__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_0091 import stanza
+from sleekxmpp.plugins.xep_0091.stanza import LegacyDelay
+from sleekxmpp.plugins.xep_0091.legacy_delay import XEP_0091
+
+
+register_plugin(XEP_0091)
diff --git a/sleekxmpp/plugins/xep_0091/legacy_delay.py b/sleekxmpp/plugins/xep_0091/legacy_delay.py
new file mode 100644
index 00000000..7323d468
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0091/legacy_delay.py
@@ -0,0 +1,29 @@
+"""
+ 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, Presence
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0091 import stanza
+
+
+class XEP_0091(BasePlugin):
+
+ """
+ XEP-0091: Legacy Delayed Delivery
+ """
+
+ name = 'xep_0091'
+ description = 'XEP-0091: Legacy Delayed Delivery'
+ dependencies = set()
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, stanza.LegacyDelay)
+ register_stanza_plugin(Presence, stanza.LegacyDelay)
diff --git a/sleekxmpp/plugins/xep_0091/stanza.py b/sleekxmpp/plugins/xep_0091/stanza.py
new file mode 100644
index 00000000..17e55764
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0091/stanza.py
@@ -0,0 +1,47 @@
+"""
+ 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.
+"""
+
+import datetime as dt
+
+from sleekxmpp.jid import JID
+from sleekxmpp.xmlstream import ElementBase
+from sleekxmpp.plugins import xep_0082
+
+
+class LegacyDelay(ElementBase):
+
+ name = 'x'
+ namespace = 'jabber:x:delay'
+ plugin_attrib = 'legacy_delay'
+ interfaces = set(('from', 'stamp', 'text'))
+
+ def get_from(self):
+ 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) if timestamp else None
+
+ def set_stamp(self, value):
+ if isinstance(value, dt.datetime):
+ value = value.astimezone(xep_0082.tzutc)
+ value = xep_0082.format_datetime(value)
+ self._set_attr('stamp', value[0:19].replace('-', ''))
+
+ def get_text(self):
+ return self.xml.text
+
+ def set_text(self, value):
+ self.xml.text = value
+
+ def del_text(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py
index 463da158..b16ad516 100644
--- a/sleekxmpp/plugins/xep_0092/version.py
+++ b/sleekxmpp/plugins/xep_0092/version.py
@@ -30,16 +30,18 @@ class XEP_0092(BasePlugin):
description = 'XEP-0092: Software Version'
dependencies = set(['xep_0030'])
stanza = stanza
+ default_config = {
+ 'software_name': 'SleekXMPP',
+ 'version': sleekxmpp.__version__,
+ 'os': ''
+ }
def plugin_init(self):
"""
Start the XEP-0092 plugin.
"""
- self.name = self.config.get('name', 'SleekXMPP')
- self.version = self.config.get('version', sleekxmpp.__version__)
- self.os = self.config.get('os', '')
-
- self.getVersion = self.get_version
+ if 'name' in self.config:
+ self.software_name = self.config['name']
self.xmpp.register_handler(
Callback('Software Version',
@@ -63,12 +65,12 @@ class XEP_0092(BasePlugin):
iq -- The Iq stanza containing the software version query.
"""
iq.reply()
- iq['software_version']['name'] = self.name
+ iq['software_version']['name'] = self.software_name
iq['software_version']['version'] = self.version
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.
@@ -80,11 +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
+ 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_0106.py b/sleekxmpp/plugins/xep_0106.py
new file mode 100644
index 00000000..1859a77b
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0106.py
@@ -0,0 +1,26 @@
+"""
+ 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 import BasePlugin, register_plugin
+
+
+class XEP_0106(BasePlugin):
+
+ name = 'xep_0106'
+ description = 'XEP-0106: JID Escaping'
+ dependencies = set(['xep_0030'])
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(feature='jid\\20escaping')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='jid\\20escaping')
+
+
+register_plugin(XEP_0106)
diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py
index 8ce10edb..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
@@ -33,19 +34,19 @@ class XEP_0115(BasePlugin):
description = 'XEP-0115: Entity Capabilities'
dependencies = set(['xep_0030', 'xep_0128', 'xep_0004'])
stanza = stanza
+ default_config = {
+ 'hash': 'sha-1',
+ 'caps_node': None,
+ 'broadcast': True
+ }
def plugin_init(self):
self.hashes = {'sha-1': hashlib.sha1,
'sha1': hashlib.sha1,
'md5': hashlib.md5}
- self.hash = self.config.get('hash', 'sha-1')
- self.caps_node = self.config.get('caps_node', None)
- self.broadcast = self.config.get('broadcast', True)
-
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)
@@ -89,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)
@@ -103,12 +107,17 @@ class XEP_0115(BasePlugin):
self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace)
def _filter_add_caps(self, stanza):
- if isinstance(stanza, Presence) and self.broadcast:
- ver = self.get_verstring(stanza['from'])
- if ver:
- stanza['caps']['node'] = self.caps_node
- stanza['caps']['hash'] = self.hash
- stanza['caps']['ver'] = ver
+ if not isinstance(stanza, Presence) or not self.broadcast:
+ return stanza
+
+ if stanza['type'] not in ('available', 'chat', 'away', 'dnd', 'xa'):
+ return stanza
+
+ ver = self.get_verstring(stanza['from'])
+ if ver:
+ stanza['caps']['node'] = self.caps_node
+ stanza['caps']['hash'] = self.hash
+ stanza['caps']['ver'] = ver
return stanza
def _handle_caps(self, presence):
@@ -129,12 +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=ver)
+ if existing_caps is not None:
+ self.assign_verstring(pres['from'], ver)
return
if pres['caps']['hash'] not in self.hashes:
@@ -145,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):
@@ -157,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
@@ -168,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):
@@ -179,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:
@@ -261,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):
@@ -275,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_0131/__init__.py b/sleekxmpp/plugins/xep_0131/__init__.py
new file mode 100644
index 00000000..ec71c98d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0131/__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_0131 import stanza
+from sleekxmpp.plugins.xep_0131.stanza import Headers
+from sleekxmpp.plugins.xep_0131.headers import XEP_0131
+
+
+register_plugin(XEP_0131)
diff --git a/sleekxmpp/plugins/xep_0131/headers.py b/sleekxmpp/plugins/xep_0131/headers.py
new file mode 100644
index 00000000..3e47541a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0131/headers.py
@@ -0,0 +1,41 @@
+"""
+ 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.
+"""
+
+from sleekxmpp import Message, Presence
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0131 import stanza
+from sleekxmpp.plugins.xep_0131.stanza import Headers
+
+
+class XEP_0131(BasePlugin):
+
+ name = 'xep_0131'
+ description = 'XEP-0131: Stanza Headers and Internet Metadata'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+ default_config = {
+ 'supported_headers': set()
+ }
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, Headers)
+ register_stanza_plugin(Presence, Headers)
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature=Headers.namespace)
+ for header in self.supported_headers:
+ self.xmpp['xep_0030'].del_feature(
+ feature='%s#%s' % (Headers.namespace, header))
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature(Headers.namespace)
+ for header in self.supported_headers:
+ self.xmpp['xep_0030'].add_feature('%s#%s' % (
+ Headers.namespace,
+ header))
diff --git a/sleekxmpp/plugins/xep_0131/stanza.py b/sleekxmpp/plugins/xep_0131/stanza.py
new file mode 100644
index 00000000..347adf96
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0131/stanza.py
@@ -0,0 +1,51 @@
+"""
+ 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.thirdparty import OrderedDict
+from sleekxmpp.xmlstream import ET, ElementBase
+
+
+class Headers(ElementBase):
+ name = 'headers'
+ namespace = 'http://jabber.org/protocol/shim'
+ plugin_attrib = 'headers'
+ interfaces = set(['headers'])
+ is_extension = True
+
+ def get_headers(self):
+ result = OrderedDict()
+ headers = self.xml.findall('{%s}header' % self.namespace)
+ for header in headers:
+ name = header.attrib.get('name', '')
+ value = header.text
+ if name in result:
+ if not isinstance(result[name], set):
+ result[name] = [result[name]]
+ else:
+ result[name] = []
+ result[name].add(value)
+ else:
+ result[name] = value
+ return result
+
+ def set_headers(self, values):
+ self.del_headers()
+ for name in values:
+ vals = values[name]
+ if not isinstance(vals, (list, set)):
+ vals = [values[name]]
+ for value in vals:
+ header = ET.Element('{%s}header' % self.namespace)
+ header.attrib['name'] = name
+ header.text = value
+ self.xml.append(header)
+
+ def del_headers(self):
+ headers = self.xml.findall('{%s}header' % self.namespace)
+ for header in headers:
+ self.xml.remove(header)
diff --git a/sleekxmpp/plugins/xep_0133.py b/sleekxmpp/plugins/xep_0133.py
new file mode 100644
index 00000000..7bbe4c3c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0133.py
@@ -0,0 +1,54 @@
+"""
+ 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 import BasePlugin, register_plugin
+
+
+class XEP_0133(BasePlugin):
+
+ name = 'xep_0133'
+ description = 'XEP-0133: Service Administration'
+ dependencies = set(['xep_0030', 'xep_0004', 'xep_0050'])
+ commands = set(['add-user', 'delete-user', 'disable-user',
+ 'reenable-user', 'end-user-session', 'get-user-password',
+ 'change-user-password', 'get-user-roster',
+ 'get-user-lastlogin', 'user-stats', 'edit-blacklist',
+ 'edit-whitelist', 'get-registered-users-num',
+ 'get-disabled-users-num', 'get-online-users-num',
+ 'get-active-users-num', 'get-idle-users-num',
+ 'get-registered-users-list', 'get-disabled-users-list',
+ 'get-online-users-list', 'get-online-users',
+ 'get-active-users', 'get-idle-userslist', 'announce',
+ 'set-motd', 'edit-motd', 'delete-motd', 'set-welcome',
+ 'delete-welcome', 'edit-admin', 'restart', 'shutdown'])
+
+ def get_commands(self, jid=None, **kwargs):
+ if jid is None:
+ jid = self.xmpp.boundjid.server
+ return self.xmpp['xep_0050'].get_commands(jid, **kwargs)
+
+
+def create_command(name):
+ def admin_command(self, jid=None, session=None, ifrom=None, block=False):
+ if jid is None:
+ jid = self.xmpp.boundjid.server
+ self.xmpp['xep_0050'].start_command(
+ jid=jid,
+ node='http://jabber.org/protocol/admin#%s' % name,
+ session=session,
+ ifrom=ifrom,
+ block=block)
+ return admin_command
+
+
+for cmd in XEP_0133.commands:
+ setattr(XEP_0133, cmd.replace('-', '_'), create_command(cmd))
+
+
+register_plugin(XEP_0133)
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 1e32595a..ec1ae782 100644
--- a/sleekxmpp/plugins/xep_0153/vcard_avatar.py
+++ b/sleekxmpp/plugins/xep_0153/vcard_avatar.py
@@ -8,11 +8,11 @@
import hashlib
import logging
+import threading
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
@@ -30,11 +30,14 @@ class XEP_0153(BasePlugin):
def plugin_init(self):
self._hashes = {}
+ self._allow_advertising = threading.Event()
+
register_stanza_plugin(Presence, VCardTempUpdate)
self.xmpp.add_filter('out', self._update_presence)
self.xmpp.add_event_handler('session_start', self._start)
+ self.xmpp.add_event_handler('session_end', self._end)
self.xmpp.add_event_handler('presence_available', self._recv_presence)
self.xmpp.add_event_handler('presence_dnd', self._recv_presence)
@@ -44,10 +47,12 @@ class XEP_0153(BasePlugin):
self.api.register(self._set_hash, 'set_hash', default=True)
self.api.register(self._get_hash, 'get_hash', default=True)
+ self.api.register(self._reset_hash, 'reset_hash', default=True)
def plugin_end(self):
self.xmpp.del_filter('out', self._update_presence)
self.xmpp.del_event_handler('session_start', self._start)
+ self.xmpp.del_event_handler('session_end', self._end)
self.xmpp.del_event_handler('presence_available', self._recv_presence)
self.xmpp.del_event_handler('presence_dnd', self._recv_presence)
self.xmpp.del_event_handler('presence_xa', self._recv_presence)
@@ -56,56 +61,87 @@ class XEP_0153(BasePlugin):
def set_avatar(self, jid=None, avatar=None, mtype=None, block=True,
timeout=None, callback=None):
+ if jid is None:
+ jid = self.xmpp.boundjid.bare
+
vcard = self.xmpp['xep_0054'].get_vcard(jid, cached=True)
vcard = vcard['vcard_temp']
vcard['PHOTO']['TYPE'] = mtype
vcard['PHOTO']['BINVAL'] = avatar
+
self.xmpp['xep_0054'].publish_vcard(jid=jid, vcard=vcard)
- self._reset_hash(jid)
+
+ self.api['reset_hash'](jid)
+ self.xmpp.roster[jid].send_last_presence()
def _start(self, event):
- self.xmpp['xep_0054'].get_vcard()
+ 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()
def _update_presence(self, stanza):
if not isinstance(stanza, Presence):
return stanza
+ if stanza['type'] not in ('available', 'dnd', 'chat', 'away', 'xa'):
+ return stanza
+
current_hash = self.api['get_hash'](stanza['from'])
stanza['vcard_temp_update']['photo'] = current_hash
return stanza
- def _reset_hash(self, jid=None):
+ def _reset_hash(self, jid, node, ifrom, args):
own_jid = (jid.bare == self.xmpp.boundjid.bare)
if self.xmpp.is_component:
own_jid = (jid.domain == self.xmpp.boundjid.domain)
- if jid is not None:
- jid = jid.bare
self.api['set_hash'](jid, args=None)
if own_jid:
self.xmpp.roster[jid].send_last_presence()
- iq = self.xmpp['xep_0054'].get_vcard(
- jid=jid,
- ifrom=self.xmpp.boundjid)
- data = iq['vcard_temp']['PHOTO']['BINVAL']
- if not data:
- new_hash = ''
- else:
- new_hash = hashlib.sha1(data).hexdigest()
- self.api['set_hash'](jid, args=new_hash)
- if own_jid:
- self.xmpp.roster[jid].send_last_presence()
+ try:
+ iq = self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom)
+
+ data = iq['vcard_temp']['PHOTO']['BINVAL']
+ if not data:
+ new_hash = ''
+ else:
+ new_hash = hashlib.sha1(data).hexdigest()
+
+ self.api['set_hash'](jid, args=new_hash)
+ except XMPPError:
+ 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
+
data = pres['vcard_temp_update']['photo']
if data is None:
return
- elif data == '' or data != self.api['get_hash'](pres['to']):
- self._reset_hash(pres['from'])
+ 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 044fa83f..3e97d8db 100644
--- a/sleekxmpp/plugins/xep_0184/receipt.py
+++ b/sleekxmpp/plugins/xep_0184/receipt.py
@@ -26,13 +26,14 @@ class XEP_0184(BasePlugin):
description = 'XEP-0184: Message Delivery Receipts'
dependencies = set(['xep_0030'])
stanza = stanza
+ default_config = {
+ 'auto_ack': True,
+ 'auto_request': False
+ }
ack_types = ('normal', 'chat', 'headline')
def plugin_init(self):
- self.auto_ack = self.config.get('auto_ack', True)
- self.auto_request = self.config.get('auto_request', False)
-
register_stanza_plugin(Message, Request)
register_stanza_plugin(Message, Received)
@@ -68,7 +69,7 @@ class XEP_0184(BasePlugin):
ack['to'] = msg['from']
ack['from'] = msg['to']
ack['receipt'] = msg['id']
- ack['id'] = self.xmpp.new_id()
+ ack['id'] = msg['id']
ack.send()
def _handle_receipt_received(self, msg):
@@ -117,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_0191/blocking.py b/sleekxmpp/plugins/xep_0191/blocking.py
index 0d903acc..57632319 100644
--- a/sleekxmpp/plugins/xep_0191/blocking.py
+++ b/sleekxmpp/plugins/xep_0191/blocking.py
@@ -22,7 +22,7 @@ log = logging.getLogger(__name__)
class XEP_0191(BasePlugin):
name = 'xep_0191'
- description = 'XEP-0191: Simple Communications Blocking'
+ description = 'XEP-0191: Blocking Command'
dependencies = set(['xep_0030'])
stanza = stanza
@@ -48,7 +48,7 @@ class XEP_0191(BasePlugin):
def get_blocked(self, ifrom=None, block=True, timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
- iq['from'] = 'ifrom'
+ iq['from'] = ifrom
iq.enable('blocklist')
return iq.send(block=block, timeout=timeout, callback=callback)
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_0198/stream_management.py b/sleekxmpp/plugins/xep_0198/stream_management.py
index a150ad39..48029913 100644
--- a/sleekxmpp/plugins/xep_0198/stream_management.py
+++ b/sleekxmpp/plugins/xep_0198/stream_management.py
@@ -34,39 +34,44 @@ class XEP_0198(BasePlugin):
description = 'XEP-0198: Stream Management'
dependencies = set()
stanza = stanza
+ default_config = {
+ #: The last ack number received from the server.
+ 'last_ack': 0,
- def plugin_init(self):
- """Start the XEP-0198 plugin."""
-
- # Only enable stream management for non-components,
- # since components do not yet perform feature negotiation.
- if self.xmpp.is_component:
- return
+ #: The number of stanzas to wait between sending ack requests to
+ #: the server. Setting this to ``1`` will send an ack request after
+ #: every sent stanza. Defaults to ``5``.
+ 'window': 5,
#: The stream management ID for the stream. Knowing this value is
#: required in order to do stream resumption.
- self.sm_id = self.config.get('sm_id', None)
+ 'sm_id': None,
#: A counter of handled incoming stanzas, mod 2^32.
- self.handled = self.config.get('handled', 0)
+ 'handled': 0,
#: A counter of unacked outgoing stanzas, mod 2^32.
- self.seq = self.config.get('seq', 0)
+ 'seq': 0,
- #: The last ack number received from the server.
- self.last_ack = self.config.get('last_ack', 0)
+ #: Control whether or not the ability to resume the stream will be
+ #: requested when enabling stream management. Defaults to ``True``.
+ 'allow_resume': True,
+
+ 'order': 10100,
+ 'resume_order': 9000
+ }
+
+ def plugin_init(self):
+ """Start the XEP-0198 plugin."""
+
+ # Only enable stream management for non-components,
+ # since components do not yet perform feature negotiation.
+ if self.xmpp.is_component:
+ return
- #: The number of stanzas to wait between sending ack requests to
- #: the server. Setting this to ``1`` will send an ack request after
- #: every sent stanza. Defaults to ``5``.
- self.window = self.config.get('window', 5)
self.window_counter = self.window
self.window_counter_lock = threading.Lock()
- #: Control whether or not the ability to resume the stream will be
- #: requested when enabling stream management. Defaults to ``True``.
- self.allow_resume = self.config.get('allow_resume', True)
-
self.enabled = threading.Event()
self.unacked_queue = collections.deque()
@@ -92,11 +97,11 @@ class XEP_0198(BasePlugin):
self.xmpp.register_feature('sm',
self._handle_sm_feature,
restart=True,
- order=self.config.get('order', 10100))
+ order=self.order)
self.xmpp.register_feature('sm',
self._handle_sm_feature,
restart=True,
- order=self.config.get('resume_order', 9000))
+ order=self.resume_order)
self.xmpp.register_handler(
Callback('Stream Management Enabled',
@@ -137,8 +142,8 @@ class XEP_0198(BasePlugin):
if self.xmpp.is_component:
return
- self.xmpp.unregister_feature('sm', self.config.get('order', 10100))
- self.xmpp.unregister_feature('sm', self.config.get('resume_order', 9000))
+ self.xmpp.unregister_feature('sm', self.order)
+ self.xmpp.unregister_feature('sm', self.resume_order)
self.xmpp.del_event_handler('session_end', self.session_end)
self.xmpp.del_filter('in', self._handle_incoming)
self.xmpp.del_filter('out_sync', self._handle_outgoing)
diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py
index b9d145aa..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.
@@ -51,14 +51,16 @@ class XEP_0199(BasePlugin):
description = 'XEP-0199: XMPP Ping'
dependencies = set(['xep_0030'])
stanza = stanza
+ default_config = {
+ 'keepalive': False,
+ 'interval': 300,
+ 'timeout': 30
+ }
def plugin_init(self):
"""
Start the XEP-0199 plugin.
"""
- self.keepalive = self.config.get('keepalive', False)
- self.frequency = float(self.config.get('frequency', 300))
- self.timeout = self.config.get('timeout', 30)
register_stanza_plugin(Iq, Ping)
@@ -69,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()
@@ -159,21 +143,44 @@ class XEP_0199(BasePlugin):
iq['from'] = ifrom
iq.enable('ping')
- start_time = time.clock()
-
- try:
- resp = iq.send(block=block,
- timeout=timeout,
- callback=callback)
- except IqError as err:
- resp = err.iq
+ return iq.send(block=block, timeout=timeout, callback=callback)
- end_time = time.clock()
+ def ping(self, jid=None, ifrom=None, timeout=None):
+ """Send a ping request and calculate RTT.
- 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 50af4730..d5b3af37 100644
--- a/sleekxmpp/plugins/xep_0202/time.py
+++ b/sleekxmpp/plugins/xep_0202/time.py
@@ -30,24 +30,25 @@ class XEP_0202(BasePlugin):
description = 'XEP-0202: Entity Time'
dependencies = set(['xep_0030', 'xep_0082'])
stanza = stanza
+ default_config = {
+ #: As a default, respond to time requests with the
+ #: local time returned by XEP-0082. However, a
+ #: custom function can be supplied which accepts
+ #: the JID of the entity to query for the time.
+ 'local_time': None,
+ 'tz_offset': 0
+ }
def plugin_init(self):
"""Start the XEP-0203 plugin."""
- self.tz_offset = self.config.get('tz_offset', 0)
-
- # As a default, respond to time requests with the
- # local time returned by XEP-0082. However, a
- # custom function can be supplied which accepts
- # the JID of the entity to query for the time.
- self.local_time = self.config.get('local_time', None)
-
- def default_local_time(jid):
- return xep_0082.datetime(offset=self.tz_offset)
if not self.local_time:
+ def default_local_time(jid):
+ return xep_0082.datetime(offset=self.tz_offset)
+
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 baae4cd3..e147e975 100644
--- a/sleekxmpp/plugins/xep_0203/stanza.py
+++ b/sleekxmpp/plugins/xep_0203/stanza.py
@@ -8,23 +8,28 @@
import datetime as dt
+from sleekxmpp.jid import JID
from sleekxmpp.xmlstream import ElementBase
from sleekxmpp.plugins import xep_0082
class Delay(ElementBase):
- """
- """
-
name = 'delay'
namespace = 'urn:xmpp:delay'
plugin_attrib = 'delay'
interfaces = set(('from', 'stamp', 'text'))
+ def get_from(self):
+ 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 724ef968..2cc7f703 100644
--- a/sleekxmpp/plugins/xep_0222.py
+++ b/sleekxmpp/plugins/xep_0222.py
@@ -22,7 +22,7 @@ class XEP_0222(BasePlugin):
"""
name = 'xep_0222'
- description = 'XEP-0222: Persistent Storage of Private Data via PubSub'
+ description = 'XEP-0222: Persistent Storage of Public Data via PubSub'
dependencies = set(['xep_0163', 'xep_0060', 'xep_0004'])
profile = {'pubsub#persist_items': True,
@@ -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_0235/__init__.py b/sleekxmpp/plugins/xep_0235/__init__.py
new file mode 100644
index 00000000..29d4408a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0235/__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_0235 import stanza
+from sleekxmpp.plugins.xep_0235.stanza import OAuth
+from sleekxmpp.plugins.xep_0235.oauth import XEP_0235
+
+
+register_plugin(XEP_0235)
diff --git a/sleekxmpp/plugins/xep_0235/oauth.py b/sleekxmpp/plugins/xep_0235/oauth.py
new file mode 100644
index 00000000..df0e2ebf
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0235/oauth.py
@@ -0,0 +1,32 @@
+"""
+ 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.
+"""
+
+
+import logging
+
+from sleekxmpp import Message
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0235 import stanza, OAuth
+
+
+class XEP_0235(BasePlugin):
+
+ name = 'xep_0235'
+ description = 'XEP-0235: OAuth Over XMPP'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, OAuth)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:oauth:0')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:oauth:0')
diff --git a/sleekxmpp/plugins/xep_0235/stanza.py b/sleekxmpp/plugins/xep_0235/stanza.py
new file mode 100644
index 00000000..0050d583
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0235/stanza.py
@@ -0,0 +1,80 @@
+"""
+ 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.
+"""
+
+import hmac
+import hashlib
+import urllib
+import base64
+
+from sleekxmpp.xmlstream import ET, ElementBase, JID
+
+
+class OAuth(ElementBase):
+
+ name = 'oauth'
+ namespace = 'urn:xmpp:oauth:0'
+ plugin_attrib = 'oauth'
+ interfaces = set(['oauth_consumer_key', 'oauth_nonce', 'oauth_signature',
+ 'oauth_signature_method', 'oauth_timestamp',
+ 'oauth_token', 'oauth_version'])
+ sub_interfaces = interfaces
+
+ def generate_signature(self, stanza, sfrom, sto, consumer_secret,
+ token_secret, method='HMAC-SHA1'):
+ self['oauth_signature_method'] = method
+
+ request = urllib.quote('%s&%s' % (sfrom, sto), '')
+ parameters = urllib.quote('&'.join([
+ 'oauth_consumer_key=%s' % self['oauth_consumer_key'],
+ 'oauth_nonce=%s' % self['oauth_nonce'],
+ 'oauth_signature_method=%s' % self['oauth_signature_method'],
+ 'oauth_timestamp=%s' % self['oauth_timestamp'],
+ 'oauth_token=%s' % self['oauth_token'],
+ 'oauth_version=%s' % self['oauth_version']
+ ]), '')
+
+ sigbase = '%s&%s&%s' % (stanza, request, parameters)
+
+ consumer_secret = urllib.quote(consumer_secret, '')
+ token_secret = urllib.quote(token_secret, '')
+ key = '%s&%s' % (consumer_secret, token_secret)
+
+ if method == 'HMAC-SHA1':
+ sig = base64.b64encode(hmac.new(key, sigbase, hashlib.sha1).digest())
+ elif method == 'PLAINTEXT':
+ sig = key
+
+ self['oauth_signature'] = sig
+ return sig
+
+ def verify_signature(self, stanza, sfrom, sto, consumer_secret,
+ token_secret):
+ method = self['oauth_signature_method']
+
+ request = urllib.quote('%s&%s' % (sfrom, sto), '')
+ parameters = urllib.quote('&'.join([
+ 'oauth_consumer_key=%s' % self['oauth_consumer_key'],
+ 'oauth_nonce=%s' % self['oauth_nonce'],
+ 'oauth_signature_method=%s' % self['oauth_signature_method'],
+ 'oauth_timestamp=%s' % self['oauth_timestamp'],
+ 'oauth_token=%s' % self['oauth_token'],
+ 'oauth_version=%s' % self['oauth_version']
+ ]), '')
+
+ sigbase = '%s&%s&%s' % (stanza, request, parameters)
+
+ consumer_secret = urllib.quote(consumer_secret, '')
+ token_secret = urllib.quote(token_secret, '')
+ key = '%s&%s' % (consumer_secret, token_secret)
+
+ if method == 'HMAC-SHA1':
+ sig = base64.b64encode(hmac.new(key, sigbase, hashlib.sha1).digest())
+ elif method == 'PLAINTEXT':
+ sig = key
+
+ return self['oauth_signature'] == sig
diff --git a/sleekxmpp/plugins/xep_0242.py b/sleekxmpp/plugins/xep_0242.py
new file mode 100644
index 00000000..c1bada27
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0242.py
@@ -0,0 +1,21 @@
+"""
+ 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 import BasePlugin, register_plugin
+
+
+class XEP_0242(BasePlugin):
+
+ name = 'xep_0242'
+ description = 'XEP-0242: XMPP Client Compliance 2009'
+ dependencies = set(['xep_0030', 'xep_0115', 'xep_0054',
+ 'xep_0045', 'xep_0085', 'xep_0016',
+ 'xep_0191'])
+
+
+register_plugin(XEP_0242)
diff --git a/sleekxmpp/plugins/xep_0256.py b/sleekxmpp/plugins/xep_0256.py
index dd407fff..0db8ea3b 100644
--- a/sleekxmpp/plugins/xep_0256.py
+++ b/sleekxmpp/plugins/xep_0256.py
@@ -25,10 +25,11 @@ class XEP_0256(BasePlugin):
description = 'XEP-0256: Last Activity in Presence'
dependencies = set(['xep_0012'])
stanza = stanza
+ default_config = {
+ 'auto_last_activity': False
+ }
def plugin_init(self):
- self.auto_last_activity = self.config.get('auto_last_activity', False)
-
register_stanza_plugin(Presence, LastActivity)
self.xmpp.add_filter('out', self._initial_presence_activity)
diff --git a/sleekxmpp/plugins/xep_0257/__init__.py b/sleekxmpp/plugins/xep_0257/__init__.py
new file mode 100644
index 00000000..8c5311fd
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0257/__init__.py
@@ -0,0 +1,17 @@
+"""
+ 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_0257 import stanza
+from sleekxmpp.plugins.xep_0257.stanza import Certs, AppendCert
+from sleekxmpp.plugins.xep_0257.stanza import DisableCert, RevokeCert
+from sleekxmpp.plugins.xep_0257.client_cert_management import XEP_0257
+
+
+register_plugin(XEP_0257)
diff --git a/sleekxmpp/plugins/xep_0257/client_cert_management.py b/sleekxmpp/plugins/xep_0257/client_cert_management.py
new file mode 100644
index 00000000..49317843
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0257/client_cert_management.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.
+"""
+
+import logging
+
+from sleekxmpp import Iq
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0257 import stanza, Certs
+from sleekxmpp.plugins.xep_0257 import AppendCert, DisableCert, RevokeCert
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0257(BasePlugin):
+
+ name = 'xep_0257'
+ description = 'XEP-0258: Client Certificate Management for SASL EXTERNAL'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, Certs)
+ register_stanza_plugin(Iq, AppendCert)
+ register_stanza_plugin(Iq, DisableCert)
+ register_stanza_plugin(Iq, RevokeCert)
+
+ def get_certs(self, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ iq.enable('sasl_certs')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def add_cert(self, name, cert, allow_management=True, ifrom=None,
+ block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['sasl_cert_append']['name'] = name
+ iq['sasl_cert_append']['x509cert'] = cert
+ iq['sasl_cert_append']['cert_management'] = allow_management
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def disable_cert(self, name, ifrom=None, block=True,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['sasl_cert_disable']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def revoke_cert(self, name, ifrom=None, block=True,
+ timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq['sasl_cert_revoke']['name'] = name
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0257/stanza.py b/sleekxmpp/plugins/xep_0257/stanza.py
new file mode 100644
index 00000000..c3c41db2
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0257/stanza.py
@@ -0,0 +1,87 @@
+"""
+ 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, register_stanza_plugin
+
+
+class Certs(ElementBase):
+ name = 'items'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'sasl_certs'
+ interfaces = set()
+
+
+class CertItem(ElementBase):
+ name = 'item'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'item'
+ plugin_multi_attrib = 'items'
+ interfaces = set(['name', 'x509cert', 'users'])
+ sub_interfaces = set(['name', 'x509cert'])
+
+ def get_users(self):
+ resources = self.xml.findall('{%s}users/{%s}resource' % (
+ self.namespace, self.namespace))
+ return set([res.text for res in resources])
+
+ def set_users(self, values):
+ users = self.xml.find('{%s}users' % self.namespace)
+ if users is None:
+ users = ET.Element('{%s}users' % self.namespace)
+ self.xml.append(users)
+ for resource in values:
+ res = ET.Element('{%s}resource' % self.namespace)
+ res.text = resource
+ users.append(res)
+
+ def del_users(self):
+ users = self.xml.find('{%s}users' % self.namespace)
+ if users is not None:
+ self.xml.remove(users)
+
+
+class AppendCert(ElementBase):
+ name = 'append'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'sasl_cert_append'
+ interfaces = set(['name', 'x509cert', 'cert_management'])
+ sub_interfaces = set(['name', 'x509cert'])
+
+ def get_cert_management(self):
+ manage = self.xml.find('{%s}no-cert-management' % self.namespace)
+ return manage is None
+
+ def set_cert_management(self, value):
+ self.del_cert_management()
+ if not value:
+ manage = ET.Element('{%s}no-cert-management' % self.namespace)
+ self.xml.append(manage)
+
+ def del_cert_management(self):
+ manage = self.xml.find('{%s}no-cert-management' % self.namespace)
+ if manage is not None:
+ self.xml.remove(manage)
+
+
+class DisableCert(ElementBase):
+ name = 'disable'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'sasl_cert_disable'
+ interfaces = set(['name'])
+ sub_interfaces = interfaces
+
+
+class RevokeCert(ElementBase):
+ name = 'revoke'
+ namespace = 'urn:xmpp:saslcert:1'
+ plugin_attrib = 'sasl_cert_revoke'
+ interfaces = set(['name'])
+ sub_interfaces = interfaces
+
+
+register_stanza_plugin(Certs, CertItem, iterable=True)
diff --git a/sleekxmpp/plugins/xep_0258/stanza.py b/sleekxmpp/plugins/xep_0258/stanza.py
index 4d828a46..a506064b 100644
--- a/sleekxmpp/plugins/xep_0258/stanza.py
+++ b/sleekxmpp/plugins/xep_0258/stanza.py
@@ -8,8 +8,7 @@
from base64 import b64encode, b64decode
-from sleekxmpp.thirdparty.suelta.util import bytes
-
+from sleekxmpp.util import bytes
from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin
diff --git a/sleekxmpp/plugins/xep_0279/__init__.py b/sleekxmpp/plugins/xep_0279/__init__.py
new file mode 100644
index 00000000..93db9e7c
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0279/__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_0279 import stanza
+from sleekxmpp.plugins.xep_0279.stanza import IPCheck
+from sleekxmpp.plugins.xep_0279.ipcheck import XEP_0279
+
+
+register_plugin(XEP_0279)
diff --git a/sleekxmpp/plugins/xep_0279/ipcheck.py b/sleekxmpp/plugins/xep_0279/ipcheck.py
new file mode 100644
index 00000000..f8c167c7
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0279/ipcheck.py
@@ -0,0 +1,39 @@
+"""
+ 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.
+"""
+
+
+import logging
+
+from sleekxmpp import Iq
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins.xep_0279 import stanza, IPCheck
+
+
+class XEP_0279(BasePlugin):
+
+ name = 'xep_0279'
+ description = 'XEP-0279: Server IP Check'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Iq, IPCheck)
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:sic:0')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:sic:0')
+
+ def check_ip(self, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['from'] = ifrom
+ iq.enable('ip_check')
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0279/stanza.py b/sleekxmpp/plugins/xep_0279/stanza.py
new file mode 100644
index 00000000..181b5957
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0279/stanza.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.xmlstream import ElementBase
+
+
+class IPCheck(ElementBase):
+
+ name = 'ip'
+ namespace = 'urn:xmpp:sic:0'
+ plugin_attrib = 'ip_check'
+ interfaces = set(['ip_check'])
+ is_extension = True
+
+ def get_ip_check(self):
+ return self.xml.text
+
+ def set_ip_check(self, value):
+ if value:
+ self.xml.text = value
+ else:
+ self.xml.text = ''
+
+ def del_ip_check(self):
+ self.xml.text = ''
diff --git a/sleekxmpp/plugins/xep_0280/__init__.py b/sleekxmpp/plugins/xep_0280/__init__.py
new file mode 100644
index 00000000..929321af
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0280/__init__.py
@@ -0,0 +1,17 @@
+"""
+ 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_0280.stanza import ReceivedCarbon, SentCarbon
+from sleekxmpp.plugins.xep_0280.stanza import PrivateCarbon
+from sleekxmpp.plugins.xep_0280.stanza import CarbonEnable, CarbonDisable
+from sleekxmpp.plugins.xep_0280.carbons import XEP_0280
+
+
+register_plugin(XEP_0280)
diff --git a/sleekxmpp/plugins/xep_0280/carbons.py b/sleekxmpp/plugins/xep_0280/carbons.py
new file mode 100644
index 00000000..482d046a
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0280/carbons.py
@@ -0,0 +1,81 @@
+"""
+ 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
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp.stanza import Message, 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.xep_0280 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0280(BasePlugin):
+
+ """
+ XEP-0280 Message Carbons
+ """
+
+ name = 'xep_0280'
+ description = 'XEP-0280: Message Carbons'
+ dependencies = set(['xep_0030', 'xep_0297'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('Carbon Received',
+ StanzaPath('message/carbon_received'),
+ self._handle_carbon_received))
+ self.xmpp.register_handler(
+ Callback('Carbon Sent',
+ StanzaPath('message/carbon_sent'),
+ self._handle_carbon_sent))
+
+ register_stanza_plugin(Message, stanza.ReceivedCarbon)
+ register_stanza_plugin(Message, stanza.SentCarbon)
+ register_stanza_plugin(Message, stanza.PrivateCarbon)
+ register_stanza_plugin(Iq, stanza.CarbonEnable)
+ register_stanza_plugin(Iq, stanza.CarbonDisable)
+
+ register_stanza_plugin(stanza.ReceivedCarbon,
+ self.xmpp['xep_0297'].stanza.Forwarded)
+ register_stanza_plugin(stanza.SentCarbon,
+ self.xmpp['xep_0297'].stanza.Forwarded)
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Carbon Received')
+ self.xmpp.remove_handler('Carbon Sent')
+ self.xmpp.plugin['xep_0030'].del_feature(feature='urn:xmpp:carbons:2')
+
+ def session_bind(self, jid):
+ self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:carbons:2')
+
+ def _handle_carbon_received(self, msg):
+ self.xmpp.event('carbon_received', msg)
+
+ def _handle_carbon_sent(self, msg):
+ self.xmpp.event('carbon_sent', msg)
+
+ def enable(self, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq.enable('carbon_enable')
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def disable(self, ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['from'] = ifrom
+ iq.enable('carbon_disable')
+ return iq.send(block=block, timeout=timeout, callback=callback)
diff --git a/sleekxmpp/plugins/xep_0280/stanza.py b/sleekxmpp/plugins/xep_0280/stanza.py
new file mode 100644
index 00000000..2f3aad86
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0280/stanza.py
@@ -0,0 +1,64 @@
+"""
+ 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.xmlstream import ElementBase
+
+
+class ReceivedCarbon(ElementBase):
+ name = 'received'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_received'
+ interfaces = set(['carbon_received'])
+ is_extension = True
+
+ def get_carbon_received(self):
+ return self['forwarded']['stanza']
+
+ def del_carbon_received(self):
+ del self['forwarded']['stanza']
+
+ def set_carbon_received(self, stanza):
+ self['forwarded']['stanza'] = stanza
+
+
+class SentCarbon(ElementBase):
+ name = 'sent'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_sent'
+ interfaces = set(['carbon_sent'])
+ is_extension = True
+
+ def get_carbon_sent(self):
+ return self['forwarded']['stanza']
+
+ def del_carbon_sent(self):
+ del self['forwarded']['stanza']
+
+ def set_carbon_sent(self, stanza):
+ self['forwarded']['stanza'] = stanza
+
+
+class PrivateCarbon(ElementBase):
+ name = 'private'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_private'
+ interfaces = set()
+
+
+class CarbonEnable(ElementBase):
+ name = 'enable'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_enable'
+ interfaces = set()
+
+
+class CarbonDisable(ElementBase):
+ name = 'disable'
+ namespace = 'urn:xmpp:carbons:2'
+ plugin_attrib = 'carbon_disable'
+ interfaces = set()
diff --git a/sleekxmpp/plugins/xep_0297/__init__.py b/sleekxmpp/plugins/xep_0297/__init__.py
new file mode 100644
index 00000000..551d9420
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0297/__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_0297 import stanza
+from sleekxmpp.plugins.xep_0297.stanza import Forwarded
+from sleekxmpp.plugins.xep_0297.forwarded import XEP_0297
+
+
+register_plugin(XEP_0297)
diff --git a/sleekxmpp/plugins/xep_0297/forwarded.py b/sleekxmpp/plugins/xep_0297/forwarded.py
new file mode 100644
index 00000000..95703a2d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0297/forwarded.py
@@ -0,0 +1,64 @@
+"""
+ 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.
+"""
+
+
+import logging
+
+from sleekxmpp import Iq, Message, 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_0297 import stanza, Forwarded
+
+
+class XEP_0297(BasePlugin):
+
+ name = 'xep_0297'
+ description = 'XEP-0297: Stanza Forwarding'
+ dependencies = set(['xep_0030', 'xep_0203'])
+ stanza = stanza
+
+ def plugin_init(self):
+ register_stanza_plugin(Message, Forwarded)
+
+ # While these are marked as iterable, that is just for
+ # making it easier to extract the forwarded stanza. There
+ # still can be only a single forwarded stanza.
+ register_stanza_plugin(Forwarded, Message, iterable=True)
+ register_stanza_plugin(Forwarded, Presence, iterable=True)
+ register_stanza_plugin(Forwarded, Iq, iterable=True)
+
+ register_stanza_plugin(Forwarded, self.xmpp['xep_0203'].stanza.Delay)
+
+ self.xmpp.register_handler(
+ Callback('Forwarded Stanza',
+ StanzaPath('message/forwarded'),
+ self._handle_forwarded))
+
+ def session_bind(self, jid):
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:forward:0')
+
+ def plugin_end(self):
+ self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:forward:0')
+ self.xmpp.remove_handler('Forwarded Stanza')
+
+ def forward(self, stanza=None, mto=None, mbody=None, mfrom=None, delay=None):
+ stanza.stream = None
+
+ msg = self.xmpp.Message()
+ msg['to'] = mto
+ msg['from'] = mfrom
+ msg['body'] = mbody
+ msg['forwarded']['stanza'] = stanza
+ if delay is not None:
+ msg['forwarded']['delay']['stamp'] = delay
+ msg.send()
+
+ def _handle_forwarded(self, msg):
+ self.xmpp.event('forwarded_stanza', msg)
diff --git a/sleekxmpp/plugins/xep_0297/stanza.py b/sleekxmpp/plugins/xep_0297/stanza.py
new file mode 100644
index 00000000..8b97accc
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0297/stanza.py
@@ -0,0 +1,36 @@
+"""
+ 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, Presence, Iq
+from sleekxmpp.xmlstream import ElementBase
+
+
+class Forwarded(ElementBase):
+ name = 'forwarded'
+ namespace = 'urn:xmpp:forward:0'
+ plugin_attrib = 'forwarded'
+ interfaces = set(['stanza'])
+
+ def get_stanza(self):
+ for stanza in self:
+ if isinstance(stanza, (Message, Presence, Iq)):
+ return stanza
+ return ''
+
+ def set_stanza(self, value):
+ self.del_stanza()
+ self.append(value)
+
+ def del_stanza(self):
+ found_stanzas = []
+ for stanza in self:
+ if isinstance(stanza, (Message, Presence, Iq)):
+ found_stanzas.append(stanza)
+ for stanza in found_stanzas:
+ self.iterables.remove(stanza)
+ self.xml.remove(stanza.xml)
diff --git a/sleekxmpp/plugins/xep_0308/__init__.py b/sleekxmpp/plugins/xep_0308/__init__.py
new file mode 100644
index 00000000..a6a100ee
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0308/__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_0308.stanza import Replace
+from sleekxmpp.plugins.xep_0308.correction import XEP_0308
+
+
+register_plugin(XEP_0308)
diff --git a/sleekxmpp/plugins/xep_0308/correction.py b/sleekxmpp/plugins/xep_0308/correction.py
new file mode 100644
index 00000000..d32b4bc4
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0308/correction.py
@@ -0,0 +1,52 @@
+"""
+ 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
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp.stanza import 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.xep_0308 import stanza, Replace
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0308(BasePlugin):
+
+ """
+ XEP-0308 Last Message Correction
+ """
+
+ name = 'xep_0308'
+ description = 'XEP-0308: Last Message Correction'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
+
+ def plugin_init(self):
+ self.xmpp.register_handler(
+ Callback('Message Correction',
+ StanzaPath('message/replace'),
+ self._handle_correction))
+
+ register_stanza_plugin(Message, Replace)
+
+ self.xmpp.use_message_ids = True
+
+ def plugin_end(self):
+ self.xmpp.remove_handler('Message Correction')
+ self.xmpp.plugin['xep_0030'].del_feature(feature=Replace.namespace)
+
+ def session_bind(self, jid):
+ self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace)
+
+ def _handle_correction(self, msg):
+ self.xmpp.event('message_correction', msg)
diff --git a/sleekxmpp/plugins/xep_0308/stanza.py b/sleekxmpp/plugins/xep_0308/stanza.py
new file mode 100644
index 00000000..8f88cbc0
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0308/stanza.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 permissio
+"""
+
+from sleekxmpp.xmlstream import ElementBase
+
+
+class Replace(ElementBase):
+ name = 'replace'
+ namespace = 'urn:xmpp:message-correct:0'
+ plugin_attrib = 'replace'
+ interfaces = set(['id'])
diff --git a/sleekxmpp/plugins/xep_0313/__init__.py b/sleekxmpp/plugins/xep_0313/__init__.py
new file mode 100644
index 00000000..8b6ed97d
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0313/__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_0313.stanza import Result, MAM, Preferences
+from sleekxmpp.plugins.xep_0313.mam import XEP_0313
+
+
+register_plugin(XEP_0313)
diff --git a/sleekxmpp/plugins/xep_0313/mam.py b/sleekxmpp/plugins/xep_0313/mam.py
new file mode 100644
index 00000000..4b82ca03
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0313/mam.py
@@ -0,0 +1,94 @@
+"""
+ 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
+"""
+
+import logging
+
+import sleekxmpp
+from sleekxmpp.stanza import Message, Iq
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.xmlstream.handler import Collector
+from sleekxmpp.xmlstream.matcher import StanzaPath
+from sleekxmpp.xmlstream import register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0313 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class XEP_0313(BasePlugin):
+
+ """
+ XEP-0313 Message Archive Management
+ """
+
+ name = 'xep_0313'
+ description = 'XEP-0313: Message Archive Management'
+ dependencies = set(['xep_0030', 'xep_0050', 'xep_0059', 'xep_0297'])
+ stanza = stanza
+
+ def plugin_init(self):
+ 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,
+ block=True, timeout=None, callback=None, iterator=False):
+ iq = self.xmpp.Iq()
+ query_id = iq['id']
+
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['type'] = 'get'
+ iq['mam']['queryid'] = query_id
+ iq['mam']['start'] = start
+ iq['mam']['end'] = end
+ iq['mam']['with'] = with_jid
+
+ collector = Collector(
+ 'MAM_Results_%s' % query_id,
+ StanzaPath('message/mam_result@queryid=%s' % query_id))
+ self.xmpp.register_handler(collector)
+
+ if iterator:
+ return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results')
+ elif not block and callback is not None:
+ def wrapped_cb(iq):
+ results = collector.stop()
+ if iq['type'] == 'result':
+ iq['mam']['results'] = results
+ callback(iq)
+ return iq.send(block=block, timeout=timeout, callback=wrapped_cb)
+ else:
+ try:
+ resp = iq.send(block=block, timeout=timeout, callback=callback)
+ resp['mam']['results'] = collector.stop()
+ return resp
+ except XMPPError as e:
+ collector.stop()
+ raise e
+
+ def set_preferences(self, jid=None, default=None, always=None, never=None,
+ ifrom=None, block=True, timeout=None, callback=None):
+ iq = self.xmpp.Iq()
+ iq['type'] = 'set'
+ iq['to'] = jid
+ iq['from'] = ifrom
+ iq['mam_prefs']['default'] = default
+ iq['mam_prefs']['always'] = always
+ iq['mam_prefs']['never'] = never
+ return iq.send(block=block, timeout=timeout, callback=callback)
+
+ def get_configuration_commands(self, jid, **kwargs):
+ return self.xmpp['xep_0030'].get_items(
+ jid=jid,
+ node='urn:xmpp:mam#configure',
+ **kwargs)
diff --git a/sleekxmpp/plugins/xep_0313/stanza.py b/sleekxmpp/plugins/xep_0313/stanza.py
new file mode 100644
index 00000000..81576cd4
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0313/stanza.py
@@ -0,0 +1,139 @@
+"""
+ 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
+"""
+
+import datetime as dt
+
+from sleekxmpp.jid import JID
+from sleekxmpp.xmlstream import ElementBase, ET
+from sleekxmpp.plugins import xep_0082
+
+
+class MAM(ElementBase):
+ name = 'query'
+ namespace = 'urn:xmpp:mam:tmp'
+ plugin_attrib = 'mam'
+ interfaces = set(['queryid', 'start', 'end', 'with', 'results'])
+ sub_interfaces = set(['start', 'end', 'with'])
+
+ def setup(self, xml=None):
+ ElementBase.setup(self, xml)
+ self._results = []
+
+ def get_start(self):
+ 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_sub_text('start', value)
+
+ def get_end(self):
+ timestamp = self._get_sub_text('end')
+ return xep_0082.parse(timestamp)
+
+ def set_end(self, value):
+ if isinstance(value, dt.datetime):
+ value = xep_0082.format_datetime(value)
+ self._set_sub_text('end', value)
+
+ def get_with(self):
+ return JID(self._get_sub_text('with'))
+
+ def set_with(self, value):
+ self._set_sub_text('with', str(value))
+
+ # The results interface is meant only as an easy
+ # way to access the set of collected message responses
+ # from the query.
+
+ def get_results(self):
+ return self._results
+
+ def set_results(self, values):
+ self._results = values
+
+ def del_results(self):
+ self._results = []
+
+
+class Preferences(ElementBase):
+ name = 'prefs'
+ namespace = 'urn:xmpp:mam:tmp'
+ plugin_attrib = 'mam_prefs'
+ interfaces = set(['default', 'always', 'never'])
+ sub_interfaces = set(['always', 'never'])
+
+ def get_always(self):
+ results = set()
+
+ jids = self.xml.findall('{%s}always/{%s}jid' % (
+ self.namespace, self.namespace))
+
+ for jid in jids:
+ results.add(JID(jid.text))
+
+ return results
+
+ def set_always(self, value):
+ self._set_sub_text('always', '', keep=True)
+ always = self.xml.find('{%s}always' % self.namespace)
+ always.clear()
+
+ if not isinstance(value, (list, set)):
+ value = [value]
+
+ for jid in value:
+ jid_xml = ET.Element('{%s}jid' % self.namespace)
+ jid_xml.text = str(jid)
+ always.append(jid_xml)
+
+ def get_never(self):
+ results = set()
+
+ jids = self.xml.findall('{%s}never/{%s}jid' % (
+ self.namespace, self.namespace))
+
+ for jid in jids:
+ results.add(JID(jid.text))
+
+ return results
+
+ def set_never(self, value):
+ self._set_sub_text('never', '', keep=True)
+ never = self.xml.find('{%s}never' % self.namespace)
+ never.clear()
+
+ if not isinstance(value, (list, set)):
+ value = [value]
+
+ for jid in value:
+ jid_xml = ET.Element('{%s}jid' % self.namespace)
+ jid_xml.text = str(jid)
+ never.append(jid_xml)
+
+
+class Result(ElementBase):
+ name = 'result'
+ namespace = 'urn:xmpp:mam:tmp'
+ plugin_attrib = 'mam_result'
+ interfaces = set(['queryid', 'id'])
+
+
+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)
+