Rename to slixmpp

This commit is contained in:
Florent Le Coz
2014-07-17 14:19:04 +02:00
parent e5582694c0
commit 5ab77c7452
514 changed files with 3473 additions and 3530 deletions
+86
View File
@@ -0,0 +1,86 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin
from slixmpp.plugins.base import register_plugin, load_plugin
__all__ = [
# 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', # Blocking Command
'xep_0196', # User Gaming
'xep_0198', # Stream Management
'xep_0199', # Ping
'xep_0202', # Entity Time
'xep_0203', # Delayed Delivery
'xep_0221', # Data Forms Media Element
'xep_0222', # Persistent Storage of Public Data via Pubsub
'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
]
+360
View File
@@ -0,0 +1,360 @@
# -*- encoding: utf-8 -*-
"""
slixmpp.plugins.base
~~~~~~~~~~~~~~~~~~~~~~
This module provides XMPP functionality that
is specific to client connections.
Part of Slixmpp: The Slick XMPP Library
:copyright: (c) 2012 Nathanael C. Fritz
:license: MIT, see LICENSE for more details
"""
import sys
import copy
import logging
import threading
if sys.version_info >= (3, 0):
unicode = str
log = logging.getLogger(__name__)
#: Associate short string names of plugins with implementations. The
#: plugin names are based on the spec used by the plugin, such as
#: `'xep_0030'` for a plugin that implements XEP-0030.
PLUGIN_REGISTRY = {}
#: In order to do cascading plugin disabling, reverse dependencies
#: must be tracked.
PLUGIN_DEPENDENTS = {}
#: Only allow one thread to manipulate the plugin registry at a time.
REGISTRY_LOCK = threading.RLock()
class PluginNotFound(Exception):
"""Raised if an unknown plugin is accessed."""
def register_plugin(impl, name=None):
"""Add a new plugin implementation to the registry.
:param class impl: The plugin class.
The implementation class must provide a :attr:`~BasePlugin.name`
value that will be used as a short name for enabling and disabling
the plugin. The name should be based on the specification used by
the plugin. For example, a plugin implementing XEP-0030 would be
named `'xep_0030'`.
"""
if name is None:
name = impl.name
with REGISTRY_LOCK:
PLUGIN_REGISTRY[name] = impl
if name not in PLUGIN_DEPENDENTS:
PLUGIN_DEPENDENTS[name] = set()
for dep in impl.dependencies:
if dep not in PLUGIN_DEPENDENTS:
PLUGIN_DEPENDENTS[dep] = set()
PLUGIN_DEPENDENTS[dep].add(name)
def load_plugin(name, module=None):
"""Find and import a plugin module so that it can be registered.
This function is called to import plugins that have selected for
enabling, but no matching registered plugin has been found.
:param str name: The name of the plugin. It is expected that
plugins are in packages matching their name,
even though the plugin class name does not
have to match.
:param str module: The name of the base module to search
for the plugin.
"""
try:
if not module:
try:
module = 'slixmpp.plugins.%s' % name
__import__(module)
mod = sys.modules[module]
except ImportError:
module = 'slixmpp.features.%s' % name
__import__(module)
mod = sys.modules[module]
elif isinstance(module, (str, unicode)):
__import__(module)
mod = sys.modules[module]
else:
mod = module
# Add older style plugins to the registry.
if hasattr(mod, name):
plugin = getattr(mod, name)
if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'):
plugin.name = name
# Mark the plugin as an older style plugin so
# we can work around dependency issues.
plugin.old_style = True
register_plugin(plugin, name)
except ImportError:
log.exception("Unable to load plugin: %s", name)
class PluginManager(object):
def __init__(self, xmpp, config=None):
#: We will track all enabled plugins in a set so that we
#: can enable plugins in batches and pull in dependencies
#: without problems.
self._enabled = set()
#: Maintain references to active plugins.
self._plugins = {}
self._plugin_lock = threading.RLock()
#: Globally set default plugin configuration. This will
#: be used for plugins that are auto-enabled through
#: dependency loading.
self.config = config if config else {}
self.xmpp = xmpp
def register(self, plugin, enable=True):
"""Register a new plugin, and optionally enable it.
:param class plugin: The implementation class of the plugin
to register.
:param bool enable: If ``True``, immediately enable the
plugin after registration.
"""
register_plugin(plugin)
if enable:
self.enable(plugin.name)
def enable(self, name, config=None, enabled=None):
"""Enable a plugin, including any dependencies.
:param string name: The short name of the plugin.
:param dict config: Optional settings dictionary for
configuring plugin behaviour.
"""
top_level = False
if enabled is None:
enabled = set()
with self._plugin_lock:
if name not in self._enabled:
enabled.add(name)
self._enabled.add(name)
if not self.registered(name):
load_plugin(name)
plugin_class = PLUGIN_REGISTRY.get(name, None)
if not plugin_class:
raise PluginNotFound(name)
if config is None:
config = self.config.get(name, None)
plugin = plugin_class(self.xmpp, config)
self._plugins[name] = plugin
for dep in plugin.dependencies:
self.enable(dep, enabled=enabled)
plugin._init()
if top_level:
for name in enabled:
if hasattr(self.plugins[name], 'old_style'):
# Older style plugins require post_init()
# to run just before stream processing begins,
# so we don't call it here.
pass
self.plugins[name].post_init()
def enable_all(self, names=None, config=None):
"""Enable all registered plugins.
:param list names: A list of plugin names to enable. If
none are provided, all registered plugins
will be enabled.
:param dict config: A dictionary mapping plugin names to
configuration dictionaries, as used by
:meth:`~PluginManager.enable`.
"""
names = names if names else PLUGIN_REGISTRY.keys()
if config is None:
config = {}
for name in names:
self.enable(name, config.get(name, {}))
def enabled(self, name):
"""Check if a plugin has been enabled.
:param string name: The name of the plugin to check.
:return: boolean
"""
return name in self._enabled
def registered(self, name):
"""Check if a plugin has been registered.
:param string name: The name of the plugin to check.
:return: boolean
"""
return name in PLUGIN_REGISTRY
def disable(self, name, _disabled=None):
"""Disable a plugin, including any dependent upon it.
:param string name: The name of the plugin to disable.
:param set _disabled: Private set used to track the
disabled status of plugins during
the cascading process.
"""
if _disabled is None:
_disabled = set()
with self._plugin_lock:
if name not in _disabled and name in self._enabled:
_disabled.add(name)
plugin = self._plugins.get(name, None)
if plugin is None:
raise PluginNotFound(name)
for dep in PLUGIN_DEPENDENTS[name]:
self.disable(dep, _disabled)
plugin._end()
if name in self._enabled:
self._enabled.remove(name)
del self._plugins[name]
def __keys__(self):
"""Return the set of enabled plugins."""
return self._plugins.keys()
def __getitem__(self, name):
"""
Allow plugins to be accessed through the manager as if
it were a dictionary.
"""
plugin = self._plugins.get(name, None)
if plugin is None:
raise PluginNotFound(name)
return plugin
def __iter__(self):
"""Return an iterator over the set of enabled plugins."""
return self._plugins.__iter__()
def __len__(self):
"""Return the number of enabled plugins."""
return len(self._plugins)
class BasePlugin(object):
#: A short name for the plugin based on the implemented specification.
#: For example, a plugin for XEP-0030 would use `'xep_0030'`.
name = ''
#: A longer name for the plugin, describing its purpose. For example,
#: a plugin for XEP-0030 would use `'Service Discovery'` as its
#: description value.
description = ''
#: Some plugins may depend on others in order to function properly.
#: Any plugin names included in :attr:`~BasePlugin.dependencies` will
#: 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:
self.api = self.xmpp.api.wrap(self.name)
#: A plugin's behaviour may be configurable, in which case those
#: configuration settings will be provided as a dictionary.
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.
Also sets up required event handlers.
"""
if self.xmpp is not None:
self.xmpp.add_event_handler('session_bind', self.session_bind)
if self.xmpp.session_bind_event.is_set():
self.session_bind(self.xmpp.boundjid.full)
self.plugin_init()
log.debug('Loaded Plugin: %s', self.description)
def _end(self):
"""Cleanup plugin state, and prepare for plugin removal.
Also removes required event handlers.
"""
if self.xmpp is not None:
self.xmpp.del_event_handler('session_bind', self.session_bind)
self.plugin_end()
log.debug('Disabled Plugin: %s' % self.description)
def plugin_init(self):
"""Initialize plugin state, such as registering event handlers."""
pass
def plugin_end(self):
"""Cleanup plugin state, and prepare for plugin removal."""
pass
def session_bind(self, jid):
"""Initialize plugin state based on the bound JID."""
pass
def post_init(self):
"""Initialize any cross-plugin state.
Only needed if the plugin has circular dependencies.
"""
pass
base_plugin = BasePlugin
+149
View File
@@ -0,0 +1,149 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from . import base
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.iq import Iq
log = logging.getLogger(__name__)
class GmailQuery(ElementBase):
namespace = 'google:mail:notify'
name = 'query'
plugin_attrib = 'gmail'
interfaces = set(('newer-than-time', 'newer-than-tid', 'q', 'search'))
def getSearch(self):
return self['q']
def setSearch(self, search):
self['q'] = search
def delSearch(self):
del self['q']
class MailBox(ElementBase):
namespace = 'google:mail:notify'
name = 'mailbox'
plugin_attrib = 'mailbox'
interfaces = set(('result-time', 'total-matched', 'total-estimate',
'url', 'threads', 'matched', 'estimate'))
def getThreads(self):
threads = []
for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace,
MailThread.name)):
threads.append(MailThread(xml=threadXML, parent=None))
return threads
def getMatched(self):
return self['total-matched']
def getEstimate(self):
return self['total-estimate'] == '1'
class MailThread(ElementBase):
namespace = 'google:mail:notify'
name = 'mail-thread-info'
plugin_attrib = 'thread'
interfaces = set(('tid', 'participation', 'messages', 'date',
'senders', 'url', 'labels', 'subject', 'snippet'))
sub_interfaces = set(('labels', 'subject', 'snippet'))
def getSenders(self):
senders = []
sendersXML = self.xml.find('{%s}senders' % self.namespace)
if sendersXML is not None:
for senderXML in sendersXML.findall('{%s}sender' % self.namespace):
senders.append(MailSender(xml=senderXML, parent=None))
return senders
class MailSender(ElementBase):
namespace = 'google:mail:notify'
name = 'sender'
plugin_attrib = 'sender'
interfaces = set(('address', 'name', 'originator', 'unread'))
def getOriginator(self):
return self.xml.attrib.get('originator', '0') == '1'
def getUnread(self):
return self.xml.attrib.get('unread', '0') == '1'
class NewMail(ElementBase):
namespace = 'google:mail:notify'
name = 'new-mail'
plugin_attrib = 'new-mail'
class gmail_notify(base.base_plugin):
"""
Google Talk: Gmail Notifications
"""
def plugin_init(self):
self.description = 'Google Talk: Gmail Notifications'
self.xmpp.registerHandler(
Callback('Gmail Result',
MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
MailBox.namespace,
MailBox.name)),
self.handle_gmail))
self.xmpp.registerHandler(
Callback('Gmail New Mail',
MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns,
NewMail.namespace,
NewMail.name)),
self.handle_new_mail))
registerStanzaPlugin(Iq, GmailQuery)
registerStanzaPlugin(Iq, MailBox)
registerStanzaPlugin(Iq, NewMail)
self.last_result_time = None
def handle_gmail(self, iq):
mailbox = iq['mailbox']
approx = ' approximately' if mailbox['estimated'] else ''
log.info('Gmail: Received%s %s emails', approx, mailbox['total-matched'])
self.last_result_time = mailbox['result-time']
self.xmpp.event('gmail_messages', iq)
def handle_new_mail(self, iq):
log.info("Gmail: New emails received!")
self.xmpp.event('gmail_notify')
self.checkEmail()
def getEmail(self, query=None):
return self.search(query)
def checkEmail(self):
return self.search(newer=self.last_result_time)
def search(self, query=None, newer=None):
if query is None:
log.info("Gmail: Checking for new emails")
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']['q'] = query
iq['gmail']['newer-than-time'] = newer
return iq.send()
+47
View File
@@ -0,0 +1,47 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin, BasePlugin
from slixmpp.plugins.google.gmail import Gmail
from slixmpp.plugins.google.auth import GoogleAuth
from slixmpp.plugins.google.settings import GoogleSettings
from slixmpp.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)
+10
View File
@@ -0,0 +1,10 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.google.auth import stanza
from slixmpp.plugins.google.auth.auth import GoogleAuth
+52
View File
@@ -0,0 +1,52 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.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
+49
View File
@@ -0,0 +1,49 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.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.disovery_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)
+10
View File
@@ -0,0 +1,10 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.google.gmail import stanza
from slixmpp.plugins.google.gmail.notifications import Gmail
@@ -0,0 +1,96 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp.stanza import Iq
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import MatchXPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.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 = data['gmail_messages']['result_time']
threads = data['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)
+101
View File
@@ -0,0 +1,101 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.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)
+10
View File
@@ -0,0 +1,10 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.google.nosave import stanza
from slixmpp.plugins.google.nosave.nosave import GoogleNoSave
+83
View File
@@ -0,0 +1,83 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp.stanza import Iq, Message
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.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)
+59
View File
@@ -0,0 +1,59 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.jid import JID
from slixmpp.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):
self._set_attr('source', str(value))
register_stanza_plugin(NoSaveQuery, Item)
@@ -0,0 +1,10 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.google.settings import stanza
from slixmpp.plugins.google.settings.settings import GoogleSettings
@@ -0,0 +1,65 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp.stanza import Iq
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.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)
+110
View File
@@ -0,0 +1,110 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.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')
+22
View File
@@ -0,0 +1,22 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0004.stanza import Form
from slixmpp.plugins.xep_0004.stanza import FormField, FieldOption
from slixmpp.plugins.xep_0004.dataforms import XEP_0004
register_plugin(XEP_0004)
# Retain some backwards compatibility
xep_0004 = XEP_0004
xep_0004.makeForm = xep_0004.make_form
xep_0004.buildForm = xep_0004.build_form
+57
View File
@@ -0,0 +1,57 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp import Message
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0004 import stanza
from slixmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption
class XEP_0004(BasePlugin):
"""
XEP-0004: Data Forms
"""
name = 'xep_0004'
description = 'XEP-0004: Data Forms'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
self.xmpp.register_handler(
Callback('Data Form',
StanzaPath('message/form'),
self.handle_form))
register_stanza_plugin(FormField, FieldOption, iterable=True)
register_stanza_plugin(Form, FormField, iterable=True)
register_stanza_plugin(Message, Form)
def plugin_end(self):
self.xmpp.remove_handler('Data Form')
self.xmpp['xep_0030'].del_feature(feature='jabber:x:data')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('jabber:x:data')
def make_form(self, ftype='form', title='', instructions=''):
f = Form()
f['type'] = ftype
f['title'] = title
f['instructions'] = instructions
return f
def handle_form(self, message):
self.xmpp.event("message_xform", message)
def build_form(self, xml):
return Form(xml=xml)
@@ -0,0 +1,10 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.xep_0004.stanza.field import FormField, FieldOption
from slixmpp.plugins.xep_0004.stanza.form import Form
+183
View File
@@ -0,0 +1,183 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, ET
class FormField(ElementBase):
namespace = 'jabber:x:data'
name = 'field'
plugin_attrib = 'field'
interfaces = set(('answer', 'desc', 'required', 'value',
'options', 'label', 'type', 'var'))
sub_interfaces = set(('desc',))
plugin_tag_map = {}
plugin_attrib_map = {}
field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi',
'jid-single', 'list-multi', 'list-single',
'text-multi', 'text-private', 'text-single'))
true_values = set((True, '1', 'true'))
option_types = set(('list-multi', 'list-single'))
multi_line_types = set(('hidden', 'text-multi'))
multi_value_types = set(('hidden', 'jid-multi',
'list-multi', 'text-multi'))
def setup(self, xml=None):
if ElementBase.setup(self, xml):
self._type = None
else:
self._type = self['type']
def set_type(self, value):
self._set_attr('type', value)
if value:
self._type = value
def add_option(self, label='', value=''):
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'])
def del_options(self):
optsXML = self.xml.findall('{%s}option' % self.namespace)
for optXML in optsXML:
self.xml.remove(optXML)
def del_required(self):
reqXML = self.xml.find('{%s}required' % self.namespace)
if reqXML is not None:
self.xml.remove(reqXML)
def del_value(self):
valsXML = self.xml.findall('{%s}value' % self.namespace)
for valXML in valsXML:
self.xml.remove(valXML)
def get_answer(self):
return self['value']
def get_options(self):
options = []
optsXML = self.xml.findall('{%s}option' % self.namespace)
for optXML in optsXML:
opt = FieldOption(xml=optXML)
options.append({'label': opt['label'], 'value': opt['value']})
return options
def get_required(self):
reqXML = self.xml.find('{%s}required' % self.namespace)
return reqXML is not None
def get_value(self, convert=True):
valsXML = self.xml.findall('{%s}value' % self.namespace)
if len(valsXML) == 0:
return None
elif self._type == 'boolean':
if convert:
return valsXML[0].text in self.true_values
return valsXML[0].text
elif self._type in self.multi_value_types or len(valsXML) > 1:
values = []
for valXML in valsXML:
if valXML.text is None:
valXML.text = ''
values.append(valXML.text)
if self._type == 'text-multi' and convert:
values = "\n".join(values)
return values
else:
if valsXML[0].text is None:
return ''
return valsXML[0].text
def set_answer(self, answer):
self['value'] = answer
def set_false(self):
self['value'] = False
def set_options(self, options):
for value in options:
if isinstance(value, dict):
self.add_option(**value)
else:
self.add_option(value=value)
def set_required(self, required):
exists = self['required']
if not exists and required:
self.xml.append(ET.Element('{%s}required' % self.namespace))
elif exists and not required:
del self['required']
def set_true(self):
self['value'] = True
def set_value(self, value):
del self['value']
valXMLName = '{%s}value' % self.namespace
if self._type == 'boolean':
if value in self.true_values:
valXML = ET.Element(valXMLName)
valXML.text = '1'
self.xml.append(valXML)
else:
valXML = ET.Element(valXMLName)
valXML.text = '0'
self.xml.append(valXML)
elif self._type in self.multi_value_types or self._type in ('', None):
if isinstance(value, bool):
value = [value]
if not isinstance(value, list):
value = value.replace('\r', '')
value = value.split('\n')
for val in value:
if self._type in ('', None) and val in self.true_values:
val = '1'
valXML = ET.Element(valXMLName)
valXML.text = val
self.xml.append(valXML)
else:
if isinstance(value, list):
raise ValueError("Cannot add multiple values " + \
"to a %s field." % self._type)
valXML = ET.Element(valXMLName)
valXML.text = value
self.xml.append(valXML)
class FieldOption(ElementBase):
namespace = 'jabber:x:data'
name = 'option'
plugin_attrib = 'option'
interfaces = set(('label', 'value'))
sub_interfaces = set(('value',))
FormField.addOption = FormField.add_option
FormField.delOptions = FormField.del_options
FormField.delRequired = FormField.del_required
FormField.delValue = FormField.del_value
FormField.getAnswer = FormField.get_answer
FormField.getOptions = FormField.get_options
FormField.getRequired = FormField.get_required
FormField.getValue = FormField.get_value
FormField.setAnswer = FormField.set_answer
FormField.setFalse = FormField.set_false
FormField.setOptions = FormField.set_options
FormField.setRequired = FormField.set_required
FormField.setTrue = FormField.set_true
FormField.setValue = FormField.set_value
+257
View File
@@ -0,0 +1,257 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import copy
import logging
from slixmpp.thirdparty import OrderedDict
from slixmpp.xmlstream import ElementBase, ET
from slixmpp.plugins.xep_0004.stanza import FormField
log = logging.getLogger(__name__)
class Form(ElementBase):
namespace = 'jabber:x:data'
name = 'x'
plugin_attrib = 'form'
interfaces = set(('fields', 'instructions', 'items',
'reported', 'title', 'type', 'values'))
sub_interfaces = set(('title',))
form_types = set(('cancel', 'form', 'result', 'submit'))
def __init__(self, *args, **kwargs):
title = None
if 'title' in kwargs:
title = kwargs['title']
del kwargs['title']
ElementBase.__init__(self, *args, **kwargs)
if title is not None:
self['title'] = title
def setup(self, xml=None):
if ElementBase.setup(self, xml):
# If we had to generate xml
self['type'] = 'form'
@property
def field(self):
return self['fields']
def set_type(self, ftype):
self._set_attr('type', ftype)
if ftype == 'submit':
fields = self['fields']
for var in fields:
field = fields[var]
del field['type']
del field['label']
del field['desc']
del field['required']
del field['options']
elif ftype == 'cancel':
del self['fields']
def add_field(self, var='', ftype=None, label='', desc='',
required=False, value=None, options=None, **kwargs):
kwtype = kwargs.get('type', None)
if kwtype is None:
kwtype = ftype
field = FormField()
field['var'] = var
field['type'] = kwtype
field['value'] = value
if self['type'] in ('form', 'result'):
field['label'] = label
field['desc'] = desc
field['required'] = required
if options is not None:
field['options'] = options
else:
del field['type']
self.append(field)
return field
def getXML(self, type='submit'):
self['type'] = type
log.warning("Form.getXML() is deprecated API compatibility " + \
"with plugins/old_0004.py")
return self.xml
def fromXML(self, xml):
log.warning("Form.fromXML() is deprecated API compatibility " + \
"with plugins/old_0004.py")
n = Form(xml=xml)
return n
def add_item(self, values):
itemXML = ET.Element('{%s}item' % self.namespace)
self.xml.append(itemXML)
reported_vars = self['reported'].keys()
for var in reported_vars:
field = FormField()
field._type = self['reported'][var]['type']
field['var'] = var
field['value'] = values.get(var, None)
itemXML.append(field.xml)
def add_reported(self, var, ftype=None, label='', desc='', **kwargs):
kwtype = kwargs.get('type', None)
if kwtype is None:
kwtype = ftype
reported = self.xml.find('{%s}reported' % self.namespace)
if reported is None:
reported = ET.Element('{%s}reported' % self.namespace)
self.xml.append(reported)
fieldXML = ET.Element('{%s}field' % FormField.namespace)
reported.append(fieldXML)
field = FormField(xml=fieldXML)
field['var'] = var
field['type'] = kwtype
field['label'] = label
field['desc'] = desc
return field
def cancel(self):
self['type'] = 'cancel'
def del_fields(self):
fieldsXML = self.xml.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
self.xml.remove(fieldXML)
def del_instructions(self):
instsXML = self.xml.findall('{%s}instructions')
for instXML in instsXML:
self.xml.remove(instXML)
def del_items(self):
itemsXML = self.xml.find('{%s}item' % self.namespace)
for itemXML in itemsXML:
self.xml.remove(itemXML)
def del_reported(self):
reportedXML = self.xml.find('{%s}reported' % self.namespace)
if reportedXML is not None:
self.xml.remove(reportedXML)
def get_fields(self, use_dict=False):
fields = OrderedDict()
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])
def get_items(self):
items = []
itemsXML = self.xml.findall('{%s}item' % self.namespace)
for itemXML in itemsXML:
item = OrderedDict()
fieldsXML = itemXML.findall('{%s}field' % FormField.namespace)
for fieldXML in fieldsXML:
field = FormField(xml=fieldXML)
item[field['var']] = field['value']
items.append(item)
return items
def get_reported(self):
fields = OrderedDict()
xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace,
FormField.namespace))
for field in xml:
field = FormField(xml=field)
fields[field['var']] = field
return fields
def get_values(self):
values = OrderedDict()
fields = self['fields']
for var in fields:
values[var] = fields[var]['value']
return values
def reply(self):
if self['type'] == 'form':
self['type'] = 'submit'
elif self['type'] == 'submit':
self['type'] = 'result'
def set_fields(self, fields):
del self['fields']
if not isinstance(fields, list):
fields = fields.items()
for var, field in fields:
field['var'] = var
self.add_field(**field)
def set_instructions(self, instructions):
del self['instructions']
if instructions in [None, '']:
return
if not isinstance(instructions, list):
instructions = instructions.split('\n')
for instruction in instructions:
inst = ET.Element('{%s}instructions' % self.namespace)
inst.text = instruction
self.xml.append(inst)
def set_items(self, items):
for item in items:
self.add_item(item)
def set_reported(self, reported):
for var in reported:
field = reported[var]
field['var'] = var
self.add_reported(var, **field)
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):
new = copy.copy(self)
if type(other) == dict:
new['values'] = other
return new
nfields = new['fields']
ofields = other['fields']
nfields.update(ofields)
new['fields'] = nfields
return new
Form.setType = Form.set_type
Form.addField = Form.add_field
Form.addItem = Form.add_item
Form.addReported = Form.add_reported
Form.delFields = Form.del_fields
Form.delInstructions = Form.del_instructions
Form.delItems = Form.del_items
Form.delReported = Form.del_reported
Form.getFields = Form.get_fields
Form.getInstructions = Form.get_instructions
Form.getItems = Form.get_items
Form.getReported = Form.get_reported
Form.getValues = Form.get_values
Form.setFields = Form.set_fields
Form.setInstructions = Form.set_instructions
Form.setItems = Form.set_items
Form.setReported = Form.set_reported
Form.setValues = Form.set_values
+20
View File
@@ -0,0 +1,20 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0009 import stanza
from slixmpp.plugins.xep_0009.rpc import XEP_0009
from slixmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse
register_plugin(XEP_0009)
# Retain some backwards compatibility
xep_0009 = XEP_0009
+173
View File
@@ -0,0 +1,173 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ET
import base64
import logging
import time
import sys
if sys.version_info > (3, 0):
unicode = str
log = logging.getLogger(__name__)
_namespace = 'jabber:iq:rpc'
def fault2xml(fault):
value = dict()
value['faultCode'] = fault['code']
value['faultString'] = fault['string']
fault = ET.Element("fault", {'xmlns': _namespace})
fault.append(_py2xml((value)))
return fault
def xml2fault(params):
vals = []
for value in params.findall('{%s}value' % _namespace):
vals.append(_xml2py(value))
fault = dict()
fault['code'] = vals[0]['faultCode']
fault['string'] = vals[0]['faultString']
return fault
def py2xml(*args):
params = ET.Element("{%s}params" % _namespace)
for x in args:
param = ET.Element("{%s}param" % _namespace)
param.append(_py2xml(x))
params.append(param) #<params><param>...
return params
def _py2xml(*args):
for x in args:
val = ET.Element("{%s}value" % _namespace)
if x is None:
nil = ET.Element("{%s}nil" % _namespace)
val.append(nil)
elif type(x) is int:
i4 = ET.Element("{%s}i4" % _namespace)
i4.text = str(x)
val.append(i4)
elif type(x) is bool:
boolean = ET.Element("{%s}boolean" % _namespace)
boolean.text = str(int(x))
val.append(boolean)
elif type(x) in (str, unicode):
string = ET.Element("{%s}string" % _namespace)
string.text = x
val.append(string)
elif type(x) is float:
double = ET.Element("{%s}double" % _namespace)
double.text = str(x)
val.append(double)
elif type(x) is rpcbase64:
b64 = ET.Element("{%s}base64" % _namespace)
b64.text = x.encoded()
val.append(b64)
elif type(x) is rpctime:
iso = ET.Element("{%s}dateTime.iso8601" % _namespace)
iso.text = str(x)
val.append(iso)
elif type(x) in (list, tuple):
array = ET.Element("{%s}array" % _namespace)
data = ET.Element("{%s}data" % _namespace)
for y in x:
data.append(_py2xml(y))
array.append(data)
val.append(array)
elif type(x) is dict:
struct = ET.Element("{%s}struct" % _namespace)
for y in x.keys():
member = ET.Element("{%s}member" % _namespace)
name = ET.Element("{%s}name" % _namespace)
name.text = y
member.append(name)
member.append(_py2xml(x[y]))
struct.append(member)
val.append(struct)
return val
def xml2py(params):
namespace = 'jabber:iq:rpc'
vals = []
for param in params.findall('{%s}param' % namespace):
vals.append(_xml2py(param.find('{%s}value' % namespace)))
return vals
def _xml2py(value):
namespace = 'jabber:iq:rpc'
if value.find('{%s}nil' % namespace) is not None:
return None
if value.find('{%s}i4' % namespace) is not None:
return int(value.find('{%s}i4' % namespace).text)
if value.find('{%s}int' % namespace) is not None:
return int(value.find('{%s}int' % namespace).text)
if value.find('{%s}boolean' % namespace) is not None:
return bool(int(value.find('{%s}boolean' % namespace).text))
if value.find('{%s}string' % namespace) is not None:
return value.find('{%s}string' % namespace).text
if value.find('{%s}double' % namespace) is not None:
return float(value.find('{%s}double' % namespace).text)
if value.find('{%s}base64' % namespace) is not None:
return rpcbase64(value.find('{%s}base64' % namespace).text.encode())
if value.find('{%s}Base64' % namespace) is not None:
# Older versions of XEP-0009 used Base64
return rpcbase64(value.find('{%s}Base64' % namespace).text.encode())
if value.find('{%s}dateTime.iso8601' % namespace) is not None:
return rpctime(value.find('{%s}dateTime.iso8601' % namespace).text)
if value.find('{%s}struct' % namespace) is not None:
struct = {}
for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace):
struct[member.find('{%s}name' % namespace).text] = _xml2py(member.find('{%s}value' % namespace))
return struct
if value.find('{%s}array' % namespace) is not None:
array = []
for val in value.find('{%s}array' % namespace).find('{%s}data' % namespace).findall('{%s}value' % namespace):
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.b64decode(self.data)
def __str__(self):
return self.decode().decode()
def encoded(self):
return self.data.decode()
class rpctime(object):
def __init__(self,data=None):
#assume string data is in iso format YYYYMMDDTHH:MM:SS
if type(data) in (str, unicode):
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()
+742
View File
@@ -0,0 +1,742 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from binding import py2xml, xml2py, xml2fault, fault2xml
from threading import RLock
import abc
import inspect
import logging
import slixmpp
import sys
import threading
import traceback
log = logging.getLogger(__name__)
def _intercept(method, name, public):
def _resolver(instance, *args, **kwargs):
log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args)
try:
value = method(instance, *args, **kwargs)
if value == NotImplemented:
raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__))
return value
except InvocationException:
raise
except Exception as e:
raise InvocationException("A problem occured calling %s.%s!" % (instance.FQN(), method.__name__), e)
_resolver._rpc = public
_resolver._rpc_name = method.__name__ if name is None else name
return _resolver
def remote(function_argument, public = True):
'''
Decorator for methods which are remotely callable. This decorator
works in conjunction with classes which extend ABC Endpoint.
Example:
@remote
def remote_method(arg1, arg2)
Arguments:
function_argument -- a stand-in for either the actual method
OR a new name (string) for the method. In that case the
method is considered mapped:
Example:
@remote("new_name")
def remote_method(arg1, arg2)
public -- A flag which indicates if this method should be part
of the known dictionary of remote methods. Defaults to True.
Example:
@remote(False)
def remote_method(arg1, arg2)
Note: renaming and revising (public vs. private) can be combined.
Example:
@remote("new_name", False)
def remote_method(arg1, arg2)
'''
if hasattr(function_argument, '__call__'):
return _intercept(function_argument, None, public)
else:
if not isinstance(function_argument, basestring):
if not isinstance(function_argument, bool):
raise Exception('Expected an RPC method name or visibility modifier!')
else:
def _wrap_revised(function):
function = _intercept(function, None, function_argument)
return function
return _wrap_revised
def _wrap_remapped(function):
function = _intercept(function, function_argument, public)
return function
return _wrap_remapped
class ACL:
'''
An Access Control List (ACL) is a list of rules, which are evaluated
in order until a match is found. The policy of the matching rule
is then applied.
Rules are 3-tuples, consisting of a policy enumerated type, a JID
expression and a RCP resource expression.
Examples:
[ (ACL.ALLOW, '*', '*') ] allow everyone everything, no restrictions
[ (ACL.DENY, '*', '*') ] deny everyone everything, no restrictions
[ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'),
(ACL.DENY, '*', '*') ] deny everyone everything, except named
JID, which is allowed access to endpoint 'test' only.
The use of wildcards is allowed in expressions, as follows:
'*' everyone, or everything (= all endpoints and methods)
'test@xmpp.org/*' every JID regardless of JID resource
'*@xmpp.org/rpc' every JID from domain xmpp.org with JID res 'rpc'
'frank@*' every 'frank', regardless of domain or JID res
'system.*' all methods of endpoint 'system'
'*.reboot' all methods reboot regardless of endpoint
'''
ALLOW = True
DENY = False
@classmethod
def check(cls, rules, jid, resource):
if rules is None:
return cls.DENY # No rules means no access!
jid = str(jid) # Check the string representation of the JID.
if not jid:
return cls.DENY # Can't check an empty JID.
for rule in rules:
policy = cls._check(rule, jid, resource)
if policy is not None:
return policy
return cls.DENY # By default if not rule matches, deny access.
@classmethod
def _check(cls, rule, jid, resource):
if cls._match(jid, rule[1]) and cls._match(resource, rule[2]):
return rule[0]
else:
return None
@classmethod
def _next_token(cls, expression, index):
new_index = expression.find('*', index)
if new_index == 0:
return ''
else:
if new_index == -1:
return expression[index : ]
else:
return expression[index : new_index]
@classmethod
def _match(cls, value, expression):
#! print "_match [VALUE] %s [EXPR] %s" % (value, expression)
index = 0
position = 0
while index < len(expression):
token = cls._next_token(expression, index)
#! print "[TOKEN] '%s'" % token
size = len(token)
if size > 0:
token_index = value.find(token, position)
if token_index == -1:
return False
else:
#! print "[INDEX-OF] %s" % token_index
position = token_index + len(token)
pass
if size == 0:
index += 1
else:
index += size
#! print "index %s position %s" % (index, position)
return True
ANY_ALL = [ (ACL.ALLOW, '*', '*') ]
class RemoteException(Exception):
'''
Base exception for RPC. This exception is raised when a problem
occurs in the network layer.
'''
def __init__(self, message="", cause=None):
'''
Initializes a new RemoteException.
Arguments:
message -- The message accompanying this exception.
cause -- The underlying cause of this exception.
'''
self._message = message
self._cause = cause
pass
def __str__(self):
return repr(self._message)
def get_message(self):
return self._message
def get_cause(self):
return self._cause
class InvocationException(RemoteException):
'''
Exception raised when a problem occurs during the remote invocation
of a method.
'''
pass
class AuthorizationException(RemoteException):
'''
Exception raised when the caller is not authorized to invoke the
remote method.
'''
pass
class TimeoutException(Exception):
'''
Exception raised when the synchronous execution of a method takes
longer than the given threshold because an underlying asynchronous
reply did not arrive in time.
'''
pass
class Callback(object):
'''
A base class for callback handlers.
'''
__metaclass__ = abc.ABCMeta
@abc.abstractproperty
def set_value(self, value):
return NotImplemented
@abc.abstractproperty
def cancel_with_error(self, exception):
return NotImplemented
class Future(Callback):
'''
Represents the result of an asynchronous computation.
'''
def __init__(self):
'''
Initializes a new Future.
'''
self._value = None
self._exception = None
self._event = threading.Event()
pass
def set_value(self, value):
'''
Sets the value of this Future. Once the value is set, a caller
blocked on get_value will be able to continue.
'''
self._value = value
self._event.set()
def get_value(self, timeout=None):
'''
Gets the value of this Future. This call will block until
the result is available, or until an optional timeout expires.
When this Future is cancelled with an error,
Arguments:
timeout -- The maximum waiting time to obtain the value.
'''
self._event.wait(timeout)
if self._exception:
raise self._exception
if not self._event.is_set():
raise TimeoutException
return self._value
def is_done(self):
'''
Returns true if a value has been returned.
'''
return self._event.is_set()
def cancel_with_error(self, exception):
'''
Cancels the Future because of an error. Once cancelled, a
caller blocked on get_value will be able to continue.
'''
self._exception = exception
self._event.set()
class Endpoint(object):
'''
The Endpoint class is an abstract base class for all objects
participating in an RPC-enabled XMPP network.
A user subclassing this class is required to implement the method:
FQN(self)
where FQN stands for Fully Qualified Name, an unambiguous name
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):
'''
Initialize a new Endpoint. This constructor should never be
invoked by a user, instead it will be called by the factories
which instantiate the RPC-enabled objects, of which only
the classes are provided by the user.
Arguments:
session -- An RPC session instance.
target_jid -- the identity of the remote XMPP entity.
'''
self.session = session
self.target_jid = target_jid
@abc.abstractproperty
def FQN(self):
return NotImplemented
def get_methods(self):
'''
Returns a dictionary of all RPC method names provided by this
class. This method returns the actual method names as found
in the class definition which have been decorated with:
@remote
def some_rpc_method(arg1, arg2)
Unless:
(1) the name has been remapped, in which case the new
name will be returned.
@remote("new_name")
def some_rpc_method(arg1, arg2)
(2) the method is set to hidden
@remote(False)
def some_hidden_method(arg1, arg2)
'''
result = dict()
for function in dir(self):
test_attr = getattr(self, function, None)
try:
if test_attr._rpc:
result[test_attr._rpc_name] = test_attr
except Exception:
pass
return result
class Proxy(Endpoint):
'''
Implementation of the Proxy pattern which is intended to wrap
around Endpoints in order to intercept calls, marshall them and
forward them to the remote object.
'''
def __init__(self, endpoint, callback = None):
'''
Initializes a new Proxy.
Arguments:
endpoint -- The endpoint which is proxified.
'''
self._endpoint = endpoint
self._callback = callback
def __getattribute__(self, name, *args):
if name in ('__dict__', '_endpoint', 'async', '_callback'):
return object.__getattribute__(self, name)
else:
attribute = self._endpoint.__getattribute__(name)
if hasattr(attribute, '__call__'):
try:
if attribute._rpc:
def _remote_call(*args, **kwargs):
log.debug("Remotely calling '%s.%s' with arguments %s.", self._endpoint.FQN(), attribute._rpc_name, args)
return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs)
return _remote_call
except:
pass # If the attribute doesn't exist, don't care!
return attribute
def async(self, callback):
return Proxy(self._endpoint, callback)
def get_endpoint(self):
'''
Returns the proxified endpoint.
'''
return self._endpoint
def FQN(self):
return self._endpoint.FQN()
class JabberRPCEntry(object):
def __init__(self, endpoint_FQN, call):
self._endpoint_FQN = endpoint_FQN
self._call = call
def call_method(self, args):
return_value = self._call(*args)
if return_value is None:
return return_value
else:
return self._return(return_value)
def get_endpoint_FQN(self):
return self._endpoint_FQN
def _return(self, *args):
return args
class RemoteSession(object):
'''
A context object for a Jabber-RPC session.
'''
def __init__(self, client, session_close_callback):
'''
Initializes a new RPC session.
Arguments:
client -- The Slixmpp client associated with this session.
session_close_callback -- A callback called when the
session is closed.
'''
self._client = client
self._session_close_callback = session_close_callback
self._event = threading.Event()
self._entries = {}
self._callbacks = {}
self._acls = {}
self._lock = RLock()
def _wait(self):
self._event.wait()
def _notify(self, event):
log.debug("RPC Session as %s started.", self._client.boundjid.full)
self._client.sendPresence()
self._event.set()
pass
def _register_call(self, endpoint, method, name=None):
'''
Registers a method from an endpoint as remotely callable.
'''
if name is None:
name = method.__name__
key = "%s.%s" % (endpoint, name)
log.debug("Registering call handler for %s (%s).", key, method)
with self._lock:
if key in self._entries:
raise KeyError("A handler for %s has already been regisered!" % endpoint)
self._entries[key] = JabberRPCEntry(endpoint, method)
return key
def _register_acl(self, endpoint, acl):
log.debug("Registering ACL %s for endpoint %s.", repr(acl), endpoint)
with self._lock:
self._acls[endpoint] = acl
def _register_callback(self, pid, callback):
with self._lock:
self._callbacks[pid] = callback
def forget_callback(self, callback):
with self._lock:
pid = self._find_key(self._callbacks, callback)
if pid is not None:
del self._callback[pid]
else:
raise ValueError("Unknown callback!")
pass
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]
if len(search) == 0:
return None
else:
return search[0]
def _unregister_call(self, key):
#removes the registered call
with self._lock:
if self._entries[key]:
del self._entries[key]
else:
raise ValueError()
def new_proxy(self, target_jid, endpoint_cls):
'''
Instantiates a new proxy object, which proxies to a remote
endpoint. This method uses a class reference without
constructor arguments to instantiate the proxy.
Arguments:
target_jid -- the XMPP entity ID hosting the endpoint.
endpoint_cls -- The remote (duck) type.
'''
try:
argspec = inspect.getargspec(endpoint_cls.__init__)
args = [None] * (len(argspec[0]) - 1)
result = endpoint_cls(*args)
Endpoint.__init__(result, self, target_jid)
return Proxy(result)
except:
traceback.print_exc(file=sys.stdout)
def new_handler(self, acl, handler_cls, *args, **kwargs):
'''
Instantiates a new handler object, which is called remotely
by others. The user can control the effect of the call by
implementing the remote method in the local endpoint class. The
returned reference can be called locally and will behave as a
regular instance.
Arguments:
acl -- Access control list (see ACL class)
handler_clss -- The local (duck) type.
*args -- Constructor arguments for the local type.
**kwargs -- Constructor keyworded arguments for the local
type.
'''
argspec = inspect.getargspec(handler_cls.__init__)
base_argspec = inspect.getargspec(Endpoint.__init__)
if(argspec == base_argspec):
result = handler_cls(self, self._client.boundjid.full)
else:
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():
#!!! 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)
return result
# def is_available(self, targetCls, pto):
# return self._client.is_available(pto)
def _call_remote(self, pto, pmethod, callback, *arguments):
iq = self._client.plugin['xep_0009'].make_iq_method_call(pto, pmethod, py2xml(*arguments))
pid = iq['id']
if callback is None:
future = Future()
self._register_callback(pid, future)
iq.send()
return future.get_value(30)
else:
log.debug("[RemoteSession] _call_remote %s", callback)
self._register_callback(pid, callback)
iq.send()
def close(self):
'''
Closes this session.
'''
self._client.disconnect(False)
self._session_close_callback()
def _on_jabber_rpc_method_call(self, iq):
iq.enable('rpc_query')
params = iq['rpc_query']['method_call']['params']
args = xml2py(params)
pmethod = iq['rpc_query']['method_call']['method_name']
try:
with self._lock:
entry = self._entries[pmethod]
rules = self._acls[entry.get_endpoint_FQN()]
if ACL.check(rules, iq['from'], pmethod):
return_value = entry.call_method(args)
else:
raise AuthorizationException("Unauthorized access to %s from %s!" % (pmethod, iq['from']))
if return_value is None:
return_value = ()
response = self._client.plugin['xep_0009'].make_iq_method_response(iq['id'], iq['from'], py2xml(*return_value))
response.send()
except InvocationException as ie:
fault = dict()
fault['code'] = 500
fault['string'] = ie.get_message()
self._client.plugin['xep_0009']._send_fault(iq, fault2xml(fault))
except AuthorizationException as ae:
log.error(ae.get_message())
error = self._client.plugin['xep_0009']._forbidden(iq)
error.send()
except Exception as e:
if isinstance(e, KeyError):
log.error("No handler available for %s!", pmethod)
error = self._client.plugin['xep_0009']._item_not_found(iq)
else:
traceback.print_exc(file=sys.stderr)
log.error("An unexpected problem occurred invoking method %s!", pmethod)
error = self._client.plugin['xep_0009']._undefined_condition(iq)
#! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e
error.send()
def _on_jabber_rpc_method_response(self, iq):
iq.enable('rpc_query')
args = xml2py(iq['rpc_query']['method_response']['params'])
pid = iq['id']
with self._lock:
callback = self._callbacks[pid]
del self._callbacks[pid]
if(len(args) > 0):
callback.set_value(args[0])
else:
callback.set_value(None)
pass
def _on_jabber_rpc_method_response2(self, iq):
iq.enable('rpc_query')
if iq['rpc_query']['method_response']['fault'] is not None:
self._on_jabber_rpc_method_fault(iq)
else:
args = xml2py(iq['rpc_query']['method_response']['params'])
pid = iq['id']
with self._lock:
callback = self._callbacks[pid]
del self._callbacks[pid]
if(len(args) > 0):
callback.set_value(args[0])
else:
callback.set_value(None)
pass
def _on_jabber_rpc_method_fault(self, iq):
iq.enable('rpc_query')
fault = xml2fault(iq['rpc_query']['method_response']['fault'])
pid = iq['id']
with self._lock:
callback = self._callbacks[pid]
del self._callbacks[pid]
e = {
500: InvocationException
}[fault['code']](fault['string'])
callback.cancel_with_error(e)
def _on_jabber_rpc_error(self, iq):
pid = iq['id']
pmethod = self._client.plugin['xep_0009']._extract_method(iq['rpc_query'])
code = iq['error']['code']
type = iq['error']['type']
condition = iq['error']['condition']
#! print("['REMOTE.PY']._BINDING_handle_remote_procedure_error -> ERROR! ERROR! ERROR! Condition is '%s'" % condition)
with self._lock:
callback = self._callbacks[pid]
del self._callbacks[pid]
e = {
'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])),
'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])),
'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])),
}[condition]
if e is None:
RemoteException("An unexpected exception occurred at %s!" % iq['from'])
callback.cancel_with_error(e)
class Remote(object):
'''
Bootstrap class for Jabber-RPC sessions. New sessions are openend
with an existing XMPP client, or one is instantiated on demand.
'''
_instance = None
_sessions = dict()
_lock = threading.RLock()
@classmethod
def new_session_with_client(cls, client, callback=None):
'''
Opens a new session with a given client.
Arguments:
client -- An XMPP client.
callback -- An optional callback which can be used to track
the starting state of the session.
'''
with Remote._lock:
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;
def _session_close_callback():
with Remote._lock:
del cls._sessions[client.boundjid.bare]
result = RemoteSession(client, _session_close_callback)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call, threaded=True)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response, threaded=True)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault, threaded=True)
client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error, threaded=True)
if callback is None:
start_event_handler = result._notify
else:
start_event_handler = callback
client.add_event_handler("session_start", start_event_handler)
if client.connect():
client.process(threaded=True)
else:
raise RemoteException("Could not connect to XMPP server!")
pass
if callback is None:
result._wait()
return result
@classmethod
def new_session(cls, jid, password, callback=None):
'''
Opens a new session and instantiates a new XMPP client.
Arguments:
jid -- The XMPP JID for logging in.
password -- The password for logging in.
callback -- An optional callback which can be used to track
the starting state of the session.
'''
client = slixmpp.ClientXMPP(jid, password)
#? Register plug-ins.
client.registerPlugin('xep_0004') # Data Forms
client.registerPlugin('xep_0009') # Jabber-RPC
client.registerPlugin('xep_0030') # Service Discovery
client.registerPlugin('xep_0060') # PubSub
client.registerPlugin('xep_0199') # XMPP Ping
return cls.new_session_with_client(client, callback)
+218
View File
@@ -0,0 +1,218 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp import Iq
from slixmpp.xmlstream import ET, register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import MatchXPath
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0009 import stanza
from slixmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
log = logging.getLogger(__name__)
class XEP_0009(BasePlugin):
name = 'xep_0009'
description = 'XEP-0009: Jabber-RPC'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Iq, RPCQuery)
register_stanza_plugin(RPCQuery, MethodCall)
register_stanza_plugin(RPCQuery, MethodResponse)
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.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.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)),
self._handle_error)
)
self.xmpp.add_event_handler('jabber_rpc_method_call', self._on_jabber_rpc_method_call)
self.xmpp.add_event_handler('jabber_rpc_method_response', self._on_jabber_rpc_method_response)
self.xmpp.add_event_handler('jabber_rpc_method_fault', self._on_jabber_rpc_method_fault)
self.xmpp.add_event_handler('jabber_rpc_error', self._on_jabber_rpc_error)
self.xmpp.add_event_handler('error', self._handle_error)
#self.activeCalls = []
self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
self.xmpp['xep_0030'].add_identity('automation','rpc')
def make_iq_method_call(self, pto, pmethod, params):
iq = self.xmpp.makeIqSet()
iq.attrib['to'] = pto
iq.attrib['from'] = self.xmpp.boundjid.full
iq.enable('rpc_query')
iq['rpc_query']['method_call']['method_name'] = pmethod
iq['rpc_query']['method_call']['params'] = params
return iq;
def make_iq_method_response(self, pid, pto, params):
iq = self.xmpp.makeIqResult(pid)
iq.attrib['to'] = pto
iq.attrib['from'] = self.xmpp.boundjid.full
iq.enable('rpc_query')
iq['rpc_query']['method_response']['params'] = params
return iq
def make_iq_method_response_fault(self, pid, pto, params):
iq = self.xmpp.makeIqResult(pid)
iq.attrib['to'] = pto
iq.attrib['from'] = self.xmpp.boundjid.full
iq.enable('rpc_query')
iq['rpc_query']['method_response']['params'] = None
iq['rpc_query']['method_response']['fault'] = params
return iq
# def make_iq_method_error(self, pto, pid, pmethod, params, code, type, condition):
# iq = self.xmpp.makeIqError(pid)
# iq.attrib['to'] = pto
# iq.attrib['from'] = self.xmpp.boundjid.full
# iq['error']['code'] = code
# iq['error']['type'] = type
# iq['error']['condition'] = condition
# iq['rpc_query']['method_call']['method_name'] = pmethod
# iq['rpc_query']['method_call']['params'] = params
# return iq
def _item_not_found(self, iq):
payload = iq.get_payload()
iq.reply().error().set_payload(payload);
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
return iq
def _undefined_condition(self, iq):
payload = iq.get_payload()
iq.reply().error().set_payload(payload)
iq['error']['code'] = '500'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'undefined-condition'
return iq
def _forbidden(self, iq):
payload = iq.get_payload()
iq.reply().error().set_payload(payload)
iq['error']['code'] = '403'
iq['error']['type'] = 'auth'
iq['error']['condition'] = 'forbidden'
return iq
def _recipient_unvailable(self, iq):
payload = iq.get_payload()
iq.reply().error().set_payload(payload)
iq['error']['code'] = '404'
iq['error']['type'] = 'wait'
iq['error']['condition'] = 'recipient-unavailable'
return iq
def _handle_method_call(self, iq):
type = iq['type']
if type == 'set':
log.debug("Incoming Jabber-RPC call from %s", iq['from'])
self.xmpp.event('jabber_rpc_method_call', iq)
else:
if type == 'error' and ['rpc_query'] is None:
self.handle_error(iq)
else:
log.debug("Incoming Jabber-RPC error from %s", iq['from'])
self.xmpp.event('jabber_rpc_error', iq)
def _handle_method_response(self, iq):
if iq['rpc_query']['method_response']['fault'] is not None:
log.debug("Incoming Jabber-RPC fault from %s", iq['from'])
#self._on_jabber_rpc_method_fault(iq)
self.xmpp.event('jabber_rpc_method_fault', iq)
else:
log.debug("Incoming Jabber-RPC response from %s", iq['from'])
self.xmpp.event('jabber_rpc_method_response', iq)
def _handle_error(self, iq):
print("['XEP-0009']._handle_error -> ERROR! Iq is '%s'" % iq)
print("#######################")
print("### NOT IMPLEMENTED ###")
print("#######################")
def _on_jabber_rpc_method_call(self, iq, forwarded=False):
"""
A default handler for Jabber-RPC method call. If another
handler is registered, this one will defer and not run.
If this handler is called by your own custom handler with
forwarded set to True, then it will run as normal.
"""
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1:
return
# Reply with error by default
error = self.client.plugin['xep_0009']._item_not_found(iq)
error.send()
def _on_jabber_rpc_method_response(self, iq, forwarded=False):
"""
A default handler for Jabber-RPC method response. If another
handler is registered, this one will defer and not run.
If this handler is called by your own custom handler with
forwarded set to True, then it will run as normal.
"""
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1:
return
error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
error.send()
def _on_jabber_rpc_method_fault(self, iq, forwarded=False):
"""
A default handler for Jabber-RPC fault response. If another
handler is registered, this one will defer and not run.
If this handler is called by your own custom handler with
forwarded set to True, then it will run as normal.
"""
if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1:
return
error = self.client.plugin['xep_0009']._recpient_unavailable(iq)
error.send()
def _on_jabber_rpc_error(self, iq, forwarded=False):
"""
A default handler for Jabber-RPC error response. If another
handler is registered, this one will defer and not run.
If this handler is called by your own custom handler with
forwarded set to True, then it will run as normal.
"""
if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1:
return
error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload())
error.send()
def _send_fault(self, iq, fault_xml): #
fault = self.make_iq_method_response_fault(iq['id'], iq['from'], fault_xml)
fault.send()
def _send_error(self, iq):
print("['XEP-0009']._send_error -> ERROR! Iq is '%s'" % iq)
print("#######################")
print("### NOT IMPLEMENTED ###")
print("#######################")
def _extract_method(self, stanza):
xml = ET.fromstring("%s" % stanza)
return xml.find("./methodCall/methodName").text
+64
View File
@@ -0,0 +1,64 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream.stanzabase import ElementBase
from xml.etree import cElementTree as ET
class RPCQuery(ElementBase):
name = 'query'
namespace = 'jabber:iq:rpc'
plugin_attrib = 'rpc_query'
interfaces = set(())
subinterfaces = set(())
plugin_attrib_map = {}
plugin_tag_map = {}
class MethodCall(ElementBase):
name = 'methodCall'
namespace = 'jabber:iq:rpc'
plugin_attrib = 'method_call'
interfaces = set(('method_name', 'params'))
subinterfaces = set(())
plugin_attrib_map = {}
plugin_tag_map = {}
def get_method_name(self):
return self._get_sub_text('methodName')
def set_method_name(self, value):
return self._set_sub_text('methodName', value)
def get_params(self):
return self.xml.find('{%s}params' % self.namespace)
def set_params(self, params):
self.append(params)
class MethodResponse(ElementBase):
name = 'methodResponse'
namespace = 'jabber:iq:rpc'
plugin_attrib = 'method_response'
interfaces = set(('params', 'fault'))
subinterfaces = set(())
plugin_attrib_map = {}
plugin_tag_map = {}
def get_params(self):
return self.xml.find('{%s}params' % self.namespace)
def set_params(self, params):
self.append(params)
def get_fault(self):
return self.xml.find('{%s}fault' % self.namespace)
def set_fault(self, fault):
self.append(fault)
@@ -0,0 +1,9 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON).
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
+19
View File
@@ -0,0 +1,19 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0012.stanza import LastActivity
from slixmpp.plugins.xep_0012.last_activity import XEP_0012
register_plugin(XEP_0012)
# Retain some backwards compatibility
xep_0004 = XEP_0012
+157
View File
@@ -0,0 +1,157 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from datetime import datetime, timedelta
from slixmpp.plugins import BasePlugin, register_plugin
from slixmpp import Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream import JID, register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins.xep_0012 import stanza, LastActivity
log = logging.getLogger(__name__)
class XEP_0012(BasePlugin):
"""
XEP-0012 Last Activity
"""
name = 'xep_0012'
description = 'XEP-0012: Last Activity'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Iq, LastActivity)
self._last_activities = {}
self.xmpp.register_handler(
Callback('Last Activity',
StanzaPath('iq@type=get/last_activity'),
self._handle_get_last_activity))
self.api.register(self._default_get_last_activity,
'get_last_activity',
default=True)
self.api.register(self._default_set_last_activity,
'set_last_activity',
default=True)
self.api.register(self._default_del_last_activity,
'del_last_activity',
default=True)
def plugin_end(self):
self.xmpp.remove_handler('Last Activity')
self.xmpp['xep_0030'].del_feature(feature='jabber:iq:last')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('jabber:iq:last')
def begin_idle(self, jid=None, status=None):
self.set_last_activity(jid, 0, status)
def end_idle(self, jid=None):
self.del_last_activity(jid)
def start_uptime(self, status=None):
self.set_last_activity(jid, 0, status)
def set_last_activity(self, jid=None, seconds=None, status=None):
self.api['set_last_activity'](jid, args={
'seconds': seconds,
'status': status})
def del_last_activity(self, jid):
self.api['del_last_activity'](jid)
def get_last_activity(self, jid, local=False, ifrom=None, block=True,
timeout=None, callback=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
if local or jid in (None, ''):
log.debug("Looking up local last activity data for %s", jid)
return self.api['get_last_activity'](jid, None, ifrom, None)
iq = self.xmpp.Iq()
iq['from'] = ifrom
iq['to'] = jid
iq['type'] = 'get'
iq.enable('last_activity')
return iq.send(timeout=timeout,
block=block,
callback=callback)
def _handle_get_last_activity(self, iq):
log.debug("Received last activity query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
reply = self.api['get_last_activity'](iq['to'], None, iq['from'], iq)
reply.send()
# =================================================================
# Default in-memory implementations for storing last activity data.
# =================================================================
def _default_set_last_activity(self, jid, node, ifrom, data):
seconds = data.get('seconds', None)
if seconds is None:
seconds = 0
status = data.get('status', None)
if status is None:
status = ''
self._last_activities[jid] = {
'seconds': datetime.now() - timedelta(seconds=seconds),
'status': status}
def _default_del_last_activity(self, jid, node, ifrom, data):
if jid in self._last_activities:
del self._last_activities[jid]
def _default_get_last_activity(self, jid, node, ifrom, iq):
if not isinstance(iq, Iq):
reply = self.xmpp.Iq()
else:
iq.reply()
reply = iq
if jid not in self._last_activities:
raise XMPPError('service-unavailable')
bare = JID(jid).bare
if bare != self.xmpp.boundjid.bare:
if bare in self.xmpp.roster[jid]:
sub = self.xmpp.roster[jid][bare]['subscription']
if sub not in ('from', 'both'):
raise XMPPError('forbidden')
td = datetime.now() - self._last_activities[jid]['seconds']
seconds = td.seconds + td.days * 24 * 3600
status = self._last_activities[jid]['status']
reply['last_activity']['seconds'] = seconds
reply['last_activity']['status'] = status
return reply
+32
View File
@@ -0,0 +1,32 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class LastActivity(ElementBase):
name = 'query'
namespace = 'jabber:iq:last'
plugin_attrib = 'last_activity'
interfaces = set(('seconds', 'status'))
def get_seconds(self):
return int(self._get_attr('seconds'))
def set_seconds(self, value):
self._set_attr('seconds', str(value))
def get_status(self):
return self.xml.text
def set_status(self, value):
self.xml.text = str(value)
def del_status(self):
self.xml.text = ''
+15
View File
@@ -0,0 +1,15 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permissio
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0013.stanza import Offline
from slixmpp.plugins.xep_0013.offline import XEP_0013
register_plugin(XEP_0013)
+134
View File
@@ -0,0 +1,134 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permissio
"""
import logging
import slixmpp
from slixmpp.stanza import Message, Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream.handler import Collector
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.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)
+53
View File
@@ -0,0 +1,53 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permissio
"""
from slixmpp.jid import JID
from slixmpp.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)
+16
View File
@@ -0,0 +1,16 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0016 import stanza
from slixmpp.plugins.xep_0016.stanza import Privacy
from slixmpp.plugins.xep_0016.privacy import XEP_0016
register_plugin(XEP_0016)
+110
View File
@@ -0,0 +1,110 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp import Iq
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0016 import stanza
from slixmpp.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)
+103
View File
@@ -0,0 +1,103 @@
from slixmpp.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)
+16
View File
@@ -0,0 +1,16 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0020 import stanza
from slixmpp.plugins.xep_0020.stanza import FeatureNegotiation
from slixmpp.plugins.xep_0020.feature_negotiation import XEP_0020
register_plugin(XEP_0020)
@@ -0,0 +1,36 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp import Iq, Message
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin, JID
from slixmpp.plugins.xep_0020 import stanza, FeatureNegotiation
from slixmpp.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)
+17
View File
@@ -0,0 +1,17 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class FeatureNegotiation(ElementBase):
name = 'feature'
namespace = 'http://jabber.org/protocol/feature-neg'
plugin_attrib = 'feature_neg'
interfaces = set()
+15
View File
@@ -0,0 +1,15 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0027.stanza import Signed, Encrypted
from slixmpp.plugins.xep_0027.gpg import XEP_0027
register_plugin(XEP_0027)
+170
View File
@@ -0,0 +1,170 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.thirdparty import GPG
from slixmpp.stanza import Presence, Message
from slixmpp.plugins.base import BasePlugin, register_plugin
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins.xep_0027 import stanza, Signed, Encrypted
def _extract_data(data, kind):
stripped = []
begin_headers = False
begin_data = False
for line in data.split('\n'):
if not begin_headers and 'BEGIN PGP %s' % kind in line:
begin_headers = True
continue
if begin_headers and line.strip() == '':
begin_data = True
continue
if 'END PGP %s' % kind in line:
return '\n'.join(stripped)
if begin_data:
stripped.append(line)
return ''
class XEP_0027(BasePlugin):
name = 'xep_0027'
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 = GPG(gnupghome=self.gpg_home,
gpgbinary=self.gpg_binary,
use_agent=self.use_agent,
keyring=self.keyring)
self.xmpp.add_filter('out', self._sign_presence)
self._keyids = {}
self.api.register(self._set_keyid, 'set_keyid', default=True)
self.api.register(self._get_keyid, 'get_keyid', default=True)
self.api.register(self._del_keyid, 'del_keyid', default=True)
self.api.register(self._get_keyids, 'get_keyids', default=True)
register_stanza_plugin(Presence, Signed)
register_stanza_plugin(Message, Encrypted)
self.xmpp.add_event_handler('unverified_signed_presence',
self._handle_unverified_signed_presence,
threaded=True)
self.xmpp.register_handler(
Callback('Signed Presence',
StanzaPath('presence/signed'),
self._handle_signed_presence))
self.xmpp.register_handler(
Callback('Encrypted Message',
StanzaPath('message/encrypted'),
self._handle_encrypted_message))
def plugin_end(self):
self.xmpp.remove_handler('Encrypted Message')
self.xmpp.remove_handler('Signed Presence')
self.xmpp.del_filter('out', self._sign_presence)
self.xmpp.del_event_handler('unverified_signed_presence',
self._handle_unverified_signed_presence)
def _sign_presence(self, stanza):
if isinstance(stanza, Presence):
if stanza['type'] == 'available' or \
stanza['type'] in Presence.showtypes:
stanza['signed'] = stanza['status']
return stanza
def sign(self, data, jid=None):
keyid = self.get_keyid(jid)
if keyid:
signed = self.gpg.sign(data, keyid=keyid)
return _extract_data(signed.data, 'SIGNATURE')
def encrypt(self, data, jid=None):
keyid = self.get_keyid(jid)
if keyid:
enc = self.gpg.encrypt(data, keyid)
return _extract_data(enc.data, 'MESSAGE')
def decrypt(self, data, jid=None):
template = '-----BEGIN PGP MESSAGE-----\n' + \
'\n' + \
'%s\n' + \
'-----END PGP MESSAGE-----\n'
dec = self.gpg.decrypt(template % data)
return dec.data
def verify(self, data, sig, jid=None):
template = '-----BEGIN PGP SIGNED MESSAGE-----\n' + \
'Hash: SHA1\n' + \
'\n' + \
'%s\n' + \
'-----BEGIN PGP SIGNATURE-----\n' + \
'\n' + \
'%s\n' + \
'-----END PGP SIGNATURE-----\n'
v = self.gpg.verify(template % (data, sig))
return v
def set_keyid(self, jid=None, keyid=None):
self.api['set_keyid'](jid, args=keyid)
def get_keyid(self, jid=None):
return self.api['get_keyid'](jid)
def del_keyid(self, jid=None):
self.api['del_keyid'](jid)
def get_keyids(self):
return self.api['get_keyids']()
def _handle_signed_presence(self, pres):
self.xmpp.event('unverified_signed_presence', pres)
def _handle_unverified_signed_presence(self, pres):
verified = self.verify(pres['status'], pres['signed'])
if verified.key_id:
if not self.get_keyid(pres['from']):
known_keyids = [e['keyid'] for e in self.gpg.list_keys()]
if verified.key_id not in known_keyids:
self.gpg.recv_keys(self.key_server, verified.key_id)
self.set_keyid(jid=pres['from'], keyid=verified.key_id)
self.xmpp.event('signed_presence', pres)
def _handle_encrypted_message(self, msg):
self.xmpp.event('encrypted_message', msg)
# =================================================================
def _set_keyid(self, jid, node, ifrom, keyid):
self._keyids[jid] = keyid
def _get_keyid(self, jid, node, ifrom, keyid):
return self._keyids.get(jid, None)
def _del_keyid(self, jid, node, ifrom, keyid):
if jid in self._keyids:
del self._keyids[jid]
def _get_keyids(self, jid, node, ifrom, data):
return self._keyids
+53
View File
@@ -0,0 +1,53 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class Signed(ElementBase):
name = 'x'
namespace = 'jabber:x:signed'
plugin_attrib = 'signed'
interfaces = set(['signed'])
is_extension = True
def set_signed(self, value):
parent = self.parent()
xmpp = parent.stream
data = xmpp['xep_0027'].sign(value, parent['from'])
if data:
self.xml.text = data
else:
del parent['signed']
def get_signed(self):
return self.xml.text
class Encrypted(ElementBase):
name = 'x'
namespace = 'jabber:x:encrypted'
plugin_attrib = 'encrypted'
interfaces = set(['encrypted'])
is_extension = True
def set_encrypted(self, value):
parent = self.parent()
xmpp = parent.stream
data = xmpp['xep_0027'].encrypt(value, parent['to'])
if data:
self.xml.text = data
else:
del parent['encrypted']
def get_encrypted(self):
parent = self.parent()
xmpp = parent.stream
if self.xml.text:
return xmpp['xep_0027'].decrypt(self.xml.text, parent['to'])
return None
+23
View File
@@ -0,0 +1,23 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0030 import stanza
from slixmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems
from slixmpp.plugins.xep_0030.static import StaticDisco
from slixmpp.plugins.xep_0030.disco import XEP_0030
register_plugin(XEP_0030)
# Retain some backwards compatibility
xep_0030 = XEP_0030
XEP_0030.getInfo = XEP_0030.get_info
XEP_0030.getItems = XEP_0030.get_items
XEP_0030.make_static = XEP_0030.restore_defaults
+740
View File
@@ -0,0 +1,740 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp import Iq
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin, JID
from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems
from slixmpp.plugins.xep_0030 import StaticDisco
log = logging.getLogger(__name__)
class XEP_0030(BasePlugin):
"""
XEP-0030: Service Discovery
Service discovery in XMPP allows entities to discover information about
other agents in the network, such as the feature sets supported by a
client, or signposts to other, related entities.
Also see <http://www.xmpp.org/extensions/xep-0030.html>.
The XEP-0030 plugin works using a hierarchy of dynamic
node handlers, ranging from global handlers to specific
JID+node handlers. The default set of handlers operate
in a static manner, storing disco information in memory.
However, custom handlers may use any available backend
storage mechanism desired, such as SQLite or Redis.
Node handler hierarchy:
JID | Node | Level
---------------------
None | None | Global
Given | None | All nodes for the JID
None | Given | Node on self.xmpp.boundjid
Given | Given | A single node
Stream Handlers:
Disco Info -- Any Iq stanze that includes a query with the
namespace http://jabber.org/protocol/disco#info.
Disco Items -- Any Iq stanze that includes a query with the
namespace http://jabber.org/protocol/disco#items.
Events:
disco_info -- Received a disco#info Iq query result.
disco_items -- Received a disco#items Iq query result.
disco_info_query -- Received a disco#info Iq query request.
disco_items_query -- Received a disco#items Iq query request.
Attributes:
stanza -- A reference to the module containing the
stanza classes provided by this plugin.
static -- Object containing the default set of
static node handlers.
default_handlers -- A dictionary mapping operations to the default
global handler (by default, the static handlers).
xmpp -- The main Slixmpp object.
Methods:
set_node_handler -- Assign a handler to a JID/node combination.
del_node_handler -- Remove a handler from a JID/node combination.
get_info -- Retrieve disco#info data, locally or remote.
get_items -- Retrieve disco#items data, locally or remote.
set_identities --
set_features --
set_items --
del_items --
del_identity --
del_feature --
del_item --
add_identity --
add_feature --
add_item --
"""
name = 'xep_0030'
description = 'XEP-0030: Service Discovery'
dependencies = set()
stanza = stanza
default_config = {
'use_cache': True,
'wrap_results': False
}
def plugin_init(self):
"""
Start the XEP-0030 plugin.
"""
self.xmpp.register_handler(
Callback('Disco Info',
StanzaPath('iq/disco_info'),
self._handle_disco_info))
self.xmpp.register_handler(
Callback('Disco Items',
StanzaPath('iq/disco_items'),
self._handle_disco_items))
register_stanza_plugin(Iq, DiscoInfo)
register_stanza_plugin(Iq, DiscoItems)
self.static = StaticDisco(self.xmpp, self)
self._disco_ops = [
'get_info', 'set_info', 'set_identities', 'set_features',
'get_items', 'set_items', 'del_items', 'add_identity',
'del_identity', 'add_feature', 'del_feature', 'add_item',
'del_item', 'del_identities', 'del_features', 'cache_info',
'get_cached_info', 'supports', 'has_identity']
for op in self._disco_ops:
self.api.register(getattr(self.static, op), op, default=True)
def _add_disco_op(self, op, default_handler):
self.api.register(default_handler, op)
self.api.register_default(default_handler, op)
def set_node_handler(self, htype, jid=None, node=None, handler=None):
"""
Add a node handler for the given hierarchy level and
handler type.
Node handlers are ordered in a hierarchy where the
most specific handler is executed. Thus, a fallback,
global handler can be used for the majority of cases
with a few node specific handler that override the
global behavior.
Node handler hierarchy:
JID | Node | Level
---------------------
None | None | Global
Given | None | All nodes for the JID
None | Given | Node on self.xmpp.boundjid
Given | Given | A single node
Handler types:
get_info
get_items
set_identities
set_features
set_items
del_items
del_identities
del_identity
del_feature
del_features
del_item
add_identity
add_feature
add_item
Arguments:
htype -- The operation provided by the handler.
jid -- The JID the handler applies to. May be narrowed
further if a node is given.
node -- The particular node the handler is for. If no JID
is given, then the self.xmpp.boundjid.full is
assumed.
handler -- The handler function to use.
"""
self.api.register(handler, htype, jid, node)
def del_node_handler(self, htype, jid, node):
"""
Remove a handler type for a JID and node combination.
The next handler in the hierarchy will be used if one
exists. If removing the global handler, make sure that
other handlers exist to process existing nodes.
Node handler hierarchy:
JID | Node | Level
---------------------
None | None | Global
Given | None | All nodes for the JID
None | Given | Node on self.xmpp.boundjid
Given | Given | A single node
Arguments:
htype -- The type of handler to remove.
jid -- The JID from which to remove the handler.
node -- The node from which to remove the handler.
"""
self.api.unregister(htype, jid, node)
def restore_defaults(self, jid=None, node=None, handlers=None):
"""
Change all or some of a node's handlers to the default
handlers. Useful for manually overriding the contents
of a node that would otherwise be handled by a JID level
or global level dynamic handler.
The default is to use the built-in static handlers, but that
may be changed by modifying self.default_handlers.
Arguments:
jid -- The JID owning the node to modify.
node -- The node to change to using static handlers.
handlers -- Optional list of handlers to change to the
default version. If provided, only these
handlers will be changed. Otherwise, all
handlers will use the default version.
"""
if handlers is None:
handlers = self._disco_ops
for op in handlers:
self.api.restore_default(op, jid, node)
def supports(self, jid=None, node=None, feature=None, local=False,
cached=True, ifrom=None):
"""
Check if a JID supports a given feature.
Return values:
True -- The feature is supported
False -- The feature is not listed as supported
None -- Nothing could be found due to a timeout
Arguments:
jid -- Request info from this JID.
node -- The particular node to query.
feature -- The name of the feature to check.
local -- If true, then the query is for a JID/node
combination handled by this Slixmpp instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
ifrom -- Specifiy the sender's JID.
"""
data = {'feature': feature,
'local': local,
'cached': cached}
return self.api['supports'](jid, node, ifrom, data)
def has_identity(self, jid=None, node=None, category=None, itype=None,
lang=None, local=False, cached=True, ifrom=None):
"""
Check if a JID provides a given identity.
Return values:
True -- The identity is provided
False -- The identity is not listed
None -- Nothing could be found due to a timeout
Arguments:
jid -- Request info from this JID.
node -- The particular node to query.
category -- The category of the identity to check.
itype -- The type of the identity to check.
lang -- The language of the identity to check.
local -- If true, then the query is for a JID/node
combination handled by this Slixmpp instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
ifrom -- Specifiy the sender's JID.
"""
data = {'category': category,
'itype': itype,
'lang': lang,
'local': local,
'cached': cached}
return self.api['has_identity'](jid, node, ifrom, data)
def get_info(self, jid=None, node=None, local=None,
cached=None, **kwargs):
"""
Retrieve the disco#info results from a given JID/node combination.
Info may be retrieved from both local resources and remote agents;
the local parameter indicates if the information should be gathered
by executing the local node handlers, or if a disco#info stanza
must be generated and sent.
If requesting items from a local JID/node, then only a DiscoInfo
stanza will be returned. Otherwise, an Iq stanza will be returned.
Arguments:
jid -- Request info from this JID.
node -- The particular node to query.
local -- If true, then the query is for a JID/node
combination handled by this Slixmpp instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for
a reply. If None, then wait indefinitely. The
timeout value is only used when block=True.
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 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 " + \
"for %s, node %s.", jid, node)
info = self.api['get_info'](jid, node,
kwargs.get('ifrom', None),
kwargs)
info = self._fix_default_info(info)
return self._wrap(kwargs.get('ifrom', None), jid, info)
if cached:
log.debug("Looking up cached disco#info data " + \
"for %s, node %s.", jid, node)
info = self.api['get_cached_info'](jid, node,
kwargs.get('ifrom', None),
kwargs)
if info is not None:
return self._wrap(kwargs.get('ifrom', None), jid, info)
iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility
iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
iq['to'] = jid
iq['type'] = 'get'
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),
timeout_callback=kwargs.get('timeout_callback', None))
def set_info(self, jid=None, node=None, info=None):
"""
Set the disco#info data for a JID/node based on an existing
disco#info stanza.
"""
if isinstance(info, Iq):
info = info['disco_info']
self.api['set_info'](jid, node, None, info)
def get_items(self, jid=None, node=None, local=False, **kwargs):
"""
Retrieve the disco#items results from a given JID/node combination.
Items may be retrieved from both local resources and remote agents;
the local parameter indicates if the items should be gathered by
executing the local node handlers, or if a disco#items stanza must
be generated and sent.
If requesting items from a local JID/node, then only a DiscoItems
stanza will be returned. Otherwise, an Iq stanza will be returned.
Arguments:
jid -- Request info from this JID.
node -- The particular node to query.
local -- If true, then the query is for a JID/node
combination handled by this Slixmpp instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the items.
ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for
a reply. If None, then wait indefinitely.
callback -- Optional callback to execute when a reply is
received instead of blocking and waiting for
the reply.
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 local is None and jid is None:
items = self.api['get_items'](jid, node,
kwargs.get('ifrom', None),
kwargs)
return self._wrap(kwargs.get('ifrom', None), jid, items)
iq = self.xmpp.Iq()
# Check dfrom parameter for backwards compatibility
iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
iq['to'] = jid
iq['type'] = 'get'
iq['disco_items']['node'] = node if node else ''
if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
else:
return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', True),
callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None))
def set_items(self, jid=None, node=None, **kwargs):
"""
Set or replace all items for the specified JID/node combination.
The given items must be in a list or set where each item is a
tuple of the form: (jid, node, name).
Arguments:
jid -- The JID to modify.
node -- Optional node to modify.
items -- A series of items in tuple format.
"""
self.api['set_items'](jid, node, None, kwargs)
def del_items(self, jid=None, node=None, **kwargs):
"""
Remove all items from the given JID/node combination.
Arguments:
jid -- The JID to modify.
node -- Optional node to modify.
"""
self.api['del_items'](jid, node, None, kwargs)
def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
"""
Add a new item element to the given JID/node combination.
Each item is required to have a JID, but may also specify
a node value to reference non-addressable entities.
Arguments:
jid -- The JID for the item.
name -- Optional name for the item.
node -- The node to modify.
subnode -- Optional node for the item.
ijid -- The JID to modify.
"""
if not jid:
jid = self.xmpp.boundjid.full
kwargs = {'ijid': jid,
'name': name,
'inode': subnode}
self.api['add_item'](ijid, node, None, kwargs)
def del_item(self, jid=None, node=None, **kwargs):
"""
Remove a single item from the given JID/node combination.
Arguments:
jid -- The JID to modify.
node -- The node to modify.
ijid -- The item's JID.
inode -- The item's node.
"""
self.api['del_item'](jid, node, None, kwargs)
def add_identity(self, category='', itype='', name='',
node=None, jid=None, lang=None):
"""
Add a new identity to the given JID/node combination.
Each identity must be unique in terms of all four identity
components: category, type, name, and language.
Multiple, identical category/type pairs are allowed only
if the xml:lang values are different. Likewise, multiple
category/type/xml:lang pairs are allowed so long as the
names are different. A category and type is always required.
Arguments:
category -- The identity's category.
itype -- The identity's type.
name -- Optional name for the identity.
lang -- Optional two-letter language code.
node -- The node to modify.
jid -- The JID to modify.
"""
kwargs = {'category': category,
'itype': itype,
'name': name,
'lang': lang}
self.api['add_identity'](jid, node, None, kwargs)
def add_feature(self, feature, node=None, jid=None):
"""
Add a feature to a JID/node combination.
Arguments:
feature -- The namespace of the supported feature.
node -- The node to modify.
jid -- The JID to modify.
"""
kwargs = {'feature': feature}
self.api['add_feature'](jid, node, None, kwargs)
def del_identity(self, jid=None, node=None, **kwargs):
"""
Remove an identity from the given JID/node combination.
Arguments:
jid -- The JID to modify.
node -- The node to modify.
category -- The identity's category.
itype -- The identity's type value.
name -- Optional, human readable name for the identity.
lang -- Optional, the identity's xml:lang value.
"""
self.api['del_identity'](jid, node, None, kwargs)
def del_feature(self, jid=None, node=None, **kwargs):
"""
Remove a feature from a given JID/node combination.
Arguments:
jid -- The JID to modify.
node -- The node to modify.
feature -- The feature's namespace.
"""
self.api['del_feature'](jid, node, None, kwargs)
def set_identities(self, jid=None, node=None, **kwargs):
"""
Add or replace all identities for the given JID/node combination.
The identities must be in a set where each identity is a tuple
of the form: (category, type, lang, name)
Arguments:
jid -- The JID to modify.
node -- The node to modify.
identities -- A set of identities in tuple form.
lang -- Optional, xml:lang value.
"""
self.api['set_identities'](jid, node, None, kwargs)
def del_identities(self, jid=None, node=None, **kwargs):
"""
Remove all identities for a JID/node combination.
If a language is specified, only identities using that
language will be removed.
Arguments:
jid -- The JID to modify.
node -- The node to modify.
lang -- Optional. If given, only remove identities
using this xml:lang value.
"""
self.api['del_identities'](jid, node, None, kwargs)
def set_features(self, jid=None, node=None, **kwargs):
"""
Add or replace the set of supported features
for a JID/node combination.
Arguments:
jid -- The JID to modify.
node -- The node to modify.
features -- The new set of supported features.
"""
self.api['set_features'](jid, node, None, kwargs)
def del_features(self, jid=None, node=None, **kwargs):
"""
Remove all features from a JID/node combination.
Arguments:
jid -- The JID to modify.
node -- The node to modify.
"""
self.api['del_features'](jid, node, None, kwargs)
def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
"""
Execute the most specific node handler for the given
JID/node combination.
Arguments:
htype -- The handler type to execute.
jid -- The JID requested.
node -- The node requested.
data -- Optional, custom data to pass to the handler.
"""
return self.api[htype](jid, node, ifrom, data)
def _handle_disco_info(self, iq):
"""
Process an incoming disco#info stanza. If it is a get
request, find and return the appropriate identities
and features. If it is an info result, fire the
disco_info event.
Arguments:
iq -- The incoming disco#items stanza.
"""
if iq['type'] == 'get':
log.debug("Received disco info query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
info = self.api['get_info'](iq['to'],
iq['disco_info']['node'],
iq['from'],
iq)
if isinstance(info, Iq):
info['id'] = iq['id']
info.send()
else:
iq.reply()
if info:
info = self._fix_default_info(info)
iq.set_payload(info.xml)
iq.send()
elif iq['type'] == 'result':
log.debug("Received disco info result from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
if self.use_cache:
log.debug("Caching disco info result from " \
"<%s> to <%s>.", iq['from'], iq['to'])
if self.xmpp.is_component:
ito = iq['to'].full
else:
ito = None
self.api['cache_info'](iq['from'],
iq['disco_info']['node'],
ito,
iq)
self.xmpp.event('disco_info', iq)
def _handle_disco_items(self, iq):
"""
Process an incoming disco#items stanza. If it is a get
request, find and return the appropriate items. If it
is an items result, fire the disco_items event.
Arguments:
iq -- The incoming disco#items stanza.
"""
if iq['type'] == 'get':
log.debug("Received disco items query from " + \
"<%s> to <%s>.", iq['from'], iq['to'])
items = self.api['get_items'](iq['to'],
iq['disco_items']['node'],
iq['from'],
iq)
if isinstance(items, Iq):
items.send()
else:
iq.reply()
if items:
iq.set_payload(items.xml)
iq.send()
elif iq['type'] == 'result':
log.debug("Received disco items result from " + \
"%s to %s.", iq['from'], iq['to'])
self.xmpp.event('disco_items', iq)
def _fix_default_info(self, info):
"""
Disco#info results for a JID are required to include at least
one identity and feature. As a default, if no other identity is
provided, Slixmpp will use either the generic component or the
bot client identity. A the standard disco#info feature will also be
added if no features are provided.
Arguments:
info -- The disco#info quest (not the full Iq stanza) to modify.
"""
result = info
if isinstance(info, Iq):
info = info['disco_info']
if not info['node']:
if not info['identities']:
if self.xmpp.is_component:
log.debug("No identity found for this entity. " + \
"Using default component identity.")
info.add_identity('component', 'generic')
else:
log.debug("No identity found for this entity. " + \
"Using default client identity.")
info.add_identity('client', 'bot')
if not info['features']:
log.debug("No features found for this entity. " + \
"Using default disco#info feature.")
info.add_feature(info.namespace)
return result
def _wrap(self, ito, ifrom, payload, force=False):
"""
Ensure that results are wrapped in an Iq stanza
if self.wrap_results has been set to True.
Arguments:
ito -- The JID to use as the 'to' value
ifrom -- The JID to use as the 'from' value
payload -- The disco data to wrap
force -- Force wrapping, regardless of self.wrap_results
"""
if (force or self.wrap_results) and not isinstance(payload, Iq):
iq = self.xmpp.Iq()
# Since we're simulating a result, we have to treat
# the 'from' and 'to' values opposite the normal way.
iq['to'] = self.xmpp.boundjid if ito is None else ito
iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
iq['type'] = 'result'
iq.append(payload)
return iq
return payload
@@ -0,0 +1,10 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo
from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
+276
View File
@@ -0,0 +1,276 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, ET
class DiscoInfo(ElementBase):
"""
XMPP allows for users and agents to find the identities and features
supported by other entities in the XMPP network through service discovery,
or "disco". In particular, the "disco#info" query type for <iq> stanzas is
used to request the list of identities and features offered by a JID.
An identity is a combination of a category and type, such as the 'client'
category with a type of 'pc' to indicate the agent is a human operated
client with a GUI, or a category of 'gateway' with a type of 'aim' to
identify the agent as a gateway for the legacy AIM protocol. See
<http://xmpp.org/registrar/disco-categories.html> for a full list of
accepted category and type combinations.
Features are simply a set of the namespaces that identify the supported
features. For example, a client that supports service discovery will
include the feature 'http://jabber.org/protocol/disco#info'.
Since clients and components may operate in several roles at once, identity
and feature information may be grouped into "nodes". If one were to write
all of the identities and features used by a client, then node names would
be like section headings.
Example disco#info stanzas:
<iq type="get">
<query xmlns="http://jabber.org/protocol/disco#info" />
</iq>
<iq type="result">
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="client" type="bot" name="Slixmpp Bot" />
<feature var="http://jabber.org/protocol/disco#info" />
<feature var="jabber:x:data" />
<feature var="urn:xmpp:ping" />
</query>
</iq>
Stanza Interface:
node -- The name of the node to either
query or return info from.
identities -- A set of 4-tuples, where each tuple contains
the category, type, xml:lang, and name
of an identity.
features -- A set of namespaces for features.
Methods:
add_identity -- Add a new, single identity.
del_identity -- Remove a single identity.
get_identities -- Return all identities in tuple form.
set_identities -- Use multiple identities, each given in tuple form.
del_identities -- Remove all identities.
add_feature -- Add a single feature.
del_feature -- Remove a single feature.
get_features -- Return a list of all features.
set_features -- Use a given list of features.
del_features -- Remove all features.
"""
name = 'query'
namespace = 'http://jabber.org/protocol/disco#info'
plugin_attrib = 'disco_info'
interfaces = set(('node', 'features', 'identities'))
lang_interfaces = set(('identities',))
# Cache identities and features
_identities = set()
_features = set()
def setup(self, xml=None):
"""
Populate the stanza object using an optional XML object.
Overrides ElementBase.setup
Caches identity and feature information.
Arguments:
xml -- Use an existing XML object for the stanza's values.
"""
ElementBase.setup(self, xml)
self._identities = set([id[0:3] for id in self['identities']])
self._features = self['features']
def add_identity(self, category, itype, name=None, lang=None):
"""
Add a new identity element. Each identity must be unique
in terms of all four identity components.
Multiple, identical category/type pairs are allowed only
if the xml:lang values are different. Likewise, multiple
category/type/xml:lang pairs are allowed so long as the names
are different. In any case, a category and type are required.
Arguments:
category -- The general category to which the agent belongs.
itype -- A more specific designation with the category.
name -- Optional human readable name for this identity.
lang -- Optional standard xml:lang value.
"""
identity = (category, itype, lang)
if identity not in self._identities:
self._identities.add(identity)
id_xml = ET.Element('{%s}identity' % self.namespace)
id_xml.attrib['category'] = category
id_xml.attrib['type'] = itype
if lang:
id_xml.attrib['{%s}lang' % self.xml_ns] = lang
if name:
id_xml.attrib['name'] = name
self.xml.append(id_xml)
return True
return False
def del_identity(self, category, itype, name=None, lang=None):
"""
Remove a given identity.
Arguments:
category -- The general category to which the agent belonged.
itype -- A more specific designation with the category.
name -- Optional human readable name for this identity.
lang -- Optional, standard xml:lang value.
"""
identity = (category, itype, lang)
if identity in self._identities:
self._identities.remove(identity)
for id_xml in self.findall('{%s}identity' % self.namespace):
id = (id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None))
if id == identity:
self.xml.remove(id_xml)
return True
return False
def get_identities(self, lang=None, dedupe=True):
"""
Return a set of all identities in tuple form as so:
(category, type, lang, name)
If a language was specified, only return identities using
that language.
Arguments:
lang -- Optional, standard xml:lang value.
dedupe -- If True, de-duplicate identities, otherwise
return a list of all identities.
"""
if dedupe:
identities = set()
else:
identities = []
for id_xml in self.findall('{%s}identity' % self.namespace):
xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None)
if lang is None or xml_lang == lang:
id = (id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None),
id_xml.attrib.get('name', None))
if dedupe:
identities.add(id)
else:
identities.append(id)
return identities
def set_identities(self, identities, lang=None):
"""
Add or replace all identities. The identities must be a in set
where each identity is a tuple of the form:
(category, type, lang, name)
If a language is specifified, any identities using that language
will be removed to be replaced with the given identities.
NOTE: An identity's language will not be changed regardless of
the value of lang.
Arguments:
identities -- A set of identities in tuple form.
lang -- Optional, standard xml:lang value.
"""
self.del_identities(lang)
for identity in identities:
category, itype, lang, name = identity
self.add_identity(category, itype, name, lang)
def del_identities(self, lang=None):
"""
Remove all identities. If a language was specified, only
remove identities using that language.
Arguments:
lang -- Optional, standard xml:lang value.
"""
for id_xml in self.findall('{%s}identity' % self.namespace):
if lang is None:
self.xml.remove(id_xml)
elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang:
self._identities.remove((
id_xml.attrib['category'],
id_xml.attrib['type'],
id_xml.attrib.get('{%s}lang' % self.xml_ns, None)))
self.xml.remove(id_xml)
def add_feature(self, feature):
"""
Add a single, new feature.
Arguments:
feature -- The namespace of the supported feature.
"""
if feature not in self._features:
self._features.add(feature)
feature_xml = ET.Element('{%s}feature' % self.namespace)
feature_xml.attrib['var'] = feature
self.xml.append(feature_xml)
return True
return False
def del_feature(self, feature):
"""
Remove a single feature.
Arguments:
feature -- The namespace of the removed feature.
"""
if feature in self._features:
self._features.remove(feature)
for feature_xml in self.findall('{%s}feature' % self.namespace):
if feature_xml.attrib['var'] == feature:
self.xml.remove(feature_xml)
return True
return False
def get_features(self, dedupe=True):
"""Return the set of all supported features."""
if dedupe:
features = set()
else:
features = []
for feature_xml in self.findall('{%s}feature' % self.namespace):
if dedupe:
features.add(feature_xml.attrib['var'])
else:
features.append(feature_xml.attrib['var'])
return features
def set_features(self, features):
"""
Add or replace the set of supported features.
Arguments:
features -- The new set of supported features.
"""
self.del_features()
for feature in features:
self.add_feature(feature)
def del_features(self):
"""Remove all features."""
self._features = set()
for feature_xml in self.findall('{%s}feature' % self.namespace):
self.xml.remove(feature_xml)
+152
View File
@@ -0,0 +1,152 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
class DiscoItems(ElementBase):
"""
Example disco#items stanzas:
<iq type="get">
<query xmlns="http://jabber.org/protocol/disco#items" />
</iq>
<iq type="result">
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="chat.example.com"
node="xmppdev"
name="XMPP Dev" />
<item jid="chat.example.com"
node="slixdev"
name="Slixmpp Dev" />
</query>
</iq>
Stanza Interface:
node -- The name of the node to either
query or return info from.
items -- A list of 3-tuples, where each tuple contains
the JID, node, and name of an item.
Methods:
add_item -- Add a single new item.
del_item -- Remove a single item.
get_items -- Return all items.
set_items -- Set or replace all items.
del_items -- Remove all items.
"""
name = 'query'
namespace = 'http://jabber.org/protocol/disco#items'
plugin_attrib = 'disco_items'
interfaces = set(('node', 'items'))
# Cache items
_items = 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._items = set([item[0:2] for item in self['items']])
def add_item(self, jid, node=None, name=None):
"""
Add a new item element. Each item is required to have a
JID, but may also specify a node value to reference
non-addressable entitities.
Arguments:
jid -- The JID for the item.
node -- Optional additional information to reference
non-addressable items.
name -- Optional human readable name for the item.
"""
if (jid, node) not in self._items:
self._items.add((jid, node))
item = DiscoItem(parent=self)
item['jid'] = jid
item['node'] = node
item['name'] = name
self.iterables.append(item)
return True
return False
def del_item(self, jid, node=None):
"""
Remove a single item.
Arguments:
jid -- JID of the item to remove.
node -- Optional extra identifying information.
"""
if (jid, node) in self._items:
for item_xml in self.findall('{%s}item' % self.namespace):
item = (item_xml.attrib['jid'],
item_xml.attrib.get('node', None))
if item == (jid, node):
self.xml.remove(item_xml)
return True
return False
def get_items(self):
"""Return all items."""
items = set()
for item in self['substanzas']:
if isinstance(item, DiscoItem):
items.add((item['jid'], item['node'], item['name']))
return items
def set_items(self, items):
"""
Set or replace all items. The given items must be in a
list or set where each item is a tuple of the form:
(jid, node, name)
Arguments:
items -- A series of items in tuple format.
"""
self.del_items()
for item in items:
jid, node, name = item
self.add_item(jid, node, name)
def del_items(self):
"""Remove all items."""
self._items = set()
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):
name = 'item'
namespace = 'http://jabber.org/protocol/disco#items'
plugin_attrib = name
interfaces = set(('jid', 'node', 'name'))
def get_node(self):
"""Return the item's node name or ``None``."""
return self._get_attr('node', None)
def get_name(self):
"""Return the item's human readable name, or ``None``."""
return self._get_attr('name', None)
register_stanza_plugin(DiscoItems, DiscoItem, iterable=True)
+430
View File
@@ -0,0 +1,430 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
import threading
from slixmpp import Iq
from slixmpp.exceptions import XMPPError, IqError, IqTimeout
from slixmpp.xmlstream import JID
from slixmpp.plugins.xep_0030 import DiscoInfo, DiscoItems
log = logging.getLogger(__name__)
class StaticDisco(object):
"""
While components will likely require fully dynamic handling
of service discovery information, most clients and simple bots
only need to manage a few disco nodes that will remain mostly
static.
StaticDisco provides a set of node handlers that will store
static sets of disco info and items in memory.
Attributes:
nodes -- A dictionary mapping (JID, node) tuples to a dict
containing a disco#info and a disco#items stanza.
xmpp -- The main Slixmpp object.
"""
def __init__(self, xmpp, disco):
"""
Create a static disco interface. Sets of disco#info and
disco#items are maintained for every given JID and node
combination. These stanzas are used to store disco
information in memory without any additional processing.
Arguments:
xmpp -- The main Slixmpp object.
"""
self.nodes = {}
self.xmpp = xmpp
self.disco = disco
self.lock = threading.RLock()
def add_node(self, jid=None, node=None, ifrom=None):
"""
Create a new set of stanzas for the provided
JID and node combination.
Arguments:
jid -- The JID that will own the new stanzas.
node -- The node that will own the new stanzas.
"""
with self.lock:
if jid is None:
jid = self.xmpp.boundjid.full
if node is None:
node = ''
if ifrom is None:
ifrom = ''
if isinstance(ifrom, JID):
ifrom = ifrom.full
if (jid, node, ifrom) not in self.nodes:
self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(),
'items': DiscoItems()}
self.nodes[(jid, node, ifrom)]['info']['node'] = node
self.nodes[(jid, node, ifrom)]['items']['node'] = node
def get_node(self, jid=None, node=None, ifrom=None):
with self.lock:
if jid is None:
jid = self.xmpp.boundjid.full
if node is None:
node = ''
if ifrom is None:
ifrom = ''
if isinstance(ifrom, JID):
ifrom = ifrom.full
if (jid, node, ifrom) not in self.nodes:
self.add_node(jid, node, ifrom)
return self.nodes[(jid, node, ifrom)]
def node_exists(self, jid=None, node=None, ifrom=None):
with self.lock:
if jid is None:
jid = self.xmpp.boundjid.full
if node is None:
node = ''
if ifrom is None:
ifrom = ''
if isinstance(ifrom, JID):
ifrom = ifrom.full
if (jid, node, ifrom) not in self.nodes:
return False
return True
# =================================================================
# Node Handlers
#
# Each handler accepts four arguments: jid, node, ifrom, and data.
# The jid and node parameters together determine the set of info
# and items stanzas that will be retrieved or added. Additionally,
# the ifrom value allows for cached results when results vary based
# on the requester's JID. The data parameter is a dictionary with
# additional parameters that will be passed to other calls.
#
# This implementation does not allow different responses based on
# the requester's JID, except for cached results. To do that,
# register a custom node handler.
def supports(self, jid, node, ifrom, data):
"""
Check if a JID supports a given feature.
The data parameter may provide:
feature -- The feature to check for support.
local -- If true, then the query is for a JID/node
combination handled by this Slixmpp instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
"""
feature = data.get('feature', None)
data = {'local': data.get('local', False),
'cached': data.get('cached', True)}
if not feature:
return False
try:
info = self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True)
features = info['disco_info']['features']
return feature in features
except IqError:
return False
except IqTimeout:
return None
def has_identity(self, jid, node, ifrom, data):
"""
Check if a JID has a given identity.
The data parameter may provide:
category -- The category of the identity to check.
itype -- The type of the identity to check.
lang -- The language of the identity to check.
local -- If true, then the query is for a JID/node
combination handled by this Slixmpp instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info.
cached -- If true, then look for the disco info data from
the local cache system. If no results are found,
send the query as usual. The self.use_cache
setting must be set to true for this option to
be useful. If set to false, then the cache will
be skipped, even if a result has already been
cached. Defaults to false.
"""
identity = (data.get('category', None),
data.get('itype', None),
data.get('lang', None))
data = {'local': data.get('local', False),
'cached': data.get('cached', True)}
try:
info = self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True)
trunc = lambda i: (i[0], i[1], i[2])
return identity in map(trunc, info['disco_info']['identities'])
except IqError:
return False
except IqTimeout:
return None
def get_info(self, jid, node, ifrom, data):
"""
Return the stored info data for the requested JID/node combination.
The data parameter is not used.
"""
with self.lock:
if not self.node_exists(jid, node):
if not node:
return DiscoInfo()
else:
raise XMPPError(condition='item-not-found')
else:
return self.get_node(jid, node)['info']
def set_info(self, jid, node, ifrom, data):
"""
Set the entire info stanza for a JID/node at once.
The data parameter is a disco#info substanza.
"""
with self.lock:
self.add_node(jid, node)
self.get_node(jid, node)['info'] = data
def del_info(self, jid, node, ifrom, data):
"""
Reset the info stanza for a given JID/node combination.
The data parameter is not used.
"""
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['info'] = DiscoInfo()
def get_items(self, jid, node, ifrom, data):
"""
Return the stored items data for the requested JID/node combination.
The data parameter is not used.
"""
with self.lock:
if not self.node_exists(jid, node):
if not node:
return DiscoItems()
else:
raise XMPPError(condition='item-not-found')
else:
return self.get_node(jid, node)['items']
def set_items(self, jid, node, ifrom, data):
"""
Replace the stored items data for a JID/node combination.
The data parameter may provide:
items -- A set of items in tuple format.
"""
with self.lock:
items = data.get('items', set())
self.add_node(jid, node)
self.get_node(jid, node)['items']['items'] = items
def del_items(self, jid, node, ifrom, data):
"""
Reset the items stanza for a given JID/node combination.
The data parameter is not used.
"""
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['items'] = DiscoItems()
def add_identity(self, jid, node, ifrom, data):
"""
Add a new identity to te JID/node combination.
The data parameter may provide:
category -- The general category to which the agent belongs.
itype -- A more specific designation with the category.
name -- Optional human readable name for this identity.
lang -- Optional standard xml:lang value.
"""
with self.lock:
self.add_node(jid, node)
self.get_node(jid, node)['info'].add_identity(
data.get('category', ''),
data.get('itype', ''),
data.get('name', None),
data.get('lang', None))
def set_identities(self, jid, node, ifrom, data):
"""
Add or replace all identities for a JID/node combination.
The data parameter should include:
identities -- A list of identities in tuple form:
(category, type, name, lang)
"""
with self.lock:
identities = data.get('identities', set())
self.add_node(jid, node)
self.get_node(jid, node)['info']['identities'] = identities
def del_identity(self, jid, node, ifrom, data):
"""
Remove an identity from a JID/node combination.
The data parameter may provide:
category -- The general category to which the agent belonged.
itype -- A more specific designation with the category.
name -- Optional human readable name for this identity.
lang -- Optional, standard xml:lang value.
"""
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['info'].del_identity(
data.get('category', ''),
data.get('itype', ''),
data.get('name', None),
data.get('lang', None))
def del_identities(self, jid, node, ifrom, data):
"""
Remove all identities from a JID/node combination.
The data parameter is not used.
"""
with self.lock:
if self.node_exists(jid, node):
del self.get_node(jid, node)['info']['identities']
def add_feature(self, jid, node, ifrom, data):
"""
Add a feature to a JID/node combination.
The data parameter should include:
feature -- The namespace of the supported feature.
"""
with self.lock:
self.add_node(jid, node)
self.get_node(jid, node)['info'].add_feature(
data.get('feature', ''))
def set_features(self, jid, node, ifrom, data):
"""
Add or replace all features for a JID/node combination.
The data parameter should include:
features -- The new set of supported features.
"""
with self.lock:
features = data.get('features', set())
self.add_node(jid, node)
self.get_node(jid, node)['info']['features'] = features
def del_feature(self, jid, node, ifrom, data):
"""
Remove a feature from a JID/node combination.
The data parameter should include:
feature -- The namespace of the removed feature.
"""
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['info'].del_feature(
data.get('feature', ''))
def del_features(self, jid, node, ifrom, data):
"""
Remove all features from a JID/node combination.
The data parameter is not used.
"""
with self.lock:
if not self.node_exists(jid, node):
return
del self.get_node(jid, node)['info']['features']
def add_item(self, jid, node, ifrom, data):
"""
Add an item to a JID/node combination.
The data parameter may include:
ijid -- The JID for the item.
inode -- Optional additional information to reference
non-addressable items.
name -- Optional human readable name for the item.
"""
with self.lock:
self.add_node(jid, node)
self.get_node(jid, node)['items'].add_item(
data.get('ijid', ''),
node=data.get('inode', ''),
name=data.get('name', ''))
def del_item(self, jid, node, ifrom, data):
"""
Remove an item from a JID/node combination.
The data parameter may include:
ijid -- JID of the item to remove.
inode -- Optional extra identifying information.
"""
with self.lock:
if self.node_exists(jid, node):
self.get_node(jid, node)['items'].del_item(
data.get('ijid', ''),
node=data.get('inode', None))
def cache_info(self, jid, node, ifrom, data):
"""
Cache disco information for an external JID.
The data parameter is the Iq result stanza
containing the disco info to cache, or
the disco#info substanza itself.
"""
with self.lock:
if isinstance(data, Iq):
data = data['disco_info']
self.add_node(jid, node, ifrom)
self.get_node(jid, node, ifrom)['info'] = data
def get_cached_info(self, jid, node, ifrom, data):
"""
Retrieve cached disco info data.
The data parameter is not used.
"""
with self.lock:
if not self.node_exists(jid, node, ifrom):
return None
else:
return self.get_node(jid, node, ifrom)['info']
+20
View File
@@ -0,0 +1,20 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0033 import stanza
from slixmpp.plugins.xep_0033.stanza import Addresses, Address
from slixmpp.plugins.xep_0033.addresses import XEP_0033
register_plugin(XEP_0033)
# Retain some backwards compatibility
xep_0033 = XEP_0033
Addresses.addAddress = Addresses.add_address
+37
View File
@@ -0,0 +1,37 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp import Message, Presence
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0033 import stanza, Addresses
class XEP_0033(BasePlugin):
"""
XEP-0033: Extended Stanza Addressing
"""
name = 'xep_0033'
description = 'XEP-0033: Extended Stanza Addressing'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
register_stanza_plugin(Message, Addresses)
register_stanza_plugin(Presence, Addresses)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Addresses.namespace)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Addresses.namespace)
+131
View File
@@ -0,0 +1,131 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import JID, ElementBase, ET, register_stanza_plugin
class Addresses(ElementBase):
name = 'addresses'
namespace = 'http://jabber.org/protocol/address'
plugin_attrib = 'addresses'
interfaces = set()
def add_address(self, atype='to', jid='', node='', uri='',
desc='', delivered=False):
addr = Address(parent=self)
addr['type'] = atype
addr['jid'] = jid
addr['node'] = node
addr['uri'] = uri
addr['desc'] = desc
addr['delivered'] = delivered
return addr
# Additional methods for manipulating sets of addresses
# based on type are generated below.
class Address(ElementBase):
name = 'address'
namespace = 'http://jabber.org/protocol/address'
plugin_attrib = 'address'
interfaces = set(['type', 'jid', 'node', 'uri', 'desc', 'delivered'])
address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'))
def get_jid(self):
return JID(self._get_attr('jid'))
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_delivered(self):
value = self._get_attr('delivered', False)
return value and value.lower() in ('true', '1')
def set_delivered(self, delivered):
if delivered:
self._set_attr('delivered', 'true')
else:
del self['delivered']
def set_uri(self, uri):
if uri:
del self['jid']
del self['node']
self._set_attr('uri', uri)
else:
self._del_attr('uri')
# =====================================================================
# Auto-generate address type filters for the Addresses class.
def _addr_filter(atype):
def _type_filter(addr):
if isinstance(addr, Address):
if atype == 'all' or addr['type'] == atype:
return True
return False
return _type_filter
def _build_methods(atype):
def get_multi(self):
return list(filter(_addr_filter(atype), self))
def set_multi(self, value):
del self[atype]
for addr in value:
# Support assigning dictionary versions of addresses
# instead of full Address objects.
if not isinstance(addr, Address):
if atype != 'all':
addr['type'] = atype
elif 'atype' in addr and 'type' not in addr:
addr['type'] = addr['atype']
addrObj = Address()
addrObj.values = addr
addr = addrObj
self.append(addr)
def del_multi(self):
res = list(filter(_addr_filter(atype), self))
for addr in res:
self.iterables.remove(addr)
self.xml.remove(addr.xml)
return get_multi, set_multi, del_multi
for atype in ('all', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'):
get_multi, set_multi, del_multi = _build_methods(atype)
Addresses.interfaces.add(atype)
setattr(Addresses, "get_%s" % atype, get_multi)
setattr(Addresses, "set_%s" % atype, set_multi)
setattr(Addresses, "del_%s" % atype, del_multi)
# To retain backwards compatibility:
setattr(Addresses, "get%s" % atype.title(), get_multi)
setattr(Addresses, "set%s" % atype.title(), set_multi)
setattr(Addresses, "del%s" % atype.title(), del_multi)
if atype == 'all':
Addresses.interfaces.add('addresses')
setattr(Addresses, "getAddresses", get_multi)
setattr(Addresses, "setAddresses", set_multi)
setattr(Addresses, "delAddresses", del_multi)
register_stanza_plugin(Addresses, Address, iterable=True)
+402
View File
@@ -0,0 +1,402 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from __future__ import with_statement
import logging
from slixmpp import Presence
from slixmpp.plugins import BasePlugin, register_plugin
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET
from slixmpp.xmlstream.handler.callback import Callback
from slixmpp.xmlstream.matcher.xpath import MatchXPath
from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask
from slixmpp.exceptions import IqError, IqTimeout
log = logging.getLogger(__name__)
class MUCPresence(ElementBase):
name = 'x'
namespace = 'http://jabber.org/protocol/muc#user'
plugin_attrib = 'muc'
interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
affiliations = set(('', ))
roles = set(('', ))
def getXMLItem(self):
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
if item is None:
item = ET.Element('{http://jabber.org/protocol/muc#user}item')
self.xml.append(item)
return item
def getAffiliation(self):
#TODO if no affilation, set it to the default and return default
item = self.getXMLItem()
return item.get('affiliation', '')
def setAffiliation(self, value):
item = self.getXMLItem()
#TODO check for valid affiliation
item.attrib['affiliation'] = value
return self
def delAffiliation(self):
item = self.getXMLItem()
#TODO set default affiliation
if 'affiliation' in item.attrib: del item.attrib['affiliation']
return self
def getJid(self):
item = self.getXMLItem()
return JID(item.get('jid', ''))
def setJid(self, value):
item = self.getXMLItem()
if not isinstance(value, str):
value = str(value)
item.attrib['jid'] = value
return self
def delJid(self):
item = self.getXMLItem()
if 'jid' in item.attrib: del item.attrib['jid']
return self
def getRole(self):
item = self.getXMLItem()
#TODO get default role, set default role if none
return item.get('role', '')
def setRole(self, value):
item = self.getXMLItem()
#TODO check for valid role
item.attrib['role'] = value
return self
def delRole(self):
item = self.getXMLItem()
#TODO set default role
if 'role' in item.attrib: del item.attrib['role']
return self
def getNick(self):
return self.parent()['from'].resource
def getRoom(self):
return self.parent()['from'].bare
def setNick(self, value):
log.warning("Cannot set nick through mucpresence plugin.")
return self
def setRoom(self, value):
log.warning("Cannot set room through mucpresence plugin.")
return self
def delNick(self):
log.warning("Cannot delete nick through mucpresence plugin.")
return self
def delRoom(self):
log.warning("Cannot delete room through mucpresence plugin.")
return self
class XEP_0045(BasePlugin):
"""
Implements XEP-0045 Multi-User Chat
"""
name = 'xep_0045'
description = 'XEP-0045: Multi-User Chat'
dependencies = set(['xep_0030', 'xep_0004'])
def plugin_init(self):
self.rooms = {}
self.ourNicks = {}
self.xep = '0045'
# load MUC support in presence stanzas
register_stanza_plugin(Presence, MUCPresence)
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))
def handle_groupchat_invite(self, inv):
""" Handle an invite into a muc.
"""
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)
def handle_config_change(self, msg):
"""Handle a MUC configuration change (with status code)."""
self.xmpp.event('groupchat_config_status', msg)
self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
def handle_groupchat_presence(self, pr):
""" Handle a presence in a muc.
"""
got_offline = False
got_online = False
if pr['muc']['room'] not in self.rooms.keys():
return
entry = pr['muc'].getStanzaValues()
entry['show'] = pr['show']
entry['status'] = pr['status']
entry['alt_nick'] = pr['nick']
if pr['type'] == 'unavailable':
if entry['nick'] in self.rooms[entry['room']]:
del self.rooms[entry['room']][entry['nick']]
got_offline = True
else:
if entry['nick'] not in self.rooms[entry['room']]:
got_online = True
self.rooms[entry['room']][entry['nick']] = entry
log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
self.xmpp.event("groupchat_presence", pr)
self.xmpp.event("muc::%s::presence" % entry['room'], pr)
if got_offline:
self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
if got_online:
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
def handle_groupchat_message(self, msg):
""" Handle a message event in a muc.
"""
self.xmpp.event('groupchat_message', msg)
self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
def 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)
"""
self.xmpp.event('groupchat_subject', msg)
def jidInRoom(self, room, jid):
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid:
return True
return False
def getNick(self, room, jid):
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid:
return nick
def configureRoom(self, room, form=None, ifrom=None):
if form is None:
form = self.getRoomConfig(room, ifrom=ifrom)
iq = self.xmpp.makeIqSet()
iq['to'] = room
if ifrom is not None:
iq['from'] = ifrom
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
form = form.getXML('submit')
query.append(form)
iq.append(query)
# For now, swallow errors to preserve existing API
try:
result = iq.send()
except IqError:
return False
except IqTimeout:
return False
return True
def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
""" Join the specified room, requesting 'maxhistory' lines of history.
"""
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('{http://jabber.org/protocol/muc}password')
passelement.text = password
x.append(passelement)
if maxhistory:
history = ET.Element('{http://jabber.org/protocol/muc}history')
if maxhistory == "0":
history.attrib['maxchars'] = maxhistory
else:
history.attrib['maxstanzas'] = maxhistory
x.append(history)
stanza.append(x)
if not wait:
self.xmpp.send(stanza)
else:
#wait for our own room presence back
expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
self.xmpp.send(stanza, expect)
self.rooms[room] = {}
self.ourNicks[room] = nick
def destroy(self, room, reason='', altroom = '', ifrom=None):
iq = self.xmpp.makeIqSet()
if ifrom is not None:
iq['from'] = ifrom
iq['to'] = room
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
if altroom:
destroy.attrib['jid'] = altroom
xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
xreason.text = reason
destroy.append(xreason)
query.append(destroy)
iq.append(query)
# For now, swallow errors to preserve existing API
try:
r = iq.send()
except IqError:
return False
except IqTimeout:
return False
return True
def setAffiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
""" Change room affiliation."""
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
raise TypeError
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
if nick is not None:
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
else:
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
iq['from'] = ifrom
# For now, swallow errors to preserve existing API
try:
result = iq.send()
except IqError:
return False
except IqTimeout:
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)
msg['from'] = mfrom
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('{http://jabber.org/protocol/muc#user}reason')
rxml.text = reason
invite.append(rxml)
x.append(invite)
msg.append(x)
self.xmpp.send(msg)
def leaveMUC(self, room, nick, msg='', pfrom=None):
""" Leave the specified room.
"""
if msg:
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
else:
self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
del self.rooms[room]
def getRoomConfig(self, room, ifrom=''):
iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner')
iq['to'] = room
iq['from'] = ifrom
# For now, swallow errors to preserve existing API
try:
result = iq.send()
except IqError:
raise ValueError
except IqTimeout:
raise ValueError
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
if form is None:
raise ValueError
return self.xmpp.plugin['xep_0004'].buildForm(form)
def cancelConfig(self, room, ifrom=None):
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
x = ET.Element('{jabber:x:data}x', type='cancel')
query.append(x)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
iq['from'] = ifrom
iq.send()
def setRoomConfig(self, room, config, ifrom=''):
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
x = config.getXML('submit')
query.append(x)
iq = self.xmpp.makeIqSet(query)
iq['to'] = room
iq['from'] = ifrom
iq.send()
def getJoinedRooms(self):
return self.rooms.keys()
def getOurJidInRoom(self, roomJid):
""" Return the jid we're using in a room.
"""
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
def getJidProperty(self, room, nick, jidProperty):
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
If not found, return None.
"""
if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
return self.rooms[room][nick][jidProperty]
else:
return None
def getRoster(self, room):
""" Get the list of nicks in a room.
"""
if room not in self.rooms.keys():
return None
return self.rooms[room].keys()
xep_0045 = XEP_0045
register_plugin(XEP_0045)
+21
View File
@@ -0,0 +1,21 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0047 import stanza
from slixmpp.plugins.xep_0047.stanza import Open, Close, Data
from slixmpp.plugins.xep_0047.stream import IBBytestream
from slixmpp.plugins.xep_0047.ibb import XEP_0047
register_plugin(XEP_0047)
# Retain some backwards compatibility
xep_0047 = XEP_0047
+215
View File
@@ -0,0 +1,215 @@
import uuid
import logging
import threading
from slixmpp import Message, Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0047 import stanza, Open, Close, Data, IBBytestream
log = logging.getLogger(__name__)
class XEP_0047(BasePlugin):
name = 'xep_0047'
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 = {}
self._pending_lock = threading.Lock()
self._stream_lock = threading.Lock()
self._preauthed_sids_lock = threading.Lock()
self._preauthed_sids = {}
register_stanza_plugin(Iq, Open)
register_stanza_plugin(Iq, Close)
register_stanza_plugin(Iq, Data)
register_stanza_plugin(Message, Data)
self.xmpp.register_handler(Callback(
'IBB Open',
StanzaPath('iq@type=set/ibb_open'),
self._handle_open_request))
self.xmpp.register_handler(Callback(
'IBB Close',
StanzaPath('iq@type=set/ibb_close'),
self._handle_close))
self.xmpp.register_handler(Callback(
'IBB Data',
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):
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 _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'
iq['to'] = jid
iq['from'] = ifrom
iq['ibb_open']['block_size'] = block_size
iq['ibb_open']['sid'] = sid
iq['ibb_open']['stanza'] = 'iq'
stream = IBBytestream(self.xmpp, sid, block_size,
iq['from'], iq['to'], window,
use_messages)
with self._stream_lock:
self._pending_streams[iq['id']] = stream
self._pending_streams[iq['id']] = stream
if block:
resp = iq.send(timeout=timeout)
self._handle_opened_stream(resp)
return stream
else:
cb = None
if callback is not None:
def chained(resp):
self._handle_opened_stream(resp)
callback(resp)
cb = chained
else:
cb = self._handle_opened_stream
return iq.send(block=block, timeout=timeout, callback=cb)
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:
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']]
def _handle_open_request(self, iq):
sid = iq['ibb_open']['sid']
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(etype='modify', condition='not-acceptable')
if size > self.max_block_size:
raise XMPPError('resource-constraint')
stream = IBBytestream(self.xmpp, sid, size,
iq['to'], iq['from'],
self.window_size)
stream.stream_started.set()
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, 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.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')
+67
View File
@@ -0,0 +1,67 @@
import re
import base64
from slixmpp.util import bytes
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream import ElementBase
VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*')
def to_b64(data):
return bytes(base64.b64encode(bytes(data))).decode('utf-8')
def from_b64(data):
return bytes(base64.b64decode(bytes(data)))
class Open(ElementBase):
name = 'open'
namespace = 'http://jabber.org/protocol/ibb'
plugin_attrib = 'ibb_open'
interfaces = set(('block_size', 'sid', 'stanza'))
def get_block_size(self):
return int(self._get_attr('block-size'))
def set_block_size(self, value):
self._set_attr('block-size', str(value))
def del_block_size(self):
self._del_attr('block-size')
class Data(ElementBase):
name = 'data'
namespace = 'http://jabber.org/protocol/ibb'
plugin_attrib = 'ibb_data'
interfaces = set(('seq', 'sid', 'data'))
sub_interfaces = set(['data'])
def get_seq(self):
return int(self._get_attr('seq', '0'))
def set_seq(self, value):
self._set_attr('seq', str(value))
def get_data(self):
b64_data = self.xml.text.strip()
if VALID_B64.match(b64_data).group() == b64_data:
return from_b64(b64_data)
else:
raise XMPPError('not-acceptable')
def set_data(self, value):
self.xml.text = to_b64(value)
def del_data(self):
self.xml.text = ''
class Close(ElementBase):
name = 'close'
namespace = 'http://jabber.org/protocol/ibb'
plugin_attrib = 'ibb_close'
interfaces = set(['sid'])
+148
View File
@@ -0,0 +1,148 @@
import socket
import threading
import logging
from slixmpp.stanza import Iq
from slixmpp.util import Queue
from slixmpp.exceptions import XMPPError
log = logging.getLogger(__name__)
class IBBytestream(object):
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
if jid is None:
jid = xmpp.boundjid
self.self_jid = jid
self.peer_jid = peer
self.send_seq = -1
self.recv_seq = -1
self._send_seq_lock = threading.Lock()
self._recv_seq_lock = threading.Lock()
self.stream_started = threading.Event()
self.stream_in_closed = threading.Event()
self.stream_out_closed = threading.Event()
self.recv_queue = Queue()
self.send_window = threading.BoundedSemaphore(value=self.window_size)
self.window_ids = set()
self.window_empty = threading.Event()
self.window_empty.set()
def send(self, data):
if not self.stream_started.is_set() or \
self.stream_out_closed.is_set():
raise socket.error
data = data[0:self.block_size]
self.send_window.acquire()
with self._send_seq_lock:
self.send_seq = (self.send_seq + 1) % 65535
seq = self.send_seq
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):
sent_len = 0
while sent_len < len(data):
sent_len += self.send(data[sent_len:])
def _recv_ack(self, iq):
self.window_ids.remove(iq['id'])
if not self.window_ids:
self.window_empty.set()
self.send_window.release()
if iq['type'] == 'error':
self.close()
def _recv_data(self, stanza):
with self._recv_seq_lock:
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 = 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})
if isinstance(stanza, Iq):
stanza.reply()
stanza.send()
def recv(self, *args, **kwargs):
return self.read(block=True)
def read(self, block=True, timeout=None, **kwargs):
if not self.stream_started.is_set() or \
self.stream_in_closed.is_set():
raise socket.error
if timeout is not None:
block = True
try:
return self.recv_queue.get(block, timeout)
except:
return None
def close(self):
iq = self.xmpp.Iq()
iq['type'] = 'set'
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,
callback=lambda x: self.stream_in_closed.set())
self.xmpp.event('ibb_stream_end', self)
def _closed(self, iq):
self.stream_in_closed.set()
self.stream_out_closed.set()
iq.reply()
iq.send()
self.xmpp.event('ibb_stream_end', self)
def makefile(self, *args, **kwargs):
return self
def connect(*args, **kwargs):
return None
def shutdown(self, *args, **kwargs):
return None
+15
View File
@@ -0,0 +1,15 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0048.stanza import Bookmarks, Conference, URL
from slixmpp.plugins.xep_0048.bookmarks import XEP_0048
register_plugin(XEP_0048)
+76
View File
@@ -0,0 +1,76 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp import Iq
from slixmpp.plugins import BasePlugin
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.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)
+65
View File
@@ -0,0 +1,65 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.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)
+15
View File
@@ -0,0 +1,15 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0049.stanza import PrivateXML
from slixmpp.plugins.xep_0049.private_storage import XEP_0049
register_plugin(XEP_0049)
@@ -0,0 +1,53 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp import Iq
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.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)
+17
View File
@@ -0,0 +1,17 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ET, ElementBase
class PrivateXML(ElementBase):
name = 'query'
namespace = 'jabber:iq:private'
plugin_attrib = 'private'
interfaces = set()
+19
View File
@@ -0,0 +1,19 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0050.stanza import Command
from slixmpp.plugins.xep_0050.adhoc import XEP_0050
register_plugin(XEP_0050)
# Retain some backwards compatibility
xep_0050 = XEP_0050
+688
View File
@@ -0,0 +1,688 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
import time
from slixmpp import Iq
from slixmpp.exceptions import IqError
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin, JID
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0050 import stanza
from slixmpp.plugins.xep_0050 import Command
from slixmpp.plugins.xep_0004 import Form
log = logging.getLogger(__name__)
class XEP_0050(BasePlugin):
"""
XEP-0050: Ad-Hoc Commands
XMPP's Adhoc Commands provides a generic workflow mechanism for
interacting with applications. The result is similar to menu selections
and multi-step dialogs in normal desktop applications. Clients do not
need to know in advance what commands are provided by any particular
application or agent. While adhoc commands provide similar functionality
to Jabber-RPC, adhoc commands are used primarily for human interaction.
Also see <http://xmpp.org/extensions/xep-0050.html>
Configuration Values:
threaded -- Indicates if command events should be threaded.
Defaults to True.
Events:
command_execute -- Received a command with action="execute"
command_next -- Received a command with action="next"
command_complete -- Received a command with action="complete"
command_cancel -- Received a command with action="cancel"
Attributes:
threaded -- Indicates if command events should be threaded.
Defaults to True.
commands -- A dictionary mapping JID/node pairs to command
names and handlers.
sessions -- A dictionary or equivalent backend mapping
session IDs to dictionaries containing data
relevant to a command's session.
Methods:
plugin_init -- Overrides base_plugin.plugin_init
post_init -- Overrides base_plugin.post_init
new_session -- Return a new session ID.
prep_handlers -- Placeholder. May call with a list of handlers
to prepare them for use with the session storage
backend, if needed.
set_backend -- Replace the default session storage with some
external storage mechanism, such as a database.
The provided backend wrapper must be able to
act using the same syntax as a dictionary.
add_command -- Add a command for use by external entitites.
get_commands -- Retrieve a list of commands provided by a
remote agent.
send_command -- Send a command request to a remote agent.
start_command -- Command user API: initiate a command session
continue_command -- Command user API: proceed to the next step
cancel_command -- Command user API: cancel a command
complete_command -- Command user API: finish a command
terminate_command -- Command user API: delete a command's session
"""
name = 'xep_0050'
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.sessions = self.session_db
if self.sessions is None:
self.sessions = {}
self.commands = {}
self.xmpp.register_handler(
Callback("Ad-Hoc Execute",
StanzaPath('iq@type=set/command'),
self._handle_command))
register_stanza_plugin(Iq, Command)
register_stanza_plugin(Command, Form)
self.xmpp.add_event_handler('command_execute',
self._handle_command_start,
threaded=self.threaded)
self.xmpp.add_event_handler('command_next',
self._handle_command_next,
threaded=self.threaded)
self.xmpp.add_event_handler('command_cancel',
self._handle_command_cancel,
threaded=self.threaded)
self.xmpp.add_event_handler('command_complete',
self._handle_command_complete,
threaded=self.threaded)
def plugin_end(self):
self.xmpp.del_event_handler('command_execute',
self._handle_command_start)
self.xmpp.del_event_handler('command_next',
self._handle_command_next)
self.xmpp.del_event_handler('command_cancel',
self._handle_command_cancel)
self.xmpp.del_event_handler('command_complete',
self._handle_command_complete)
self.xmpp.remove_handler('Ad-Hoc Execute')
self.xmpp['xep_0030'].del_feature(feature=Command.namespace)
self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Command.namespace)
self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple())
def set_backend(self, db):
"""
Replace the default session storage dictionary with
a generic, external data storage mechanism.
The replacement backend must be able to interact through
the same syntax and interfaces as a normal dictionary.
Arguments:
db -- The new session storage mechanism.
"""
self.sessions = db
def prep_handlers(self, handlers, **kwargs):
"""
Prepare a list of functions for use by the backend service.
Intended to be replaced by the backend service as needed.
Arguments:
handlers -- A list of function pointers
**kwargs -- Any additional parameters required by the backend.
"""
pass
# =================================================================
# Server side (command provider) API
def add_command(self, jid=None, node=None, name='', handler=None):
"""
Make a new command available to external entities.
Access control may be implemented in the provided handler.
Command workflow is done across a sequence of command handlers. The
first handler is given the initial Iq stanza of the request in order
to support access control. Subsequent handlers are given only the
payload items of the command. All handlers will receive the command's
session data.
Arguments:
jid -- The JID that will expose the command.
node -- The node associated with the command.
name -- A human readable name for the command.
handler -- A function that will generate the response to the
initial command request, as well as enforcing any
access control policies.
"""
if jid is None:
jid = self.xmpp.boundjid
elif not isinstance(jid, JID):
jid = JID(jid)
item_jid = jid.full
self.xmpp['xep_0030'].add_identity(category='automation',
itype='command-list',
name='Ad-Hoc commands',
node=Command.namespace,
jid=jid)
self.xmpp['xep_0030'].add_item(jid=item_jid,
name=name,
node=Command.namespace,
subnode=node,
ijid=jid)
self.xmpp['xep_0030'].add_identity(category='automation',
itype='command-node',
name=name,
node=node,
jid=jid)
self.xmpp['xep_0030'].add_feature(Command.namespace, None, jid)
self.commands[(item_jid, node)] = (name, handler)
def new_session(self):
"""Return a new session ID."""
return str(time.time()) + '-' + self.xmpp.new_id()
def _handle_command(self, iq):
"""Raise command events based on the command action."""
self.xmpp.event('command_%s' % iq['command']['action'], iq)
def _handle_command_start(self, iq):
"""
Process an initial request to execute a command.
Arguments:
iq -- The command execution request.
"""
sessionid = self.new_session()
node = iq['command']['node']
key = (iq['to'].full, node)
name, handler = self.commands.get(key, ('Not found', None))
if not handler:
log.debug('Command not found: %s, %s', key, self.commands)
payload = []
for stanza in iq['command']['substanzas']:
payload.append(stanza)
if len(payload) == 1:
payload = payload[0]
interfaces = set([item.plugin_attrib for item in payload])
payload_classes = set([item.__class__ for item in payload])
initial_session = {'id': sessionid,
'from': iq['from'],
'to': iq['to'],
'node': node,
'payload': payload,
'interfaces': interfaces,
'payload_classes': payload_classes,
'notes': None,
'has_next': False,
'allow_complete': False,
'allow_prev': False,
'past': [],
'next': None,
'prev': None,
'cancel': None}
session = handler(iq, initial_session)
self._process_command_response(iq, session)
def _handle_command_next(self, iq):
"""
Process a request for the next 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['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)
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):
"""
Generate a command reply stanza based on the
provided session data.
Arguments:
iq -- The command request stanza.
session -- A dictionary of relevant session data.
"""
sessionid = session['id']
payload = session['payload']
if payload is None:
payload = []
if not isinstance(payload, list):
payload = [payload]
interfaces = session.get('interfaces', set())
payload_classes = session.get('payload_classes', set())
interfaces.update(set([item.plugin_attrib for item in payload]))
payload_classes.update(set([item.__class__ for item in payload]))
session['interfaces'] = interfaces
session['payload_classes'] = payload_classes
self.sessions[sessionid] = session
for item in payload:
register_stanza_plugin(Command, item.__class__, iterable=True)
iq.reply()
iq['command']['node'] = session['node']
iq['command']['sessionid'] = session['id']
if session['next'] is None:
iq['command']['actions'] = []
iq['command']['status'] = 'completed'
elif session['has_next']:
actions = ['next']
if session['allow_complete']:
actions.append('complete')
if session['allow_prev']:
actions.append('prev')
iq['command']['actions'] = actions
iq['command']['status'] = 'executing'
else:
iq['command']['actions'] = ['complete']
iq['command']['status'] = 'executing'
iq['command']['notes'] = session['notes']
for item in payload:
iq['command'].append(item)
iq.send()
def _handle_command_cancel(self, iq):
"""
Process a request to cancel a command's execution.
Arguments:
iq -- The command cancellation request.
"""
node = iq['command']['node']
sessionid = iq['command']['sessionid']
session = self.sessions.get(sessionid)
if session:
handler = session['cancel']
if handler:
handler(iq, session)
del self.sessions[sessionid]
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')
def _handle_command_complete(self, iq):
"""
Process a request to finish the execution of command
and terminate the workflow.
All data related to the command session will be removed.
Arguments:
iq -- The command completion request.
"""
node = iq['command']['node']
sessionid = iq['command']['sessionid']
session = self.sessions.get(sessionid)
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]
if handler:
handler(results, session)
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
def get_commands(self, jid, **kwargs):
"""
Return a list of commands provided by a given JID.
Arguments:
jid -- The JID to query for commands.
local -- If true, then the query is for a JID/node
combination handled by this Slixmpp instance and
no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the
remove JID to retrieve the items.
ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for
a reply. If None, then wait indefinitely.
callback -- Optional callback to execute when a reply is
received instead of blocking and waiting for
the reply.
iterator -- If True, return a result set iterator using
the XEP-0059 plugin, if the plugin is loaded.
Otherwise the parameter is ignored.
"""
return self.xmpp['xep_0030'].get_items(jid=jid,
node=Command.namespace,
**kwargs)
def send_command(self, jid, node, ifrom=None, action='execute',
payload=None, sessionid=None, flow=False, **kwargs):
"""
Create and send a command stanza, without using the provided
workflow management APIs.
Arguments:
jid -- The JID to send the command request or result.
node -- The node for the command.
ifrom -- Specify the sender's JID.
action -- May be one of: execute, cancel, complete,
or cancel.
payload -- Either a list of payload items, or a single
payload item such as a data form.
sessionid -- The current session's ID value.
flow -- If True, process the Iq result using the
command workflow methods contained in the
session instead of returning the response
stanza itself. Defaults to False.
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
slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler
function. Will be executed when a reply
stanza is received if flow=False.
"""
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = jid
iq['from'] = ifrom
iq['command']['node'] = node
iq['command']['action'] = action
if sessionid is not None:
iq['command']['sessionid'] = sessionid
if payload is not None:
if not isinstance(payload, list):
payload = [payload]
for item in payload:
iq['command'].append(item)
if not flow:
return iq.send(**kwargs)
else:
if kwargs.get('block', True):
try:
result = iq.send(**kwargs)
except IqError as err:
result = err.iq
self._handle_command_result(result)
else:
iq.send(block=False, callback=self._handle_command_result)
def start_command(self, jid, node, session, ifrom=None, block=False):
"""
Initiate executing a command provided by a remote agent.
The default workflow provided is non-blocking, but a blocking
version may be used with block=True.
The provided session dictionary should contain:
next -- A handler for processing the command result.
error -- A handler for processing any error stanzas
generated by the request.
Arguments:
jid -- The JID to send the command request.
node -- The node for the desired command.
session -- A dictionary of relevant session data.
ifrom -- Optionally specify the sender's JID.
block -- If True, block execution until a result
is received. Defaults to False.
"""
session['jid'] = jid
session['node'] = node
session['timestamp'] = time.time()
session['block'] = block
if 'payload' not in session:
session['payload'] = None
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = jid
iq['from'] = ifrom
session['from'] = ifrom
iq['command']['node'] = node
iq['command']['action'] = 'execute'
if session['payload'] is not None:
payload = session['payload']
if not isinstance(payload, list):
payload = list(payload)
for stanza in payload:
iq['command'].append(stanza)
sessionid = 'client:pending_' + iq['id']
session['id'] = sessionid
self.sessions[sessionid] = session
if session['block']:
try:
result = iq.send(block=True)
except IqError as err:
result = err.iq
self._handle_command_result(result)
else:
iq.send(block=False, callback=self._handle_command_result)
def continue_command(self, session, direction='next'):
"""
Execute the next action of the command.
Arguments:
session -- All stored data relevant to the current
command session.
"""
sessionid = 'client:' + session['id']
self.sessions[sessionid] = session
self.send_command(session['jid'],
session['node'],
ifrom=session.get('from', None),
action=direction,
payload=session.get('payload', None),
sessionid=session['id'],
flow=True,
block=session['block'])
def cancel_command(self, session):
"""
Cancel the execution of a command.
Arguments:
session -- All stored data relevant to the current
command session.
"""
sessionid = 'client:' + session['id']
self.sessions[sessionid] = session
self.send_command(session['jid'],
session['node'],
ifrom=session.get('from', None),
action='cancel',
payload=session.get('payload', None),
sessionid=session['id'],
flow=True,
block=session['block'])
def complete_command(self, session):
"""
Finish the execution of a command workflow.
Arguments:
session -- All stored data relevant to the current
command session.
"""
sessionid = 'client:' + session['id']
self.sessions[sessionid] = session
self.send_command(session['jid'],
session['node'],
ifrom=session.get('from', None),
action='complete',
payload=session.get('payload', None),
sessionid=session['id'],
flow=True,
block=session['block'])
def terminate_command(self, session):
"""
Delete a command's session after a command has completed
or an error has occured.
Arguments:
session -- All stored data relevant to the current
command session.
"""
sessionid = 'client:' + session['id']
try:
del self.sessions[sessionid]
except Exception as e:
log.error("Error deleting adhoc command session: %s" % e.message)
def _handle_command_result(self, iq):
"""
Process the results of a command request.
Will execute the 'next' handler stored in the session
data, or the 'error' handler depending on the Iq's type.
Arguments:
iq -- The command response.
"""
sessionid = 'client:' + iq['command']['sessionid']
pending = False
if sessionid not in self.sessions:
pending = True
pendingid = 'client:pending_' + iq['id']
if pendingid not in self.sessions:
return
sessionid = pendingid
session = self.sessions[sessionid]
sessionid = 'client:' + iq['command']['sessionid']
session['id'] = iq['command']['sessionid']
self.sessions[sessionid] = session
if pending:
del self.sessions[pendingid]
handler_type = 'next'
if iq['type'] == 'error':
handler_type = 'error'
handler = session.get(handler_type, None)
if handler:
handler(iq, session)
elif iq['type'] == 'error':
self.terminate_command(session)
if iq['command']['status'] == 'completed':
self.terminate_command(session)
+185
View File
@@ -0,0 +1,185 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, ET
class Command(ElementBase):
"""
XMPP's Adhoc Commands provides a generic workflow mechanism for
interacting with applications. The result is similar to menu selections
and multi-step dialogs in normal desktop applications. Clients do not
need to know in advance what commands are provided by any particular
application or agent. While adhoc commands provide similar functionality
to Jabber-RPC, adhoc commands are used primarily for human interaction.
Also see <http://xmpp.org/extensions/xep-0050.html>
Example command stanzas:
<iq type="set">
<command xmlns="http://jabber.org/protocol/commands"
node="run_foo"
action="execute" />
</iq>
<iq type="result">
<command xmlns="http://jabber.org/protocol/commands"
node="run_foo"
sessionid="12345"
status="executing">
<actions>
<complete />
</actions>
<note type="info">Information!</note>
<x xmlns="jabber:x:data">
<field var="greeting"
type="text-single"
label="Greeting" />
</x>
</command>
</iq>
Stanza Interface:
action -- The action to perform.
actions -- The set of allowable next actions.
node -- The node associated with the command.
notes -- A list of tuples for informative notes.
sessionid -- A unique identifier for a command session.
status -- May be one of: canceled, completed, or executing.
Attributes:
actions -- A set of allowed action values.
statuses -- A set of allowed status values.
next_actions -- A set of allowed next action names.
Methods:
get_action -- Return the requested action.
get_actions -- Return the allowable next actions.
set_actions -- Set the allowable next actions.
del_actions -- Remove the current set of next actions.
get_notes -- Return a list of informative note data.
set_notes -- Set informative notes.
del_notes -- Remove any note data.
add_note -- Add a single note.
"""
name = 'command'
namespace = 'http://jabber.org/protocol/commands'
plugin_attrib = 'command'
interfaces = set(('action', 'sessionid', 'node',
'status', 'actions', 'notes'))
actions = set(('cancel', 'complete', 'execute', 'next', 'prev'))
statuses = set(('canceled', 'completed', 'executing'))
next_actions = set(('prev', 'next', 'complete'))
def get_action(self):
"""
Return the value of the action attribute.
If the Iq stanza's type is "set" then use a default
value of "execute".
"""
if self.parent()['type'] == 'set':
return self._get_attr('action', default='execute')
return self._get_attr('action')
def set_actions(self, values):
"""
Assign the set of allowable next actions.
Arguments:
values -- A list containing any combination of:
'prev', 'next', and 'complete'
"""
self.del_actions()
if values:
self._set_sub_text('{%s}actions' % self.namespace, '', True)
actions = self.find('{%s}actions' % self.namespace)
for val in values:
if val in self.next_actions:
action = ET.Element('{%s}%s' % (self.namespace, val))
actions.append(action)
def get_actions(self):
"""
Return the set of allowable next actions.
"""
actions = set()
actions_xml = self.find('{%s}actions' % self.namespace)
if actions_xml is not None:
for action in self.next_actions:
action_xml = actions_xml.find('{%s}%s' % (self.namespace,
action))
if action_xml is not None:
actions.add(action)
return actions
def del_actions(self):
"""
Remove all allowable next actions.
"""
self._del_sub('{%s}actions' % self.namespace)
def get_notes(self):
"""
Return a list of note information.
Example:
[('info', 'Some informative data'),
('warning', 'Use caution'),
('error', 'The command ran, but had errors')]
"""
notes = []
notes_xml = self.findall('{%s}note' % self.namespace)
for note in notes_xml:
notes.append((note.attrib.get('type', 'info'),
note.text))
return notes
def set_notes(self, notes):
"""
Add multiple notes to the command result.
Each note is a tuple, with the first item being one of:
'info', 'warning', or 'error', and the second item being
any human readable message.
Example:
[('info', 'Some informative data'),
('warning', 'Use caution'),
('error', 'The command ran, but had errors')]
Arguments:
notes -- A list of tuples of note information.
"""
self.del_notes()
for note in notes:
self.add_note(note[1], note[0])
def del_notes(self):
"""
Remove all notes associated with the command result.
"""
notes_xml = self.findall('{%s}note' % self.namespace)
for note in notes_xml:
self.xml.remove(note)
def add_note(self, msg='', ntype='info'):
"""
Add a single note annotation to the command.
Arguments:
msg -- A human readable message.
ntype -- One of: 'info', 'warning', 'error'
"""
xml = ET.Element('{%s}note' % self.namespace)
xml.attrib['type'] = ntype
xml.text = msg
self.xml.append(xml)
+15
View File
@@ -0,0 +1,15 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0054.stanza import VCardTemp
from slixmpp.plugins.xep_0054.vcard_temp import XEP_0054
register_plugin(XEP_0054)
+561
View File
@@ -0,0 +1,561 @@
import base64
import datetime as dt
from slixmpp.util import bytes
from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin, JID
from slixmpp.plugins import xep_0082
class VCardTemp(ElementBase):
name = 'vCard'
namespace = 'vcard-temp'
plugin_attrib = 'vcard_temp'
interfaces = set(['FN', 'VERSION'])
sub_interfaces = set(['FN', 'VERSION'])
class Name(ElementBase):
name = 'N'
namespace = 'vcard-temp'
plugin_attrib = name
interfaces = set(['FAMILY', 'GIVEN', 'MIDDLE', 'PREFIX', 'SUFFIX'])
sub_interfaces = interfaces
def _set_component(self, name, value):
if isinstance(value, list):
value = ','.join(value)
if value is not None:
self._set_sub_text(name, value, keep=True)
else:
self._del_sub(name)
def _get_component(self, name):
value = self._get_sub_text(name, '')
if ',' in value:
value = [v.strip() for v in value.split(',')]
return value
def set_family(self, value):
self._set_component('FAMILY', value)
def get_family(self):
return self._get_component('FAMILY')
def set_given(self, value):
self._set_component('GIVEN', value)
def get_given(self):
return self._get_component('GIVEN')
def set_middle(self, value):
print(value)
self._set_component('MIDDLE', value)
def get_middle(self):
return self._get_component('MIDDLE')
def set_prefix(self, value):
self._set_component('PREFIX', value)
def get_prefix(self):
return self._get_component('PREFIX')
def set_suffix(self, value):
self._set_component('SUFFIX', value)
def get_suffix(self):
return self._get_component('SUFFIX')
class Nickname(ElementBase):
name = 'NICKNAME'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'nicknames'
interfaces = set([name])
is_extension = True
def set_nickname(self, value):
if not value:
self.xml.text = ''
return
if not isinstance(value, list):
value = [value]
self.xml.text = ','.join(value)
def get_nickname(self):
if self.xml.text:
return self.xml.text.split(',')
class Email(ElementBase):
name = 'EMAIL'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'emails'
interfaces = set(['HOME', 'WORK', 'INTERNET', 'PREF', 'X400', 'USERID'])
sub_interfaces = set(['USERID'])
bool_interfaces = set(['HOME', 'WORK', 'INTERNET', 'PREF', 'X400'])
class Address(ElementBase):
name = 'ADR'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'addresses'
interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INTL',
'PREF', 'POBOX', 'EXTADD', 'STREET', 'LOCALITY',
'REGION', 'PCODE', 'CTRY'])
sub_interfaces = set(['POBOX', 'EXTADD', 'STREET', 'LOCALITY',
'REGION', 'PCODE', 'CTRY'])
bool_interfaces = set(['HOME', 'WORK', 'DOM', 'INTL', 'PREF'])
class Telephone(ElementBase):
name = 'TEL'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'telephone_numbers'
interfaces = set(['HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', 'MSG',
'CELL', 'VIDEO', 'BBS', 'MODEM', 'ISDN', 'PCS',
'PREF', 'NUMBER'])
sub_interfaces = set(['NUMBER'])
bool_interfaces = set(['HOME', 'WORK', 'VOICE', 'FAX', 'PAGER',
'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM',
'ISDN', 'PCS', 'PREF'])
def setup(self, xml=None):
super(Telephone, self).setup(xml=xml)
self._set_sub_text('NUMBER', '', keep=True)
def set_number(self, value):
self._set_sub_text('NUMBER', value, keep=True)
def del_number(self):
self._set_sub_text('NUMBER', '', keep=True)
class Label(ElementBase):
name = 'LABEL'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'labels'
interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INT',
'PREF', 'lines'])
bool_interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM',
'INT', 'PREF'])
def add_line(self, value):
line = ET.Element('{%s}LINE' % self.namespace)
line.text = value
self.xml.append(line)
def get_lines(self):
lines = self.xml.find('{%s}LINE' % self.namespace)
if lines is None:
return []
return [line.text for line in lines]
def set_lines(self, values):
self.del_lines()
for line in values:
self.add_line(line)
def del_lines(self):
lines = self.xml.find('{%s}LINE' % self.namespace)
if lines is None:
return
for line in lines:
self.xml.remove(line)
class Geo(ElementBase):
name = 'GEO'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'geolocations'
interfaces = set(['LAT', 'LON'])
sub_interfaces = interfaces
class Org(ElementBase):
name = 'ORG'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'organizations'
interfaces = set(['ORGNAME', 'ORGUNIT', 'orgunits'])
sub_interfaces = set(['ORGNAME', 'ORGUNIT'])
def add_orgunit(self, value):
orgunit = ET.Element('{%s}ORGUNIT' % self.namespace)
orgunit.text = value
self.xml.append(orgunit)
def get_orgunits(self):
orgunits = self.xml.find('{%s}ORGUNIT' % self.namespace)
if orgunits is None:
return []
return [orgunit.text for orgunit in orgunits]
def set_orgunits(self, values):
self.del_orgunits()
for orgunit in values:
self.add_orgunit(orgunit)
def del_orgunits(self):
orgunits = self.xml.find('{%s}ORGUNIT' % self.namespace)
if orgunits is None:
return
for orgunit in orgunits:
self.xml.remove(orgunit)
class Photo(ElementBase):
name = 'PHOTO'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'photos'
interfaces = set(['TYPE', 'EXTVAL'])
sub_interfaces = interfaces
class Logo(ElementBase):
name = 'LOGO'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'logos'
interfaces = set(['TYPE', 'EXTVAL'])
sub_interfaces = interfaces
class Sound(ElementBase):
name = 'SOUND'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'sounds'
interfaces = set(['PHONETC', 'EXTVAL'])
sub_interfaces = interfaces
class BinVal(ElementBase):
name = 'BINVAL'
namespace = 'vcard-temp'
plugin_attrib = name
interfaces = set(['BINVAL'])
is_extension = True
def setup(self, xml=None):
self.xml = ET.Element('')
return True
def set_binval(self, value):
self.del_binval()
parent = self.parent()
if value:
xml = ET.Element('{%s}BINVAL' % self.namespace)
xml.text = bytes(base64.b64encode(value)).decode('utf-8')
parent.append(xml)
def get_binval(self):
parent = self.parent()
xml = parent.find('{%s}BINVAL' % self.namespace)
if xml is not None:
return base64.b64decode(bytes(xml.text))
return b''
def del_binval(self):
self.parent()._del_sub('{%s}BINVAL' % self.namespace)
class Classification(ElementBase):
name = 'CLASS'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'classifications'
interfaces = set(['PUBLIC', 'PRIVATE', 'CONFIDENTIAL'])
bool_interfaces = interfaces
class Categories(ElementBase):
name = 'CATEGORIES'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'categories'
interfaces = set([name])
is_extension = True
def set_categories(self, values):
self.del_categories()
for keyword in values:
item = ET.Element('{%s}KEYWORD' % self.namespace)
item.text = keyword
self.xml.append(item)
def get_categories(self):
items = self.xml.findall('{%s}KEYWORD' % self.namespace)
if items is None:
return []
keywords = []
for item in items:
keywords.append(item.text)
return keywords
def del_categories(self):
items = self.xml.findall('{%s}KEYWORD' % self.namespace)
for item in items:
self.xml.remove(item)
class Birthday(ElementBase):
name = 'BDAY'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'birthdays'
interfaces = set([name])
is_extension = True
def set_bday(self, value):
if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value)
self.xml.text = value
def get_bday(self):
if not self.xml.text:
return None
return xep_0082.parse(self.xml.text)
class Rev(ElementBase):
name = 'REV'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'revision_dates'
interfaces = set([name])
is_extension = True
def set_rev(self, value):
if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value)
self.xml.text = value
def get_rev(self):
if not self.xml.text:
return None
return xep_0082.parse(self.xml.text)
class Title(ElementBase):
name = 'TITLE'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'titles'
interfaces = set([name])
is_extension = True
def set_title(self, value):
self.xml.text = value
def get_title(self):
return self.xml.text
class Role(ElementBase):
name = 'ROLE'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'roles'
interfaces = set([name])
is_extension = True
def set_role(self, value):
self.xml.text = value
def get_role(self):
return self.xml.text
class Note(ElementBase):
name = 'NOTE'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'notes'
interfaces = set([name])
is_extension = True
def set_note(self, value):
self.xml.text = value
def get_note(self):
return self.xml.text
class Desc(ElementBase):
name = 'DESC'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'descriptions'
interfaces = set([name])
is_extension = True
def set_desc(self, value):
self.xml.text = value
def get_desc(self):
return self.xml.text
class URL(ElementBase):
name = 'URL'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'urls'
interfaces = set([name])
is_extension = True
def set_url(self, value):
self.xml.text = value
def get_url(self):
return self.xml.text
class UID(ElementBase):
name = 'UID'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'uids'
interfaces = set([name])
is_extension = True
def set_uid(self, value):
self.xml.text = value
def get_uid(self):
return self.xml.text
class ProdID(ElementBase):
name = 'PRODID'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'product_ids'
interfaces = set([name])
is_extension = True
def set_prodid(self, value):
self.xml.text = value
def get_prodid(self):
return self.xml.text
class Mailer(ElementBase):
name = 'MAILER'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'mailers'
interfaces = set([name])
is_extension = True
def set_mailer(self, value):
self.xml.text = value
def get_mailer(self):
return self.xml.text
class SortString(ElementBase):
name = 'SORT-STRING'
namespace = 'vcard-temp'
plugin_attrib = 'SORT_STRING'
plugin_multi_attrib = 'sort_strings'
interfaces = set([name])
is_extension = True
def set_sort_string(self, value):
self.xml.text = value
def get_sort_string(self):
return self.xml.text
class Agent(ElementBase):
name = 'AGENT'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'agents'
interfaces = set(['EXTVAL'])
sub_interfaces = interfaces
class JabberID(ElementBase):
name = 'JABBERID'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'jids'
interfaces = set([name])
is_extension = True
def set_jabberid(self, value):
self.xml.text = JID(value).bare
def get_jabberid(self):
return JID(self.xml.text)
class TimeZone(ElementBase):
name = 'TZ'
namespace = 'vcard-temp'
plugin_attrib = name
plugin_multi_attrib = 'timezones'
interfaces = set([name])
is_extension = True
def set_tz(self, value):
time = xep_0082.time(offset=value)
if time[-1] == 'Z':
self.xml.text = 'Z'
else:
self.xml.text = time[-6:]
def get_tz(self):
if not self.xml.text:
return xep_0082.tzutc()
time = xep_0082.parse('00:00:00%s' % self.xml.text)
return time.tzinfo
register_stanza_plugin(VCardTemp, Name)
register_stanza_plugin(VCardTemp, Address, iterable=True)
register_stanza_plugin(VCardTemp, Agent, iterable=True)
register_stanza_plugin(VCardTemp, Birthday, iterable=True)
register_stanza_plugin(VCardTemp, Categories, iterable=True)
register_stanza_plugin(VCardTemp, Desc, iterable=True)
register_stanza_plugin(VCardTemp, Email, iterable=True)
register_stanza_plugin(VCardTemp, Geo, iterable=True)
register_stanza_plugin(VCardTemp, JabberID, iterable=True)
register_stanza_plugin(VCardTemp, Label, iterable=True)
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)
register_stanza_plugin(VCardTemp, Role, iterable=True)
register_stanza_plugin(VCardTemp, SortString, iterable=True)
register_stanza_plugin(VCardTemp, Sound, iterable=True)
register_stanza_plugin(VCardTemp, Telephone, iterable=True)
register_stanza_plugin(VCardTemp, Title, iterable=True)
register_stanza_plugin(VCardTemp, TimeZone, iterable=True)
register_stanza_plugin(VCardTemp, UID, iterable=True)
register_stanza_plugin(VCardTemp, URL, iterable=True)
register_stanza_plugin(Photo, BinVal)
register_stanza_plugin(Logo, BinVal)
register_stanza_plugin(Sound, BinVal)
register_stanza_plugin(Agent, VCardTemp)
+146
View File
@@ -0,0 +1,146 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp import JID, Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0054 import VCardTemp, stanza
log = logging.getLogger(__name__)
class XEP_0054(BasePlugin):
"""
XEP-0054: vcard-temp
"""
name = 'xep_0054'
description = 'XEP-0054: vcard-temp'
dependencies = set(['xep_0030', 'xep_0082'])
stanza = stanza
def plugin_init(self):
"""
Start the XEP-0054 plugin.
"""
register_stanza_plugin(Iq, VCardTemp)
self.api.register(self._set_vcard, 'set_vcard', default=True)
self.api.register(self._get_vcard, 'get_vcard', default=True)
self.api.register(self._del_vcard, 'del_vcard', default=True)
self._vcard_cache = {}
self.xmpp.register_handler(
Callback('VCardTemp',
StanzaPath('iq/vcard_temp'),
self._handle_get_vcard))
def plugin_end(self):
self.xmpp.remove_handler('VCardTemp')
self.xmpp['xep_0030'].del_feature(feature='vcard-temp')
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature('vcard-temp')
def make_vcard(self):
return VCardTemp()
def get_vcard(self, jid=None, ifrom=None, local=None, cached=False,
block=True, callback=None, timeout=None):
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)
if not isinstance(vcard, Iq):
iq = self.xmpp.Iq()
if vcard is None:
vcard = VCardTemp()
iq.append(vcard)
return iq
return vcard
if cached:
vcard = self.api['get_vcard'](jid, None, ifrom)
if vcard is not None:
if not isinstance(vcard, Iq):
iq = self.xmpp.Iq()
iq.append(vcard)
return iq
return vcard
iq = self.xmpp.Iq()
iq['to'] = jid
iq['from'] = ifrom
iq['type'] = 'get'
iq.enable('vcard_temp')
vcard = iq.send(block=block, callback=callback, timeout=timeout)
if block:
self.api['set_vcard'](vcard['from'], args=vcard['vcard_temp'])
return vcard
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:
return
iq = self.xmpp.Iq()
iq['to'] = jid
iq['from'] = ifrom
iq['type'] = 'set'
iq.append(vcard)
return iq.send(block=block, callback=callback, timeout=timeout)
def _handle_get_vcard(self, iq):
if iq['type'] == 'result':
self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp'])
return
elif iq['type'] == 'get':
vcard = self.api['get_vcard'](iq['from'].bare)
if isinstance(vcard, Iq):
vcard.send()
else:
iq.reply()
iq.append(vcard)
iq.send()
elif iq['type'] == 'set':
raise XMPPError('service-unavailable')
# =================================================================
def _set_vcard(self, jid, node, ifrom, vcard):
self._vcard_cache[jid.bare] = vcard
def _get_vcard(self, jid, node, ifrom, vcard):
return self._vcard_cache.get(jid.bare, None)
def _del_vcard(self, jid, node, ifrom, vcard):
if jid.bare in self._vcard_cache:
del self._vcard_cache[jid.bare]
+18
View File
@@ -0,0 +1,18 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0059.stanza import Set
from slixmpp.plugins.xep_0059.rsm import ResultIterator, XEP_0059
register_plugin(XEP_0059)
# Retain some backwards compatibility
xep_0059 = XEP_0059
+145
View File
@@ -0,0 +1,145 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
import slixmpp
from slixmpp import Iq
from slixmpp.plugins import BasePlugin, register_plugin
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.xep_0059 import stanza, Set
from slixmpp.exceptions import XMPPError
log = logging.getLogger(__name__)
class ResultIterator():
"""
An iterator for Result Set Managment
"""
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
Example:
q = Iq()
q['to'] = 'pubsub.example.com'
q['disco_items']['node'] = 'blog'
for i in ResultIterator(q, 'disco_items', '10'):
print i['disco_items']['items']
"""
self.query = query
self.amount = amount
self.start = start
self.interface = interface
self.results = results
self.reverse = reverse
self._stop = False
def __iter__(self):
return self
def __next__(self):
return self.next()
def next(self):
"""
Return the next page of results from a query.
Note: If using backwards paging, then the next page of
results will be the items before the current page
of items.
"""
if self._stop:
raise StopIteration
self.query[self.interface]['rsm']['before'] = self.reverse
self.query['id'] = self.query.stream.new_id()
self.query[self.interface]['rsm']['max'] = str(self.amount)
if self.start and self.reverse:
self.query[self.interface]['rsm']['before'] = self.start
elif self.start:
self.query[self.interface]['rsm']['after'] = self.start
try:
r = self.query.send(block=True)
if not r[self.interface]['rsm']['first'] and \
not r[self.interface]['rsm']['last']:
raise StopIteration
if r[self.interface]['rsm']['count'] and \
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][self.results])
if first + num_items == count:
self._stop = True
if self.reverse:
self.start = r[self.interface]['rsm']['first']
else:
self.start = r[self.interface]['rsm']['last']
return r
except XMPPError:
raise StopIteration
class XEP_0059(BasePlugin):
"""
XEP-0050: Result Set Management
"""
name = 'xep_0059'
description = 'XEP-0059: Result Set Management'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
"""
Start the XEP-0059 plugin.
"""
register_stanza_plugin(self.xmpp['xep_0030'].stanza.DiscoItems,
self.stanza.Set)
def plugin_end(self):
self.xmpp['xep_0030'].del_feature(feature=Set.namespace)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(Set.namespace)
def iterate(self, stanza, interface, results='substanzas'):
"""
Create a new result set iterator for a given stanza query.
Arguments:
stanza -- A stanza object to serve as a template for
queries made each iteration. For example, a
basic disco#items query.
interface -- The name of the substanza to which the
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, results)
+108
View File
@@ -0,0 +1,108 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, ET
from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
class Set(ElementBase):
"""
XEP-0059 (Result Set Managment) can be used to manage the
results of queries. For example, limiting the number of items
per response or starting at certain positions.
Example set stanzas:
<iq type="get">
<query xmlns="http://jabber.org/protocol/disco#items">
<set xmlns="http://jabber.org/protocol/rsm">
<max>2</max>
</set>
</query>
</iq>
<iq type="result">
<query xmlns="http://jabber.org/protocol/disco#items">
<item jid="conference.example.com" />
<item jid="pubsub.example.com" />
<set xmlns="http://jabber.org/protocol/rsm">
<first>conference.example.com</first>
<last>pubsub.example.com</last>
</set>
</query>
</iq>
Stanza Interface:
first_index -- The index attribute of <first>
after -- The id defining from which item to start
before -- The id defining from which item to
start when browsing backwards
max -- Max amount per response
first -- Id for the first item in the response
last -- Id for the last item in the response
index -- Used to set an index to start from
count -- The number of remote items available
Methods:
set_first_index -- Sets the index attribute for <first> and
creates the element if it doesn't exist
get_first_index -- Returns the value of the index
attribute for <first>
del_first_index -- Removes the index attribute for <first>
but keeps the element
set_before -- Sets the value of <before>, if the value is True
then the element will be created without a value
get_before -- Returns the value of <before>, if it is
empty it will return True
"""
namespace = 'http://jabber.org/protocol/rsm'
name = 'set'
plugin_attrib = 'rsm'
sub_interfaces = set(('first', 'after', 'before', 'count',
'index', 'last', 'max'))
interfaces = set(('first_index', 'first', 'after', 'before',
'count', 'index', 'last', 'max'))
def set_first_index(self, val):
fi = self.find("{%s}first" % (self.namespace))
if fi is not None:
if val:
fi.attrib['index'] = val
elif 'index' in fi.attrib:
del fi.attrib['index']
elif val:
fi = ET.Element("{%s}first" % (self.namespace))
fi.attrib['index'] = val
self.xml.append(fi)
def get_first_index(self):
fi = self.find("{%s}first" % (self.namespace))
if fi is not None:
return fi.attrib.get('index', '')
def del_first_index(self):
fi = self.xml.find("{%s}first" % (self.namespace))
if fi is not None:
del fi.attrib['index']
def set_before(self, val):
b = self.xml.find("{%s}before" % (self.namespace))
if b is None and val is True:
self._set_sub_text('{%s}before' % self.namespace, '', True)
else:
self._set_sub_text('{%s}before' % self.namespace, val)
def get_before(self):
b = self.xml.find("{%s}before" % (self.namespace))
if b is not None and not b.text:
return True
elif b is not None:
return b.text
else:
return None
+19
View File
@@ -0,0 +1,19 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0060.pubsub import XEP_0060
from slixmpp.plugins.xep_0060 import stanza
register_plugin(XEP_0060)
# Retain some backwards compatibility
xep_0060 = XEP_0060
+577
View File
@@ -0,0 +1,577 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp.xmlstream import JID
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins.base import BasePlugin
from slixmpp.plugins.xep_0060 import stanza
log = logging.getLogger(__name__)
class XEP_0060(BasePlugin):
"""
XEP-0060 Publish Subscribe
"""
name = 'xep_0060'
description = 'XEP-0060: Publish-Subscribe'
dependencies = set(['xep_0030', 'xep_0004', 'xep_0082', 'xep_0131'])
stanza = stanza
def plugin_init(self):
self.node_event_map = {}
self.xmpp.register_handler(
Callback('Pubsub Event: Items',
StanzaPath('message/pubsub_event/items'),
self._handle_event_items))
self.xmpp.register_handler(
Callback('Pubsub Event: Purge',
StanzaPath('message/pubsub_event/purge'),
self._handle_event_purge))
self.xmpp.register_handler(
Callback('Pubsub Event: Delete',
StanzaPath('message/pubsub_event/delete'),
self._handle_event_delete))
self.xmpp.register_handler(
Callback('Pubsub Event: Configuration',
StanzaPath('message/pubsub_event/configuration'),
self._handle_event_configuration))
self.xmpp.register_handler(
Callback('Pubsub Event: Subscription',
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')
self.xmpp.remove_handler('Pubsub Event: Delete')
self.xmpp.remove_handler('Pubsub Event: Configuration')
self.xmpp.remove_handler('Pubsub Event: Subscription')
def _handle_event_items(self, msg):
"""Raise events for publish and retraction notifications."""
node = msg['pubsub_event']['items']['node']
multi = len(msg['pubsub_event']['items']) > 1
values = {}
if multi:
values = msg.values
del values['pubsub_event']
for item in msg['pubsub_event']['items']:
event_name = self.node_event_map.get(node, None)
event_type = 'publish'
if item.name == 'retract':
event_type = 'retract'
if multi:
condensed = self.xmpp.Message()
condensed.values = values
condensed['pubsub_event']['items']['node'] = node
condensed['pubsub_event']['items'].append(item)
self.xmpp.event('pubsub_%s' % event_type, msg)
if event_name:
self.xmpp.event('%s_%s' % (event_name, event_type),
condensed)
else:
self.xmpp.event('pubsub_%s' % event_type, msg)
if event_name:
self.xmpp.event('%s_%s' % (event_name, event_type), msg)
def _handle_event_purge(self, msg):
"""Raise events for node purge notifications."""
node = msg['pubsub_event']['purge']['node']
event_name = self.node_event_map.get(node, None)
self.xmpp.event('pubsub_purge', msg)
if event_name:
self.xmpp.event('%s_purge' % event_name, msg)
def _handle_event_delete(self, msg):
"""Raise events for node deletion notifications."""
node = msg['pubsub_event']['delete']['node']
event_name = self.node_event_map.get(node, None)
self.xmpp.event('pubsub_delete', msg)
if event_name:
self.xmpp.event('%s_delete' % event_name, msg)
def _handle_event_configuration(self, msg):
"""Raise events for node configuration notifications."""
node = msg['pubsub_event']['configuration']['node']
event_name = self.node_event_map.get(node, None)
self.xmpp.event('pubsub_config', msg)
if event_name:
self.xmpp.event('%s_config' % event_name, msg)
def _handle_event_subscription(self, msg):
"""Raise events for node subscription notifications."""
node = msg['pubsub_event']['subscription']['node']
event_name = self.node_event_map.get(node, None)
self.xmpp.event('pubsub_subscription', msg)
if event_name:
self.xmpp.event('%s_subscription' % event_name, msg)
def map_node_event(self, node, event_name):
"""
Map node names to events.
When a pubsub event is received for the given node,
raise the provided event.
For example::
map_node_event('http://jabber.org/protocol/tune',
'user_tune')
will produce the events 'user_tune_publish' and 'user_tune_retract'
when the respective notifications are received from the node
'http://jabber.org/protocol/tune', among other events.
Arguments:
node -- The node name to map to an event.
event_name -- The name of the event to raise when a
notification from the given node is received.
"""
self.node_event_map[node] = event_name
def create_node(self, jid, node, config=None, ntype=None, ifrom=None,
block=True, callback=None, timeout=None):
"""
Create and configure a new pubsub node.
A server MAY use a different name for the node than the one provided,
so be sure to check the result stanza for a server assigned name.
If no configuration form is provided, the node will be created using
the server's default configuration. To get the default configuration
use get_node_config().
Arguments:
jid -- The JID of the pubsub service.
node -- Optional name of the node to create. If no name is
provided, the server MAY generate a node ID for you.
The server can also assign a different name than the
one you provide; check the result stanza to see if
the server assigned a name.
config -- Optional XEP-0004 data form of configuration settings.
ntype -- The type of node to create. Servers typically default
to using 'leaf' if no type is provided.
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub']['create']['node'] = node
if config is not None:
form_type = 'http://jabber.org/protocol/pubsub#node_config'
if 'FORM_TYPE' in config['fields']:
config.field['FORM_TYPE']['value'] = form_type
else:
config.add_field(var='FORM_TYPE',
ftype='hidden',
value=form_type)
if ntype:
if 'pubsub#node_type' in config['fields']:
config.field['pubsub#node_type']['value'] = ntype
else:
config.add_field(var='pubsub#node_type', value=ntype)
iq['pubsub']['configure'].append(config)
return iq.send(block=block, callback=callback, timeout=timeout)
def subscribe(self, jid, node, bare=True, subscribee=None, options=None,
ifrom=None, block=True, callback=None, timeout=None):
"""
Subscribe to updates from a pubsub node.
The rules for determining the JID that is subscribing to the node are:
1. If subscribee is given, use that as provided.
2. If ifrom was given, use the bare or full version based on bare.
3. Otherwise, use self.xmpp.boundjid based on bare.
Arguments:
jid -- The pubsub service JID.
node -- The node to subscribe to.
bare -- Indicates if the subscribee is a bare or full JID.
Defaults to True for a bare JID.
subscribee -- The JID that is subscribing to the node.
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub']['subscribe']['node'] = node
if subscribee is None:
if ifrom:
if bare:
subscribee = JID(ifrom).bare
else:
subscribee = ifrom
else:
if bare:
subscribee = self.xmpp.boundjid.bare
else:
subscribee = self.xmpp.boundjid
iq['pubsub']['subscribe']['jid'] = subscribee
if options is not None:
iq['pubsub']['options'].append(options)
return iq.send(block=block, callback=callback, timeout=timeout)
def unsubscribe(self, jid, node, subid=None, bare=True, subscribee=None,
ifrom=None, block=True, callback=None, timeout=None):
"""
Unubscribe from updates from a pubsub node.
The rules for determining the JID that is unsubscribing
from the node are:
1. If subscribee is given, use that as provided.
2. If ifrom was given, use the bare or full version based on bare.
3. Otherwise, use self.xmpp.boundjid based on bare.
Arguments:
jid -- The pubsub service JID.
node -- The node to subscribe to.
subid -- The specific subscription, if multiple subscriptions
exist for this JID/node combination.
bare -- Indicates if the subscribee is a bare or full JID.
Defaults to True for a bare JID.
subscribee -- The JID that is subscribing to the node.
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub']['unsubscribe']['node'] = node
if subscribee is None:
if ifrom:
if bare:
subscribee = JID(ifrom).bare
else:
subscribee = ifrom
else:
if bare:
subscribee = self.xmpp.boundjid.bare
else:
subscribee = self.xmpp.boundjid
iq['pubsub']['unsubscribe']['jid'] = subscribee
iq['pubsub']['unsubscribe']['subid'] = subid
return iq.send(block=block, callback=callback, timeout=timeout)
def get_subscriptions(self, jid, node=None, ifrom=None, block=True,
callback=None, timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
iq['pubsub']['subscriptions']['node'] = node
return iq.send(block=block, callback=callback, timeout=timeout)
def get_affiliations(self, jid, node=None, ifrom=None, block=True,
callback=None, timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
iq['pubsub']['affiliations']['node'] = node
return iq.send(block=block, callback=callback, timeout=timeout)
def get_subscription_options(self, jid, node=None, user_jid=None,
ifrom=None, block=True, callback=None,
timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
if user_jid is None:
iq['pubsub']['default']['node'] = node
else:
iq['pubsub']['options']['node'] = node
iq['pubsub']['options']['jid'] = user_jid
return iq.send(block=block, callback=callback, timeout=timeout)
def set_subscription_options(self, jid, node, user_jid, options,
ifrom=None, block=True, callback=None,
timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
iq['pubsub']['options']['node'] = node
iq['pubsub']['options']['jid'] = user_jid
iq['pubsub']['options'].append(options)
return iq.send(block=block, callback=callback, timeout=timeout)
def get_node_config(self, jid, node=None, ifrom=None, block=True,
callback=None, timeout=None):
"""
Retrieve the configuration for a node, or the pubsub service's
default configuration for new nodes.
Arguments:
jid -- The JID of the pubsub service.
node -- The node to retrieve the configuration for. If None,
the default configuration for new nodes will be
requested. Defaults to None.
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
if node is None:
iq['pubsub_owner']['default']
else:
iq['pubsub_owner']['configure']['node'] = node
return iq.send(block=block, callback=callback, timeout=timeout)
def get_node_subscriptions(self, jid, node, ifrom=None, block=True,
callback=None, timeout=None):
"""
Retrieve the subscriptions associated with a given node.
Arguments:
jid -- The JID of the pubsub service.
node -- The node to retrieve subscriptions from.
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
iq['pubsub_owner']['subscriptions']['node'] = node
return iq.send(block=block, callback=callback, timeout=timeout)
def get_node_affiliations(self, jid, node, ifrom=None, block=True,
callback=None, timeout=None):
"""
Retrieve the affiliations associated with a given node.
Arguments:
jid -- The JID of the pubsub service.
node -- The node to retrieve affiliations from.
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
iq['pubsub_owner']['affiliations']['node'] = node
return iq.send(block=block, callback=callback, timeout=timeout)
def delete_node(self, jid, node, ifrom=None, block=True,
callback=None, timeout=None):
"""
Delete a a pubsub node.
Arguments:
jid -- The JID of the pubsub service.
node -- The node to delete.
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub_owner']['delete']['node'] = node
return iq.send(block=block, callback=callback, timeout=timeout)
def set_node_config(self, jid, node, config, ifrom=None, block=True,
callback=None, timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub_owner']['configure']['node'] = node
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,
ifrom=None, block=True, callback=None, timeout=None):
"""
Add a new item to a node, or edit an existing item.
For services that support it, you can use the publish command
as an event signal by not including an ID or payload.
When including a payload and you do not provide an ID then
the service will generally create an ID for you.
Publish options may be specified, and how those options
are processed is left to the service, such as treating
the options as preconditions that the node's settings
must match.
Arguments:
jid -- The JID of the pubsub service.
node -- The node to publish the item to.
id -- Optionally specify the ID of the item.
payload -- The item content to publish.
options -- A 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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub']['publish']['node'] = node
if id is not None:
iq['pubsub']['publish']['item']['id'] = id
if payload is not None:
iq['pubsub']['publish']['item']['payload'] = payload
iq['pubsub']['publish_options'] = options
return iq.send(block=block, callback=callback, timeout=timeout)
def retract(self, jid, node, id, notify=None, ifrom=None, block=True,
callback=None, timeout=None):
"""
Delete a single item from a node.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub']['retract']['node'] = node
iq['pubsub']['retract']['notify'] = notify
iq['pubsub']['retract']['item']['id'] = id
return iq.send(block=block, callback=callback, timeout=timeout)
def purge(self, jid, node, ifrom=None, block=True, callback=None,
timeout=None):
"""
Remove all items from a node.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub_owner']['purge']['node'] = node
return iq.send(block=block, callback=callback, timeout=timeout)
def get_nodes(self, *args, **kwargs):
"""
Discover the nodes provided by a Pubsub service, using disco.
"""
return self.xmpp['xep_0030'].get_items(*args, **kwargs)
def get_item(self, jid, node, item_id, ifrom=None, block=True,
callback=None, timeout=None):
"""
Retrieve the content of an individual item.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
item = stanza.Item()
item['id'] = item_id
iq['pubsub']['items']['node'] = node
iq['pubsub']['items'].append(item)
return iq.send(block=block, callback=callback, timeout=timeout)
def get_items(self, jid, node, item_ids=None, max_items=None,
iterator=False, ifrom=None, block=False,
callback=None, timeout=None):
"""
Request the contents of a node's items.
The desired items can be specified, or a query for the last
few published items can be used.
Pubsub services may use result set management for nodes with
many items, so an iterator can be returned if needed.
"""
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get')
iq['pubsub']['items']['node'] = node
iq['pubsub']['items']['max_items'] = max_items
if item_ids is not None:
for item_id in item_ids:
item = stanza.Item()
item['id'] = item_id
iq['pubsub']['items'].append(item)
if iterator:
return self.xmpp['xep_0059'].iterate(iq, 'pubsub')
else:
return iq.send(block=block, callback=callback, timeout=timeout)
def get_item_ids(self, jid, node, ifrom=None, block=True,
callback=None, timeout=None, iterator=False):
"""
Retrieve the ItemIDs hosted by a given node, using disco.
"""
return self.xmpp['xep_0030'].get_items(jid, node,
ifrom=ifrom,
block=block,
callback=callback,
timeout=timeout,
iterator=iterator)
def modify_affiliations(self, jid, node, affiliations=None, ifrom=None,
block=True, callback=None, timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub_owner']['affiliations']['node'] = node
if affiliations is None:
affiliations = []
for jid, affiliation in affiliations:
aff = stanza.OwnerAffiliation()
aff['jid'] = jid
aff['affiliation'] = affiliation
iq['pubsub_owner']['affiliations'].append(aff)
return iq.send(block=block, callback=callback, timeout=timeout)
def modify_subscriptions(self, jid, node, subscriptions=None, ifrom=None,
block=True, callback=None, timeout=None):
iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set')
iq['pubsub_owner']['subscriptions']['node'] = node
if subscriptions is None:
subscriptions = []
for jid, subscription in subscriptions:
sub = stanza.OwnerSubscription()
sub['jid'] = jid
sub['subscription'] = subscription
iq['pubsub_owner']['subscriptions'].append(sub)
return iq.send(block=block, callback=callback, timeout=timeout)
@@ -0,0 +1,12 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.xep_0060.stanza.pubsub import *
from slixmpp.plugins.xep_0060.stanza.pubsub_owner import *
from slixmpp.plugins.xep_0060.stanza.pubsub_event import *
from slixmpp.plugins.xep_0060.stanza.pubsub_errors import *
+29
View File
@@ -0,0 +1,29 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ET
class OptionalSetting(object):
interfaces = set(('required',))
def set_required(self, value):
if value in (True, 'true', 'True', '1'):
self.xml.append(ET.Element("{%s}required" % self.namespace))
elif self['required']:
self.del_required()
def get_required(self):
required = self.xml.find("{%s}required" % self.namespace)
return required is not None
def del_required(self):
required = self.xml.find("{%s}required" % self.namespace)
if required is not None:
self.xml.remove(required)
+272
View File
@@ -0,0 +1,272 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp import Iq, Message
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
from slixmpp.plugins import xep_0004
from slixmpp.plugins.xep_0060.stanza.base import OptionalSetting
class Pubsub(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'pubsub'
plugin_attrib = name
interfaces = set(tuple())
class Affiliations(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'affiliations'
plugin_attrib = name
interfaces = set(('node',))
class Affiliation(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'affiliation'
plugin_attrib = name
interfaces = set(('node', 'affiliation', 'jid'))
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
class Subscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscription'
plugin_attrib = name
interfaces = set(('jid', 'node', 'subscription', 'subid'))
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
class Subscriptions(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscriptions'
plugin_attrib = name
interfaces = set(('node',))
class SubscribeOptions(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscribe-options'
plugin_attrib = 'suboptions'
interfaces = set(('required',))
class Item(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'item'
plugin_attrib = name
interfaces = set(('id', 'payload'))
def set_payload(self, value):
del self['payload']
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)
if len(childs) > 0:
return childs[0]
def del_payload(self):
for child in self.xml:
self.xml.remove(child)
class Items(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'items'
plugin_attrib = name
interfaces = set(('node', 'max_items'))
def set_max_items(self, value):
self._set_attr('max_items', str(value))
class Create(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'create'
plugin_attrib = name
interfaces = set(('node',))
class Default(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'default'
plugin_attrib = name
interfaces = set(('node', 'type'))
def get_type(self):
t = self._get_attr('type')
if not t:
return 'leaf'
return t
class Publish(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'publish'
plugin_attrib = name
interfaces = set(('node',))
class Retract(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'retract'
plugin_attrib = name
interfaces = set(('node', 'notify'))
def get_notify(self):
notify = self._get_attr('notify')
if notify in ('0', 'false'):
return False
elif notify in ('1', 'true'):
return True
return None
def set_notify(self, value):
del self['notify']
if value is None:
return
elif value in (True, '1', 'true', 'True'):
self._set_attr('notify', 'true')
else:
self._set_attr('notify', 'false')
class Unsubscribe(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'unsubscribe'
plugin_attrib = name
interfaces = set(('node', 'jid', 'subid'))
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
class Subscribe(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'subscribe'
plugin_attrib = name
interfaces = set(('node', 'jid'))
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
class Configure(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'configure'
plugin_attrib = name
interfaces = set(('node', 'type'))
def getType(self):
t = self._get_attr('type')
if not t:
t == 'leaf'
return t
class Options(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'options'
plugin_attrib = name
interfaces = set(('jid', 'node', 'options'))
def __init__(self, *args, **kwargs):
ElementBase.__init__(self, *args, **kwargs)
def get_options(self):
config = self.xml.find('{jabber:x:data}x')
form = xep_0004.Form(xml=config)
return form
def set_options(self, value):
self.xml.append(value.getXML())
return self
def del_options(self):
config = self.xml.find('{jabber:x:data}x')
self.xml.remove(config)
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
class PublishOptions(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'publish-options'
plugin_attrib = 'publish_options'
interfaces = set(('publish_options',))
is_extension = True
def get_publish_options(self):
config = self.xml.find('{jabber:x:data}x')
if config is None:
return None
form = xep_0004.Form(xml=config)
return form
def set_publish_options(self, value):
if value is None:
self.del_publish_options()
else:
self.xml.append(value.getXML())
return self
def del_publish_options(self):
config = self.xml.find('{jabber:x:data}x')
if config is not None:
self.xml.remove(config)
self.parent().xml.remove(self.xml)
register_stanza_plugin(Iq, Pubsub)
register_stanza_plugin(Pubsub, Affiliations)
register_stanza_plugin(Pubsub, Configure)
register_stanza_plugin(Pubsub, Create)
register_stanza_plugin(Pubsub, Default)
register_stanza_plugin(Pubsub, Items)
register_stanza_plugin(Pubsub, Options)
register_stanza_plugin(Pubsub, Publish)
register_stanza_plugin(Pubsub, PublishOptions)
register_stanza_plugin(Pubsub, Retract)
register_stanza_plugin(Pubsub, Subscribe)
register_stanza_plugin(Pubsub, Subscription)
register_stanza_plugin(Pubsub, Subscriptions)
register_stanza_plugin(Pubsub, Unsubscribe)
register_stanza_plugin(Affiliations, Affiliation, iterable=True)
register_stanza_plugin(Configure, xep_0004.Form)
register_stanza_plugin(Items, Item, iterable=True)
register_stanza_plugin(Publish, Item, iterable=True)
register_stanza_plugin(Retract, Item)
register_stanza_plugin(Subscribe, Options)
register_stanza_plugin(Subscription, SubscribeOptions)
register_stanza_plugin(Subscriptions, Subscription, iterable=True)
@@ -0,0 +1,86 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.stanza import Error
from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
class PubsubErrorCondition(ElementBase):
plugin_attrib = 'pubsub'
interfaces = set(('condition', 'unsupported'))
plugin_attrib_map = {}
plugin_tag_map = {}
conditions = set(('closed-node', 'configuration-required', 'invalid-jid',
'invalid-options', 'invalid-payload', 'invalid-subid',
'item-forbidden', 'item-required', 'jid-required',
'max-items-exceeded', 'max-nodes-exceeded',
'nodeid-required', 'not-in-roster-group',
'not-subscribed', 'payload-too-big',
'payload-required', 'pending-subscription',
'presence-subscription-required', 'subid-required',
'too-many-subscriptions', 'unsupported'))
condition_ns = 'http://jabber.org/protocol/pubsub#errors'
def setup(self, xml):
"""Don't create XML for the plugin."""
self.xml = ET.Element('')
def get_condition(self):
"""Return the condition element's name."""
for child in self.parent().xml:
if "{%s}" % self.condition_ns in child.tag:
cond = child.tag.split('}', 1)[-1]
if cond in self.conditions:
return cond
return ''
def set_condition(self, value):
"""
Set the tag name of the condition element.
Arguments:
value -- The tag name of the condition element.
"""
if value in self.conditions:
del self['condition']
cond = ET.Element("{%s}%s" % (self.condition_ns, value))
self.parent().xml.append(cond)
return self
def del_condition(self):
"""Remove the condition element."""
for child in self.parent().xml:
if "{%s}" % self.condition_ns in child.tag:
tag = child.tag.split('}', 1)[-1]
if tag in self.conditions:
self.parent().xml.remove(child)
return self
def get_unsupported(self):
"""Return the name of an unsupported feature"""
xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns)
if xml is not None:
return xml.attrib.get('feature', '')
return ''
def set_unsupported(self, value):
"""Mark a feature as unsupported"""
self.del_unsupported()
xml = ET.Element('{%s}unsupported' % self.condition_ns)
xml.attrib['feature'] = value
self.parent().xml.append(xml)
def del_unsupported(self):
"""Delete an unsupported feature condition."""
xml = self.parent().xml.find('{%s}unsupported' % self.condition_ns)
if xml is not None:
self.parent().xml.remove(xml)
register_stanza_plugin(Error, PubsubErrorCondition)
@@ -0,0 +1,151 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import datetime as dt
from slixmpp import Message
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
from slixmpp.plugins.xep_0004 import Form
from slixmpp.plugins import xep_0082
class Event(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'event'
plugin_attrib = 'pubsub_event'
interfaces = set()
class EventItem(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'item'
plugin_attrib = name
interfaces = set(('id', 'payload', 'node', 'publisher'))
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 EventRetract(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'retract'
plugin_attrib = name
interfaces = set(('id',))
class EventItems(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'items'
plugin_attrib = name
interfaces = set(('node',))
class EventCollection(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'collection'
plugin_attrib = name
interfaces = set(('node',))
class EventAssociate(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'associate'
plugin_attrib = name
interfaces = set(('node',))
class EventDisassociate(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'disassociate'
plugin_attrib = name
interfaces = set(('node',))
class EventConfiguration(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'configuration'
plugin_attrib = name
interfaces = set(('node',))
class EventPurge(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'purge'
plugin_attrib = name
interfaces = set(('node',))
class EventDelete(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'delete'
plugin_attrib = name
interfaces = set(('node', 'redirect'))
def set_redirect(self, uri):
del self['redirect']
redirect = ET.Element('{%s}redirect' % self.namespace)
redirect.attrib['uri'] = uri
self.xml.append(redirect)
def get_redirect(self):
redirect = self.xml.find('{%s}redirect' % self.namespace)
if redirect is not None:
return redirect.attrib.get('uri', '')
return ''
def del_redirect(self):
redirect = self.xml.find('{%s}redirect' % self.namespace)
if redirect is not None:
self.xml.remove(redirect)
class EventSubscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
name = 'subscription'
plugin_attrib = name
interfaces = set(('node', 'expiry', 'jid', 'subid', 'subscription'))
def get_expiry(self):
expiry = self._get_attr('expiry')
if expiry.lower() == 'presence':
return expiry
return xep_0082.parse(expiry)
def set_expiry(self, value):
if isinstance(value, dt.datetime):
value = xep_0082.format_datetime(value)
self._set_attr('expiry', value)
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
register_stanza_plugin(Message, Event)
register_stanza_plugin(Event, EventCollection)
register_stanza_plugin(Event, EventConfiguration)
register_stanza_plugin(Event, EventPurge)
register_stanza_plugin(Event, EventDelete)
register_stanza_plugin(Event, EventItems)
register_stanza_plugin(Event, EventSubscription)
register_stanza_plugin(EventCollection, EventAssociate)
register_stanza_plugin(EventCollection, EventDisassociate)
register_stanza_plugin(EventConfiguration, Form)
register_stanza_plugin(EventItems, EventItem, iterable=True)
register_stanza_plugin(EventItems, EventRetract, iterable=True)
@@ -0,0 +1,134 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp import Iq
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
from slixmpp.plugins.xep_0004 import Form
from slixmpp.plugins.xep_0060.stanza.base import OptionalSetting
from slixmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation
from slixmpp.plugins.xep_0060.stanza.pubsub import Configure, Subscriptions
class PubsubOwner(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'pubsub'
plugin_attrib = 'pubsub_owner'
interfaces = set(tuple())
class DefaultConfig(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'default'
plugin_attrib = name
interfaces = set(('node', 'config'))
def __init__(self, *args, **kwargs):
ElementBase.__init__(self, *args, **kwargs)
def get_config(self):
return self['form']
def set_config(self, value):
del self['from']
self.append(value)
return self
class OwnerAffiliations(Affiliations):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node',))
def append(self, affiliation):
if not isinstance(affiliation, OwnerAffiliation):
raise TypeError
self.xml.append(affiliation.xml)
class OwnerAffiliation(Affiliation):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('affiliation', 'jid'))
class OwnerConfigure(Configure):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'configure'
plugin_attrib = name
interfaces = set(('node',))
class OwnerDefault(OwnerConfigure):
namespace = 'http://jabber.org/protocol/pubsub#owner'
interfaces = set(('node',))
class OwnerDelete(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'delete'
plugin_attrib = name
interfaces = set(('node',))
class OwnerPurge(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'purge'
plugin_attrib = name
interfaces = set(('node',))
class OwnerRedirect(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'redirect'
plugin_attrib = name
interfaces = set(('node', 'jid'))
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
class OwnerSubscriptions(Subscriptions):
name = 'subscriptions'
namespace = 'http://jabber.org/protocol/pubsub#owner'
plugin_attrib = name
interfaces = set(('node',))
def append(self, subscription):
if not isinstance(subscription, OwnerSubscription):
raise TypeError
self.xml.append(subscription.xml)
class OwnerSubscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
name = 'subscription'
plugin_attrib = name
interfaces = set(('jid', 'subscription'))
def set_jid(self, value):
self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
register_stanza_plugin(Iq, PubsubOwner)
register_stanza_plugin(PubsubOwner, DefaultConfig)
register_stanza_plugin(PubsubOwner, OwnerAffiliations)
register_stanza_plugin(PubsubOwner, OwnerConfigure)
register_stanza_plugin(PubsubOwner, OwnerDefault)
register_stanza_plugin(PubsubOwner, OwnerDelete)
register_stanza_plugin(PubsubOwner, OwnerPurge)
register_stanza_plugin(PubsubOwner, OwnerSubscriptions)
register_stanza_plugin(DefaultConfig, Form)
register_stanza_plugin(OwnerAffiliations, OwnerAffiliation, iterable=True)
register_stanza_plugin(OwnerConfigure, Form)
register_stanza_plugin(OwnerDefault, Form)
register_stanza_plugin(OwnerDelete, OwnerRedirect)
register_stanza_plugin(OwnerSubscriptions, OwnerSubscription, iterable=True)
+7
View File
@@ -0,0 +1,7 @@
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0065.stanza import Socks5
from slixmpp.plugins.xep_0065.proxy import XEP_0065
register_plugin(XEP_0065)
+292
View File
@@ -0,0 +1,292 @@
import logging
import threading
import socket
from hashlib import sha1
from uuid import uuid4
from slixmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5
from slixmpp.stanza import Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins.base import base_plugin
from slixmpp.plugins.xep_0065 import stanza, Socks5
log = logging.getLogger(__name__)
class XEP_0065(base_plugin):
name = 'xep_0065'
description = "Socks5 Bytestreams"
dependencies = set(['xep_0030'])
default_config = {
'auto_accept': False
}
def plugin_init(self):
register_stanza_plugin(Iq, Socks5)
self._proxies = {}
self._sessions = {}
self._sessions_lock = threading.Lock()
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))
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 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)
def get_socket(self, sid):
"""Returns the socket associated to the SID."""
return self._sessions.get(sid, None)
def handshake(self, to, ifrom=None, sid=None, timeout=None):
""" Starts the handshake to establish the socks5 bytestreams
connection.
"""
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()
iq['to'] = to
iq['from'] = ifrom
iq['type'] = 'set'
iq['socks']['sid'] = sid
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
discovered = set()
disco_items = self.xmpp['xep_0030'].get_items(jid, timeout=timeout)
for item in disco_items['disco_items']['items']:
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'])
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
return self._proxies
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):
"""Handle incoming SOCKS5 session request."""
sid = iq['socks']['sid']
if not sid:
raise XMPPError(etype='modify', condition='bad-request')
if not self._accept_stream(iq):
raise XMPPError(etype='modify', condition='not-acceptable')
streamhosts = iq['socks']['streamhosts']
conn = None
used_streamhost = None
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 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 sockets."""
for sid, sock in self._sessions.items():
sock.close()
with self._sessions_lock:
self._sessions = {}
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>
peer : The JID for the other side of the stream, regardless
of target or requester status.
"""
# 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)
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(sid)
digest.update(str(requester))
digest.update(str(target))
dest = digest.hexdigest()
# The port MUST be 0.
sock.connect((dest, 0))
log.info('Socket connected.')
_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
sock.peer_jid = peer
sock.self_jid = target if requester == peer else requester
self.xmpp.event('socks_connected', sid)
return sock
def _accept_stream(self, iq):
receiver = iq['to']
sender = iq['from']
sid = iq['socks']['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):
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
+47
View File
@@ -0,0 +1,47 @@
from slixmpp.jid import JID
from slixmpp.xmlstream import ElementBase, register_stanza_plugin
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):
name = 'streamhost'
namespace = 'http://jabber.org/protocol/bytestreams'
plugin_attrib = 'streamhost'
plugin_multi_attrib = 'streamhosts'
interfaces = set(['host', 'jid', 'port'])
def set_jid(self, value):
return self._set_attr('jid', str(value))
def get_jid(self):
return JID(self._get_attr('jid'))
class StreamHostUsed(ElementBase):
name = 'streamhost-used'
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))
def get_jid(self):
return JID(self._get_attr('jid'))
register_stanza_plugin(Socks5, StreamHost, iterable=True)
register_stanza_plugin(Socks5, StreamHostUsed)
+20
View File
@@ -0,0 +1,20 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0066 import stanza
from slixmpp.plugins.xep_0066.stanza import OOB, OOBTransfer
from slixmpp.plugins.xep_0066.oob import XEP_0066
register_plugin(XEP_0066)
# Retain some backwards compatibility
xep_0066 = XEP_0066
+158
View File
@@ -0,0 +1,158 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
from slixmpp.stanza import Message, Presence, Iq
from slixmpp.exceptions import XMPPError
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0066 import stanza
log = logging.getLogger(__name__)
class XEP_0066(BasePlugin):
"""
XEP-0066: Out of Band Data
Out of Band Data is a basic method for transferring files between
XMPP agents. The URL of the resource in question is sent to the receiving
entity, which then downloads the resource before responding to the OOB
request. OOB is also used as a generic means to transmit URLs in other
stanzas to indicate where to find additional information.
Also see <http://www.xmpp.org/extensions/xep-0066.html>.
Events:
oob_transfer -- Raised when a request to download a resource
has been received.
Methods:
send_oob -- Send a request to another entity to download a file
or other addressable resource.
"""
name = 'xep_0066'
description = 'XEP-0066: Out of Band Data'
dependencies = set(['xep_0030'])
stanza = stanza
def plugin_init(self):
"""Start the XEP-0066 plugin."""
self.url_handlers = {'global': self._default_handler,
'jid': {}}
register_stanza_plugin(Iq, stanza.OOBTransfer)
register_stanza_plugin(Message, stanza.OOB)
register_stanza_plugin(Presence, stanza.OOB)
self.xmpp.register_handler(
Callback('OOB Transfer',
StanzaPath('iq@type=set/oob_transfer'),
self._handle_transfer))
def plugin_end(self):
self.xmpp.remove_handler('OOB Transfer')
self.xmpp['xep_0030'].del_feature(feature=stanza.OOBTransfer.namespace)
self.xmpp['xep_0030'].del_feature(feature=stanza.OOB.namespace)
def session_bind(self, jid):
self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace)
self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace)
def register_url_handler(self, jid=None, handler=None):
"""
Register a handler to process download requests, either for all
JIDs or a single JID.
Arguments:
jid -- If None, then set the handler as a global default.
handler -- If None, then remove the existing handler for the
given JID, or reset the global handler if the JID
is None.
"""
if jid is None:
if handler is not None:
self.url_handlers['global'] = handler
else:
self.url_handlers['global'] = self._default_handler
else:
if handler is not None:
self.url_handlers['jid'][jid] = handler
else:
del self.url_handlers['jid'][jid]
def send_oob(self, to, url, desc=None, ifrom=None, **iqargs):
"""
Initiate a basic file transfer by sending the URL of
a file or other resource.
Arguments:
url -- The URL of the resource to transfer.
desc -- An optional human readable description of the item
that is to be transferred.
ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for
a reply. If None, then wait indefinitely.
callback -- Optional callback to execute when a reply is
received instead of blocking and waiting for
the reply.
"""
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = to
iq['from'] = ifrom
iq['oob_transfer']['url'] = url
iq['oob_transfer']['desc'] = desc
return iq.send(**iqargs)
def _run_url_handler(self, iq):
"""
Execute the appropriate handler for a transfer request.
Arguments:
iq -- The Iq stanza containing the OOB transfer request.
"""
if iq['to'] in self.url_handlers['jid']:
return self.url_handlers['jid'][iq['to']](iq)
else:
if self.url_handlers['global']:
self.url_handlers['global'](iq)
else:
raise XMPPError('service-unavailable')
def _default_handler(self, iq):
"""
As a safe default, don't actually download files.
Register a new handler using self.register_url_handler to
screen requests and download files.
Arguments:
iq -- The Iq stanza containing the OOB transfer request.
"""
raise XMPPError('service-unavailable')
def _handle_transfer(self, iq):
"""
Handle receiving an out-of-band transfer request.
Arguments:
iq -- An Iq stanza containing an OOB transfer request.
"""
log.debug('Received out-of-band data request for %s from %s:' % (
iq['oob_transfer']['url'], iq['from']))
self._run_url_handler(iq)
iq.reply().send()
+33
View File
@@ -0,0 +1,33 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
class OOBTransfer(ElementBase):
"""
"""
name = 'query'
namespace = 'jabber:iq:oob'
plugin_attrib = 'oob_transfer'
interfaces = set(('url', 'desc', 'sid'))
sub_interfaces = set(('url', 'desc'))
class OOB(ElementBase):
"""
"""
name = 'x'
namespace = 'jabber:x:oob'
plugin_attrib = 'oob'
interfaces = set(('url', 'desc'))
sub_interfaces = interfaces
+15
View File
@@ -0,0 +1,15 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permissio
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0071.stanza import XHTML_IM
from slixmpp.plugins.xep_0071.xhtml_im import XEP_0071
register_plugin(XEP_0071)
+81
View File
@@ -0,0 +1,81 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.stanza import Message
from slixmpp.util import unicode
from slixmpp.thirdparty import OrderedDict
from slixmpp.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
+30
View File
@@ -0,0 +1,30 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.stanza import Message
from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.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)
+19
View File
@@ -0,0 +1,19 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0077.stanza import Register, RegisterFeature
from slixmpp.plugins.xep_0077.register import XEP_0077
register_plugin(XEP_0077)
# Retain some backwards compatibility
xep_0077 = XEP_0077
+115
View File
@@ -0,0 +1,115 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
import ssl
from slixmpp.stanza import StreamFeatures, Iq
from slixmpp.xmlstream import register_stanza_plugin, JID
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0077 import stanza, Register, RegisterFeature
log = logging.getLogger(__name__)
class XEP_0077(BasePlugin):
"""
XEP-0077: In-Band Registration
"""
name = 'xep_0077'
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):
register_stanza_plugin(StreamFeatures, RegisterFeature)
register_stanza_plugin(Iq, Register)
if not self.xmpp.is_component:
self.xmpp.register_feature('register',
self._handle_register_feature,
restart=False,
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.order)
def _force_registration(self, event):
if self.force_registration:
self.xmpp.add_filter('in', self._force_stream_feature)
def _force_stream_feature(self, stanza):
if isinstance(stanza, StreamFeatures):
if self.xmpp.use_tls or self.xmpp.use_ssl:
if 'starttls' not in self.xmpp.features:
return stanza
elif not isinstance(self.xmpp.socket, ssl.SSLSocket):
return stanza
if 'mechanisms' not in self.xmpp.features:
log.debug('Forced adding in-band registration stream feature')
stanza.enable('register')
self.xmpp.del_filter('in', self._force_stream_feature)
return stanza
def _handle_register_feature(self, features):
if 'mechanisms' in self.xmpp.features:
# We have already logged in with an account
return False
if self.create_account and self.xmpp.event_handled('register'):
form = self.get_registration()
self.xmpp.event('register', form, direct=True)
return True
return False
def get_registration(self, jid=None, ifrom=None, block=True,
timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
iq['from'] = ifrom
iq.enable('register')
return iq.send(block=block, timeout=timeout,
callback=callback, now=True)
def cancel_registration(self, jid=None, ifrom=None, block=True,
timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = jid
iq['from'] = ifrom
iq['register']['remove'] = True
return iq.send(block=block, timeout=timeout, callback=callback)
def change_password(self, password, jid=None, ifrom=None, block=True,
timeout=None, callback=None):
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['to'] = jid
iq['from'] = ifrom
if self.xmpp.is_component:
ifrom = JID(ifrom)
iq['register']['username'] = ifrom.user
else:
iq['register']['username'] = self.xmpp.boundjid.user
iq['register']['password'] = password
return iq.send(block=block, timeout=timeout, callback=callback)
+73
View File
@@ -0,0 +1,73 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from __future__ import unicode_literals
from slixmpp.xmlstream import ElementBase, ET
class Register(ElementBase):
namespace = 'jabber:iq:register'
name = 'query'
plugin_attrib = 'register'
interfaces = set(('username', 'password', 'email', 'nick', 'name',
'first', 'last', 'address', 'city', 'state', 'zip',
'phone', 'url', 'date', 'misc', 'text', 'key',
'registered', 'remove', 'instructions', 'fields'))
sub_interfaces = interfaces
form_fields = set(('username', 'password', 'email', 'nick', 'name',
'first', 'last', 'address', 'city', 'state', 'zip',
'phone', 'url', 'date', 'misc', 'text', 'key'))
def get_registered(self):
present = self.xml.find('{%s}registered' % self.namespace)
return present is not None
def get_remove(self):
present = self.xml.find('{%s}remove' % self.namespace)
return present is not None
def set_registered(self, value):
if value:
self.add_field('registered')
else:
del self['registered']
def set_remove(self, value):
if value:
self.add_field('remove')
else:
del self['remove']
def add_field(self, value):
self._set_sub_text(value, '', keep=True)
def get_fields(self):
fields = set()
for field in self.form_fields:
if self.xml.find('{%s}%s' % (self.namespace, field)) is not None:
fields.add(field)
return fields
def set_fields(self, fields):
del self['fields']
for field in fields:
self._set_sub_text(field, '', keep=True)
def del_fields(self):
for field in self.form_fields:
self._del_sub(field)
class RegisterFeature(ElementBase):
name = 'register'
namespace = 'http://jabber.org/features/iq-register'
plugin_attrib = name
interfaces = set()
+20
View File
@@ -0,0 +1,20 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0078 import stanza
from slixmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature
from slixmpp.plugins.xep_0078.legacyauth import XEP_0078
register_plugin(XEP_0078)
# Retain some backwards compatibility
xep_0078 = XEP_0078
+147
View File
@@ -0,0 +1,147 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import uuid
import logging
import hashlib
import random
import sys
from slixmpp.jid import JID
from slixmpp.exceptions import IqError, IqTimeout
from slixmpp.stanza import Iq, StreamFeatures
from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
from slixmpp.plugins import BasePlugin
from slixmpp.plugins.xep_0078 import stanza
log = logging.getLogger(__name__)
class XEP_0078(BasePlugin):
"""
XEP-0078 NON-SASL Authentication
This XEP is OBSOLETE in favor of using SASL, so DO NOT use this plugin
unless you are forced to use an old XMPP server implementation.
"""
name = 'xep_0078'
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.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.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
log.debug("Starting jabber:iq:auth Authentication")
# Step 1: Request the auth form
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = self.xmpp.requested_jid.host
iq['auth']['username'] = self.xmpp.requested_jid.user
try:
resp = iq.send(now=True)
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
except IqTimeout:
log.info("Authentication failed: %s", 'timeout')
self.xmpp.event('failed_auth', direct=True)
self.xmpp.disconnect()
return True
# Step 2: Fill out auth form for either password or digest auth
iq = self.xmpp.Iq()
iq['type'] = 'set'
iq['auth']['username'] = self.xmpp.requested_jid.user
# A resource is required, so create a random one if necessary
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')
if sys.version_info < (3, 0):
stream_id = bytes(self.xmpp.stream_id)
password = bytes(self.xmpp.password)
else:
stream_id = bytes(self.xmpp.stream_id, encoding='utf-8')
password = bytes(self.xmpp.password, encoding='utf-8')
digest = hashlib.sha1(b'%s%s' % (stream_id, password)).hexdigest()
iq['auth']['digest'] = digest
else:
log.warning('Authenticating via jabber:iq:auth Plain.')
iq['auth']['password'] = self.xmpp.password
# Step 3: Send credentials
try:
result = iq.send(now=True)
except IqError as err:
log.info("Authentication failed")
self.xmpp.event("failed_auth", direct=True)
self.xmpp.disconnect()
except IqTimeout:
log.info("Authentication failed")
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()
self.xmpp.event('session_start')
return True
+41
View File
@@ -0,0 +1,41 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2011 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase, ET, register_stanza_plugin
class IqAuth(ElementBase):
namespace = 'jabber:iq:auth'
name = 'query'
plugin_attrib = 'auth'
interfaces = set(('fields', 'username', 'password', 'resource', 'digest'))
sub_interfaces = set(('username', 'password', 'resource', 'digest'))
plugin_tag_map = {}
plugin_attrib_map = {}
def get_fields(self):
fields = set()
for field in self.sub_interfaces:
if self.xml.find('{%s}%s' % (self.namespace, field)) is not None:
fields.add(field)
return fields
def set_resource(self, value):
self._set_sub_text('resource', value, keep=True)
def set_password(self, value):
self._set_sub_text('password', value, keep=True)
class AuthFeature(ElementBase):
namespace = 'http://jabber.org/features/iq-auth'
name = 'auth'
plugin_attrib = 'auth'
interfaces = set()
plugin_tag_map = {}
plugin_attrib_map = {}
+18
View File
@@ -0,0 +1,18 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0079.stanza import (
AMP, Rule, InvalidRules, UnsupportedConditions,
UnsupportedActions, FailedRules, FailedRule,
AMPFeature)
from slixmpp.plugins.xep_0079.amp import XEP_0079
register_plugin(XEP_0079)
+79
View File
@@ -0,0 +1,79 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permissio
"""
import logging
from slixmpp.stanza import Message, Error, StreamFeatures
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.xmlstream.matcher import StanzaPath, MatchMany
from slixmpp.xmlstream.handler import Callback
from slixmpp.plugins import BasePlugin
from slixmpp.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)
+96
View File
@@ -0,0 +1,96 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from __future__ import unicode_literals
from slixmpp.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)
+15
View File
@@ -0,0 +1,15 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0080.stanza import Geoloc
from slixmpp.plugins.xep_0080.geoloc import XEP_0080
register_plugin(XEP_0080)
+125
View File
@@ -0,0 +1,125 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
import logging
import slixmpp
from slixmpp.plugins.base import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin
from slixmpp.plugins.xep_0080 import stanza, Geoloc
log = logging.getLogger(__name__)
class XEP_0080(BasePlugin):
"""
XEP-0080: User Location
"""
name = 'xep_0080'
description = 'XEP-0080: User Location'
dependencies = set(['xep_0163'])
stanza = stanza
def plugin_end(self):
self.xmpp['xep_0163'].remove_interest(Geoloc.namespace)
self.xmpp['xep_0030'].del_feature(feature=Geoloc.namespace)
def session_bind(self, jid):
self.xmpp['xep_0163'].register_pep('user_location', Geoloc)
def publish_location(self, **kwargs):
"""
Publish the user's current location.
Arguments:
accuracy -- Horizontal GPS error in meters.
alt -- Altitude in meters above or below sea level.
area -- A named area such as a campus or neighborhood.
bearing -- GPS bearing (direction in which the entity is
heading to reach its next waypoint), measured in
decimal degrees relative to true north.
building -- A specific building on a street or in an area.
country -- The nation where the user is located.
countrycode -- The ISO 3166 two-letter country code.
datum -- GPS datum.
description -- A natural-language name for or description of
the location.
error -- Horizontal GPS error in arc minutes. Obsoleted by
the accuracy parameter.
floor -- A particular floor in a building.
lat -- Latitude in decimal degrees North.
locality -- A locality within the administrative region, such
as a town or city.
lon -- Longitude in decimal degrees East.
postalcode -- A code used for postal delivery.
region -- An administrative region of the nation, such
as a state or province.
room -- A particular room in a building.
speed -- The speed at which the entity is moving,
in meters per second.
street -- A thoroughfare within the locality, or a crossing
of two thoroughfares.
text -- A catch-all element that captures any other
information about the location.
timestamp -- UTC timestamp specifying the moment when the
reading was taken.
uri -- A URI or URL pointing to information about
the location.
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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
options = kwargs.get('options', None)
ifrom = kwargs.get('ifrom', None)
block = kwargs.get('block', None)
callback = kwargs.get('callback', None)
timeout = kwargs.get('timeout', None)
for param in ('ifrom', 'block', 'callback', 'timeout', 'options'):
if param in kwargs:
del kwargs[param]
geoloc = Geoloc()
geoloc.values = kwargs
return self.xmpp['xep_0163'].publish(geoloc,
options=options,
ifrom=ifrom,
block=block,
callback=callback,
timeout=timeout)
def stop(self, ifrom=None, block=True, callback=None, timeout=None):
"""
Clear existing user location 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 slixmpp.xmlstream.RESPONSE_TIMEOUT
callback -- Optional reference to a stream handler function. Will
be executed when a reply stanza is received.
"""
geoloc = Geoloc()
return self.xmpp['xep_0163'].publish(geoloc,
ifrom=ifrom,
block=block,
callback=callback,
timeout=timeout)
+266
View File
@@ -0,0 +1,266 @@
"""
Slixmpp: The Slick XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of Slixmpp.
See the file LICENSE for copying permission.
"""
from slixmpp.xmlstream import ElementBase
from slixmpp.plugins import xep_0082
class Geoloc(ElementBase):
"""
XMPP's <geoloc> stanza allows entities to know the current
geographical or physical location of an entity. (XEP-0080: User Location)
Example <geoloc> stanzas:
<geoloc xmlns='http://jabber.org/protocol/geoloc'/>
<geoloc xmlns='http://jabber.org/protocol/geoloc' xml:lang='en'>
<accuracy>20</accuracy>
<country>Italy</country>
<lat>45.44</lat>
<locality>Venice</locality>
<lon>12.33</lon>
</geoloc>
Stanza Interface:
accuracy -- Horizontal GPS error in meters.
alt -- Altitude in meters above or below sea level.
area -- A named area such as a campus or neighborhood.
bearing -- GPS bearing (direction in which the entity is
heading to reach its next waypoint), measured in
decimal degrees relative to true north.
building -- A specific building on a street or in an area.
country -- The nation where the user is located.
countrycode -- The ISO 3166 two-letter country code.
datum -- GPS datum.
description -- A natural-language name for or description of
the location.
error -- Horizontal GPS error in arc minutes. Obsoleted by
the accuracy parameter.
floor -- A particular floor in a building.
lat -- Latitude in decimal degrees North.
locality -- A locality within the administrative region, such
as a town or city.
lon -- Longitude in decimal degrees East.
postalcode -- A code used for postal delivery.
region -- An administrative region of the nation, such
as a state or province.
room -- A particular room in a building.
speed -- The speed at which the entity is moving,
in meters per second.
street -- A thoroughfare within the locality, or a crossing
of two thoroughfares.
text -- A catch-all element that captures any other
information about the location.
timestamp -- UTC timestamp specifying the moment when the
reading was taken.
uri -- A URI or URL pointing to information about
the location.
"""
namespace = 'http://jabber.org/protocol/geoloc'
name = 'geoloc'
interfaces = set(('accuracy', 'alt', 'area', 'bearing', 'building',
'country', 'countrycode', 'datum', 'dscription',
'error', 'floor', 'lat', 'locality', 'lon',
'postalcode', 'region', 'room', 'speed', 'street',
'text', 'timestamp', 'uri'))
sub_interfaces = interfaces
plugin_attrib = name
def exception(self, e):
"""
Override exception passback for presence.
"""
pass
def set_accuracy(self, accuracy):
"""
Set the value of the <accuracy> element.
Arguments:
accuracy -- Horizontal GPS error in meters
"""
self._set_sub_text('accuracy', text=str(accuracy))
return self
def get_accuracy(self):
"""
Return the value of the <accuracy> element as an integer.
"""
p = self._get_sub_text('accuracy')
if not p:
return None
else:
try:
return int(p)
except ValueError:
return None
def set_alt(self, alt):
"""
Set the value of the <alt> element.
Arguments:
alt -- Altitude in meters above or below sea level
"""
self._set_sub_text('alt', text=str(alt))
return self
def get_alt(self):
"""
Return the value of the <alt> element as an integer.
"""
p = self._get_sub_text('alt')
if not p:
return None
else:
try:
return int(p)
except ValueError:
return None
def set_bearing(self, bearing):
"""
Set the value of the <bearing> element.
Arguments:
bearing -- GPS bearing (direction in which the entity is heading
to reach its next waypoint), measured in decimal
degrees relative to true north
"""
self._set_sub_text('bearing', text=str(bearing))
return self
def get_bearing(self):
"""
Return the value of the <bearing> element as a float.
"""
p = self._get_sub_text('bearing')
if not p:
return None
else:
try:
return float(p)
except ValueError:
return None
def set_error(self, error):
"""
Set the value of the <error> element.
Arguments:
error -- Horizontal GPS error in arc minutes; this
element is deprecated in favor of <accuracy/>
"""
self._set_sub_text('error', text=str(error))
return self
def get_error(self):
"""
Return the value of the <error> element as a float.
"""
p = self._get_sub_text('error')
if not p:
return None
else:
try:
return float(p)
except ValueError:
return None
def set_lat(self, lat):
"""
Set the value of the <lat> element.
Arguments:
lat -- Latitude in decimal degrees North
"""
self._set_sub_text('lat', text=str(lat))
return self
def get_lat(self):
"""
Return the value of the <lat> element as a float.
"""
p = self._get_sub_text('lat')
if not p:
return None
else:
try:
return float(p)
except ValueError:
return None
def set_lon(self, lon):
"""
Set the value of the <lon> element.
Arguments:
lon -- Longitude in decimal degrees East
"""
self._set_sub_text('lon', text=str(lon))
return self
def get_lon(self):
"""
Return the value of the <lon> element as a float.
"""
p = self._get_sub_text('lon')
if not p:
return None
else:
try:
return float(p)
except ValueError:
return None
def set_speed(self, speed):
"""
Set the value of the <speed> element.
Arguments:
speed -- The speed at which the entity is moving,
in meters per second
"""
self._set_sub_text('speed', text=str(speed))
return self
def get_speed(self):
"""
Return the value of the <speed> element as a float.
"""
p = self._get_sub_text('speed')
if not p:
return None
else:
try:
return float(p)
except ValueError:
return None
def set_timestamp(self, timestamp):
"""
Set the value of the <timestamp> element.
Arguments:
timestamp -- UTC timestamp specifying the moment when
the reading was taken
"""
self._set_sub_text('timestamp', text=str(xep_0082.datetime(timestamp)))
return self
def get_timestamp(self):
"""
Return the value of the <timestamp> element as a DateTime.
"""
p = self._get_sub_text('timestamp')
if not p:
return None
else:
return xep_0082.datetime(p)

Some files were not shown because too many files have changed in this diff Show More