Rename to slixmpp
This commit is contained in:
30
slixmpp/__init__.py
Normal file
30
slixmpp/__init__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
if hasattr(logging, 'NullHandler'):
|
||||
NullHandler = logging.NullHandler
|
||||
else:
|
||||
class NullHandler(logging.Handler):
|
||||
def handle(self, record):
|
||||
pass
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
del NullHandler
|
||||
|
||||
|
||||
from slixmpp.stanza import Message, Presence, Iq
|
||||
from slixmpp.jid import JID, InvalidJID
|
||||
from slixmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin
|
||||
from slixmpp.xmlstream.handler import *
|
||||
from slixmpp.xmlstream import XMLStream, RestartStream
|
||||
from slixmpp.xmlstream.matcher import *
|
||||
from slixmpp.basexmpp import BaseXMPP
|
||||
from slixmpp.clientxmpp import ClientXMPP
|
||||
from slixmpp.componentxmpp import ComponentXMPP
|
||||
|
||||
from slixmpp.version import __version__, __version_info__
|
||||
200
slixmpp/api.py
Normal file
200
slixmpp/api.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from slixmpp.xmlstream import JID
|
||||
|
||||
|
||||
class APIWrapper(object):
|
||||
|
||||
def __init__(self, api, name):
|
||||
self.api = api
|
||||
self.name = name
|
||||
if name not in self.api.settings:
|
||||
self.api.settings[name] = {}
|
||||
|
||||
def __getattr__(self, attr):
|
||||
"""Curry API management commands with the API name."""
|
||||
if attr == 'name':
|
||||
return self.name
|
||||
elif attr == 'settings':
|
||||
return self.api.settings[self.name]
|
||||
elif attr == 'register':
|
||||
def partial(handler, op, jid=None, node=None, default=False):
|
||||
register = getattr(self.api, attr)
|
||||
return register(handler, self.name, op, jid, node, default)
|
||||
return partial
|
||||
elif attr == 'register_default':
|
||||
def partial(handler, op, jid=None, node=None):
|
||||
return getattr(self.api, attr)(handler, self.name, op)
|
||||
return partial
|
||||
elif attr in ('run', 'restore_default', 'unregister'):
|
||||
def partial(*args, **kwargs):
|
||||
return getattr(self.api, attr)(self.name, *args, **kwargs)
|
||||
return partial
|
||||
return None
|
||||
|
||||
def __getitem__(self, attr):
|
||||
def partial(jid=None, node=None, ifrom=None, args=None):
|
||||
return self.api.run(self.name, attr, jid, node, ifrom, args)
|
||||
return partial
|
||||
|
||||
|
||||
class APIRegistry(object):
|
||||
|
||||
def __init__(self, xmpp):
|
||||
self._handlers = {}
|
||||
self._handler_defaults = {}
|
||||
self.xmpp = xmpp
|
||||
self.settings = {}
|
||||
|
||||
def _setup(self, ctype, op):
|
||||
"""Initialize the API callback dictionaries.
|
||||
|
||||
:param string ctype: The name of the API to initialize.
|
||||
:param string op: The API operation to initialize.
|
||||
"""
|
||||
if ctype not in self.settings:
|
||||
self.settings[ctype] = {}
|
||||
if ctype not in self._handler_defaults:
|
||||
self._handler_defaults[ctype] = {}
|
||||
if ctype not in self._handlers:
|
||||
self._handlers[ctype] = {}
|
||||
if op not in self._handlers[ctype]:
|
||||
self._handlers[ctype][op] = {'global': None,
|
||||
'jid': {},
|
||||
'node': {}}
|
||||
|
||||
def wrap(self, ctype):
|
||||
"""Return a wrapper object that targets a specific API."""
|
||||
return APIWrapper(self, ctype)
|
||||
|
||||
def purge(self, ctype):
|
||||
"""Remove all information for a given API."""
|
||||
del self.settings[ctype]
|
||||
del self._handler_defaults[ctype]
|
||||
del self._handlers[ctype]
|
||||
|
||||
def run(self, ctype, op, jid=None, node=None, ifrom=None, args=None):
|
||||
"""Execute an API callback, based on specificity.
|
||||
|
||||
The API callback that is executed is chosen based on the combination
|
||||
of the provided JID and node:
|
||||
|
||||
JID | node | Handler
|
||||
==============================
|
||||
Given | Given | Node handler
|
||||
Given | None | JID handler
|
||||
None | None | Global handler
|
||||
|
||||
A node handler is responsible for servicing a single node at a single
|
||||
JID, while a JID handler may respond for any node at a given JID, and
|
||||
the global handler will answer to any JID+node combination.
|
||||
|
||||
Handlers should check that the JID ``ifrom`` is authorized to perform
|
||||
the desired action.
|
||||
|
||||
:param string ctype: The name of the API to use.
|
||||
:param string op: The API operation to perform.
|
||||
:param JID jid: Optionally provide specific JID.
|
||||
:param string node: Optionally provide specific node.
|
||||
:param JID ifrom: Optionally provide the requesting JID.
|
||||
:param tuple args: Optional positional arguments to the handler.
|
||||
"""
|
||||
self._setup(ctype, op)
|
||||
|
||||
if not jid:
|
||||
jid = self.xmpp.boundjid
|
||||
elif jid and not isinstance(jid, JID):
|
||||
jid = JID(jid)
|
||||
elif jid == JID(''):
|
||||
jid = self.xmpp.boundjid
|
||||
|
||||
if node is None:
|
||||
node = ''
|
||||
|
||||
if self.xmpp.is_component:
|
||||
if self.settings[ctype].get('component_bare', False):
|
||||
jid = jid.bare
|
||||
else:
|
||||
jid = jid.full
|
||||
else:
|
||||
if self.settings[ctype].get('client_bare', False):
|
||||
jid = jid.bare
|
||||
else:
|
||||
jid = jid.full
|
||||
|
||||
jid = JID(jid)
|
||||
|
||||
handler = self._handlers[ctype][op]['node'].get((jid, node), None)
|
||||
if handler is None:
|
||||
handler = self._handlers[ctype][op]['jid'].get(jid, None)
|
||||
if handler is None:
|
||||
handler = self._handlers[ctype][op].get('global', None)
|
||||
|
||||
if handler:
|
||||
try:
|
||||
return handler(jid, node, ifrom, args)
|
||||
except TypeError:
|
||||
# To preserve backward compatibility, drop the ifrom
|
||||
# parameter for existing handlers that don't understand it.
|
||||
return handler(jid, node, args)
|
||||
|
||||
def register(self, handler, ctype, op, jid=None, node=None, default=False):
|
||||
"""Register an API callback, with JID+node specificity.
|
||||
|
||||
The API callback can later be executed based on the
|
||||
specificity of the provided JID+node combination.
|
||||
|
||||
See :meth:`~ApiRegistry.run` for more details.
|
||||
|
||||
:param string ctype: The name of the API to use.
|
||||
:param string op: The API operation to perform.
|
||||
:param JID jid: Optionally provide specific JID.
|
||||
:param string node: Optionally provide specific node.
|
||||
"""
|
||||
self._setup(ctype, op)
|
||||
if jid is None and node is None:
|
||||
if handler is None:
|
||||
handler = self._handler_defaults[op]
|
||||
self._handlers[ctype][op]['global'] = handler
|
||||
elif jid is not None and node is None:
|
||||
self._handlers[ctype][op]['jid'][jid] = handler
|
||||
else:
|
||||
self._handlers[ctype][op]['node'][(jid, node)] = handler
|
||||
|
||||
if default:
|
||||
self.register_default(handler, ctype, op)
|
||||
|
||||
def register_default(self, handler, ctype, op):
|
||||
"""Register a default, global handler for an operation.
|
||||
|
||||
:param func handler: The default, global handler for the operation.
|
||||
:param string ctype: The name of the API to modify.
|
||||
:param string op: The API operation to use.
|
||||
"""
|
||||
self._setup(ctype, op)
|
||||
self._handler_defaults[ctype][op] = handler
|
||||
|
||||
def unregister(self, ctype, op, jid=None, node=None):
|
||||
"""Remove an API callback.
|
||||
|
||||
The API callback chosen for removal is based on the
|
||||
specificity of the provided JID+node combination.
|
||||
|
||||
See :meth:`~ApiRegistry.run` for more details.
|
||||
|
||||
:param string ctype: The name of the API to use.
|
||||
:param string op: The API operation to perform.
|
||||
:param JID jid: Optionally provide specific JID.
|
||||
:param string node: Optionally provide specific node.
|
||||
"""
|
||||
self._setup(ctype, op)
|
||||
self.register(None, ctype, op, jid, node)
|
||||
|
||||
def restore_default(self, ctype, op, jid=None, node=None):
|
||||
"""Reset an API callback to use a default handler.
|
||||
|
||||
:param string ctype: The name of the API to use.
|
||||
:param string op: The API operation to perform.
|
||||
:param JID jid: Optionally provide specific JID.
|
||||
:param string node: Optionally provide specific node.
|
||||
"""
|
||||
self.unregister(ctype, op, jid, node)
|
||||
self.register(self._handler_defaults[ctype][op], ctype, op, jid, node)
|
||||
832
slixmpp/basexmpp.py
Normal file
832
slixmpp/basexmpp.py
Normal file
@@ -0,0 +1,832 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
slixmpp.basexmpp
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides the common XMPP functionality
|
||||
for both clients and components.
|
||||
|
||||
Part of Slixmpp: The Slick XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import with_statement, unicode_literals
|
||||
|
||||
import sys
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from slixmpp import plugins, roster, stanza
|
||||
from slixmpp.api import APIRegistry
|
||||
from slixmpp.exceptions import IqError, IqTimeout
|
||||
|
||||
from slixmpp.stanza import Message, Presence, Iq, StreamError
|
||||
from slixmpp.stanza.roster import Roster
|
||||
from slixmpp.stanza.nick import Nick
|
||||
from slixmpp.stanza.htmlim import HTMLIM
|
||||
|
||||
from slixmpp.xmlstream import XMLStream, JID
|
||||
from slixmpp.xmlstream import ET, register_stanza_plugin
|
||||
from slixmpp.xmlstream.matcher import MatchXPath
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.stanzabase import XML_NS
|
||||
|
||||
from slixmpp.plugins import PluginManager, load_plugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# In order to make sure that Unicode is handled properly
|
||||
# in Python 2.x, reset the default encoding.
|
||||
if sys.version_info < (3, 0):
|
||||
from slixmpp.util.misc_ops import setdefaultencoding
|
||||
setdefaultencoding('utf8')
|
||||
|
||||
|
||||
class BaseXMPP(XMLStream):
|
||||
|
||||
"""
|
||||
The BaseXMPP class adapts the generic XMLStream class for use
|
||||
with XMPP. It also provides a plugin mechanism to easily extend
|
||||
and add support for new XMPP features.
|
||||
|
||||
:param default_ns: Ensure that the correct default XML namespace
|
||||
is used during initialization.
|
||||
"""
|
||||
|
||||
def __init__(self, jid='', default_ns='jabber:client'):
|
||||
XMLStream.__init__(self)
|
||||
|
||||
self.default_ns = default_ns
|
||||
self.stream_ns = 'http://etherx.jabber.org/streams'
|
||||
self.namespace_map[self.stream_ns] = 'stream'
|
||||
|
||||
#: An identifier for the stream as given by the server.
|
||||
self.stream_id = None
|
||||
|
||||
#: The JabberID (JID) requested for this connection.
|
||||
self.requested_jid = JID(jid, cache_lock=True)
|
||||
|
||||
#: The JabberID (JID) used by this connection,
|
||||
#: as set after session binding. This may even be a
|
||||
#: different bare JID than what was requested.
|
||||
self.boundjid = JID(jid, cache_lock=True)
|
||||
|
||||
self._expected_server_name = self.boundjid.host
|
||||
self._redirect_attempts = 0
|
||||
|
||||
#: The maximum number of consecutive see-other-host
|
||||
#: redirections that will be followed before quitting.
|
||||
self.max_redirects = 5
|
||||
|
||||
self.session_bind_event = threading.Event()
|
||||
|
||||
#: A dictionary mapping plugin names to plugins.
|
||||
self.plugin = PluginManager(self)
|
||||
|
||||
#: Configuration options for whitelisted plugins.
|
||||
#: If a plugin is registered without any configuration,
|
||||
#: and there is an entry here, it will be used.
|
||||
self.plugin_config = {}
|
||||
|
||||
#: A list of plugins that will be loaded if
|
||||
#: :meth:`register_plugins` is called.
|
||||
self.plugin_whitelist = []
|
||||
|
||||
#: The main roster object. This roster supports multiple
|
||||
#: owner JIDs, as in the case for components. For clients
|
||||
#: which only have a single JID, see :attr:`client_roster`.
|
||||
self.roster = roster.Roster(self)
|
||||
self.roster.add(self.boundjid)
|
||||
|
||||
#: The single roster for the bound JID. This is the
|
||||
#: equivalent of::
|
||||
#:
|
||||
#: self.roster[self.boundjid.bare]
|
||||
self.client_roster = self.roster[self.boundjid]
|
||||
|
||||
#: The distinction between clients and components can be
|
||||
#: important, primarily for choosing how to handle the
|
||||
#: ``'to'`` and ``'from'`` JIDs of stanzas.
|
||||
self.is_component = False
|
||||
|
||||
#: Messages may optionally be tagged with ID values. Setting
|
||||
#: :attr:`use_message_ids` to `True` will assign all outgoing
|
||||
#: messages an ID. Some plugin features require enabling
|
||||
#: this option.
|
||||
self.use_message_ids = False
|
||||
|
||||
#: Presence updates may optionally be tagged with ID values.
|
||||
#: Setting :attr:`use_message_ids` to `True` will assign all
|
||||
#: outgoing messages an ID.
|
||||
self.use_presence_ids = False
|
||||
|
||||
#: The API registry is a way to process callbacks based on
|
||||
#: JID+node combinations. Each callback in the registry is
|
||||
#: marked with:
|
||||
#:
|
||||
#: - An API name, e.g. xep_0030
|
||||
#: - The name of an action, e.g. get_info
|
||||
#: - The JID that will be affected
|
||||
#: - The node that will be affected
|
||||
#:
|
||||
#: API handlers with no JID or node will act as global handlers,
|
||||
#: while those with a JID and no node will service all nodes
|
||||
#: for a JID, and handlers with both a JID and node will be
|
||||
#: used only for that specific combination. The handler that
|
||||
#: provides the most specificity will be used.
|
||||
self.api = APIRegistry(self)
|
||||
|
||||
#: Flag indicating that the initial presence broadcast has
|
||||
#: been sent. Until this happens, some servers may not
|
||||
#: behave as expected when sending stanzas.
|
||||
self.sentpresence = False
|
||||
|
||||
#: A reference to :mod:`slixmpp.stanza` to make accessing
|
||||
#: stanza classes easier.
|
||||
self.stanza = stanza
|
||||
|
||||
self.register_handler(
|
||||
Callback('IM',
|
||||
MatchXPath('{%s}message/{%s}body' % (self.default_ns,
|
||||
self.default_ns)),
|
||||
self._handle_message))
|
||||
self.register_handler(
|
||||
Callback('Presence',
|
||||
MatchXPath("{%s}presence" % self.default_ns),
|
||||
self._handle_presence))
|
||||
|
||||
self.register_handler(
|
||||
Callback('Stream Error',
|
||||
MatchXPath("{%s}error" % self.stream_ns),
|
||||
self._handle_stream_error))
|
||||
|
||||
self.add_event_handler('session_start',
|
||||
self._handle_session_start)
|
||||
self.add_event_handler('disconnected',
|
||||
self._handle_disconnected)
|
||||
self.add_event_handler('presence_available',
|
||||
self._handle_available)
|
||||
self.add_event_handler('presence_dnd',
|
||||
self._handle_available)
|
||||
self.add_event_handler('presence_xa',
|
||||
self._handle_available)
|
||||
self.add_event_handler('presence_chat',
|
||||
self._handle_available)
|
||||
self.add_event_handler('presence_away',
|
||||
self._handle_available)
|
||||
self.add_event_handler('presence_unavailable',
|
||||
self._handle_unavailable)
|
||||
self.add_event_handler('presence_subscribe',
|
||||
self._handle_subscribe)
|
||||
self.add_event_handler('presence_subscribed',
|
||||
self._handle_subscribed)
|
||||
self.add_event_handler('presence_unsubscribe',
|
||||
self._handle_unsubscribe)
|
||||
self.add_event_handler('presence_unsubscribed',
|
||||
self._handle_unsubscribed)
|
||||
self.add_event_handler('roster_subscription_request',
|
||||
self._handle_new_subscription)
|
||||
|
||||
# Set up the XML stream with XMPP's root stanzas.
|
||||
self.register_stanza(Message)
|
||||
self.register_stanza(Iq)
|
||||
self.register_stanza(Presence)
|
||||
self.register_stanza(StreamError)
|
||||
|
||||
# Initialize a few default stanza plugins.
|
||||
register_stanza_plugin(Iq, Roster)
|
||||
register_stanza_plugin(Message, Nick)
|
||||
|
||||
def start_stream_handler(self, xml):
|
||||
"""Save the stream ID once the streams have been established.
|
||||
|
||||
:param xml: The incoming stream's root element.
|
||||
"""
|
||||
self.stream_id = xml.get('id', '')
|
||||
self.stream_version = xml.get('version', '')
|
||||
self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None)
|
||||
|
||||
if not self.is_component and not self.stream_version:
|
||||
log.warning('Legacy XMPP 0.9 protocol detected.')
|
||||
self.event('legacy_protocol')
|
||||
|
||||
def process(self, *args, **kwargs):
|
||||
"""Initialize plugins and begin processing the XML stream.
|
||||
|
||||
The number of threads used for processing stream events is determined
|
||||
by :data:`HANDLER_THREADS`.
|
||||
|
||||
:param bool block: If ``False``, then event dispatcher will run
|
||||
in a separate thread, allowing for the stream to be
|
||||
used in the background for another application.
|
||||
Otherwise, ``process(block=True)`` blocks the current
|
||||
thread. Defaults to ``False``.
|
||||
:param bool threaded: **DEPRECATED**
|
||||
If ``True``, then event dispatcher will run
|
||||
in a separate thread, allowing for the stream to be
|
||||
used in the background for another application.
|
||||
Defaults to ``True``. This does **not** mean that no
|
||||
threads are used at all if ``threaded=False``.
|
||||
|
||||
Regardless of these threading options, these threads will
|
||||
always exist:
|
||||
|
||||
- The event queue processor
|
||||
- The send queue processor
|
||||
- The scheduler
|
||||
"""
|
||||
for name in self.plugin:
|
||||
if not hasattr(self.plugin[name], 'post_inited'):
|
||||
if hasattr(self.plugin[name], 'post_init'):
|
||||
self.plugin[name].post_init()
|
||||
self.plugin[name].post_inited = True
|
||||
return XMLStream.process(self, *args, **kwargs)
|
||||
|
||||
def register_plugin(self, plugin, pconfig={}, module=None):
|
||||
"""Register and configure a plugin for use in this stream.
|
||||
|
||||
:param plugin: The name of the plugin class. Plugin names must
|
||||
be unique.
|
||||
:param pconfig: A dictionary of configuration data for the plugin.
|
||||
Defaults to an empty dictionary.
|
||||
:param module: Optional refence to the module containing the plugin
|
||||
class if using custom plugins.
|
||||
"""
|
||||
|
||||
# Use the global plugin config cache, if applicable
|
||||
if not pconfig:
|
||||
pconfig = self.plugin_config.get(plugin, {})
|
||||
|
||||
if not self.plugin.registered(plugin):
|
||||
load_plugin(plugin, module)
|
||||
self.plugin.enable(plugin, pconfig)
|
||||
|
||||
def register_plugins(self):
|
||||
"""Register and initialize all built-in plugins.
|
||||
|
||||
Optionally, the list of plugins loaded may be limited to those
|
||||
contained in :attr:`plugin_whitelist`.
|
||||
|
||||
Plugin configurations stored in :attr:`plugin_config` will be used.
|
||||
"""
|
||||
if self.plugin_whitelist:
|
||||
plugin_list = self.plugin_whitelist
|
||||
else:
|
||||
plugin_list = plugins.__all__
|
||||
|
||||
for plugin in plugin_list:
|
||||
if plugin in plugins.__all__:
|
||||
self.register_plugin(plugin)
|
||||
else:
|
||||
raise NameError("Plugin %s not in plugins.__all__." % plugin)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Return a plugin given its name, if it has been registered."""
|
||||
if key in self.plugin:
|
||||
return self.plugin[key]
|
||||
else:
|
||||
log.warning("Plugin '%s' is not loaded.", key)
|
||||
return False
|
||||
|
||||
def get(self, key, default):
|
||||
"""Return a plugin given its name, if it has been registered."""
|
||||
return self.plugin.get(key, default)
|
||||
|
||||
def Message(self, *args, **kwargs):
|
||||
"""Create a Message stanza associated with this stream."""
|
||||
msg = Message(self, *args, **kwargs)
|
||||
msg['lang'] = self.default_lang
|
||||
return msg
|
||||
|
||||
def Iq(self, *args, **kwargs):
|
||||
"""Create an Iq stanza associated with this stream."""
|
||||
return Iq(self, *args, **kwargs)
|
||||
|
||||
def Presence(self, *args, **kwargs):
|
||||
"""Create a Presence stanza associated with this stream."""
|
||||
pres = Presence(self, *args, **kwargs)
|
||||
pres['lang'] = self.default_lang
|
||||
return pres
|
||||
|
||||
def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None):
|
||||
"""Create a new Iq stanza with a given Id and from JID.
|
||||
|
||||
:param id: An ideally unique ID value for this stanza thread.
|
||||
Defaults to 0.
|
||||
:param ifrom: The from :class:`~slixmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param itype: The :class:`~slixmpp.stanza.iq.Iq`'s type,
|
||||
one of: ``'get'``, ``'set'``, ``'result'``,
|
||||
or ``'error'``.
|
||||
:param iquery: Optional namespace for adding a query element.
|
||||
"""
|
||||
iq = self.Iq()
|
||||
iq['id'] = str(id)
|
||||
iq['to'] = ito
|
||||
iq['from'] = ifrom
|
||||
iq['type'] = itype
|
||||
iq['query'] = iquery
|
||||
return iq
|
||||
|
||||
def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None):
|
||||
"""Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'get'``.
|
||||
|
||||
Optionally, a query element may be added.
|
||||
|
||||
:param queryxmlns: The namespace of the query to use.
|
||||
:param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['query'] = queryxmlns
|
||||
if ito:
|
||||
iq['to'] = ito
|
||||
if ifrom:
|
||||
iq['from'] = ifrom
|
||||
return iq
|
||||
|
||||
def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None):
|
||||
"""
|
||||
Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type
|
||||
``'result'`` with the given ID value.
|
||||
|
||||
:param id: An ideally unique ID value. May use :meth:`new_id()`.
|
||||
:param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
if id is None:
|
||||
id = self.new_id()
|
||||
iq['id'] = id
|
||||
iq['type'] = 'result'
|
||||
if ito:
|
||||
iq['to'] = ito
|
||||
if ifrom:
|
||||
iq['from'] = ifrom
|
||||
return iq
|
||||
|
||||
def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None):
|
||||
"""
|
||||
Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'set'``.
|
||||
|
||||
Optionally, a substanza may be given to use as the
|
||||
stanza's payload.
|
||||
|
||||
:param sub: Either an
|
||||
:class:`~slixmpp.xmlstream.stanzabase.ElementBase`
|
||||
stanza object or an
|
||||
:class:`~xml.etree.ElementTree.Element` XML object
|
||||
to use as the :class:`~slixmpp.stanza.iq.Iq`'s payload.
|
||||
:param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
iq['type'] = 'set'
|
||||
if sub != None:
|
||||
iq.append(sub)
|
||||
if ito:
|
||||
iq['to'] = ito
|
||||
if ifrom:
|
||||
iq['from'] = ifrom
|
||||
return iq
|
||||
|
||||
def make_iq_error(self, id, type='cancel',
|
||||
condition='feature-not-implemented',
|
||||
text=None, ito=None, ifrom=None, iq=None):
|
||||
"""
|
||||
Create an :class:`~slixmpp.stanza.iq.Iq` stanza of type ``'error'``.
|
||||
|
||||
:param id: An ideally unique ID value. May use :meth:`new_id()`.
|
||||
:param type: The type of the error, such as ``'cancel'`` or
|
||||
``'modify'``. Defaults to ``'cancel'``.
|
||||
:param condition: The error condition. Defaults to
|
||||
``'feature-not-implemented'``.
|
||||
:param text: A message describing the cause of the error.
|
||||
:param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
iq['id'] = id
|
||||
iq['error']['type'] = type
|
||||
iq['error']['condition'] = condition
|
||||
iq['error']['text'] = text
|
||||
if ito:
|
||||
iq['to'] = ito
|
||||
if ifrom:
|
||||
iq['from'] = ifrom
|
||||
return iq
|
||||
|
||||
def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None):
|
||||
"""
|
||||
Create or modify an :class:`~slixmpp.stanza.iq.Iq` stanza
|
||||
to use the given query namespace.
|
||||
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
:param xmlns: The query's namespace.
|
||||
:param ito: The destination :class:`~slixmpp.xmlstream.jid.JID`
|
||||
for this stanza.
|
||||
:param ifrom: The ``'from'`` :class:`~slixmpp.xmlstream.jid.JID`
|
||||
to use for this stanza.
|
||||
"""
|
||||
if not iq:
|
||||
iq = self.Iq()
|
||||
iq['query'] = xmlns
|
||||
if ito:
|
||||
iq['to'] = ito
|
||||
if ifrom:
|
||||
iq['from'] = ifrom
|
||||
return iq
|
||||
|
||||
def make_query_roster(self, iq=None):
|
||||
"""Create a roster query element.
|
||||
|
||||
:param iq: Optionally use an existing stanza instead
|
||||
of generating a new one.
|
||||
"""
|
||||
if iq:
|
||||
iq['query'] = 'jabber:iq:roster'
|
||||
return ET.Element("{jabber:iq:roster}query")
|
||||
|
||||
def make_message(self, mto, mbody=None, msubject=None, mtype=None,
|
||||
mhtml=None, mfrom=None, mnick=None):
|
||||
"""
|
||||
Create and initialize a new
|
||||
:class:`~slixmpp.stanza.message.Message` stanza.
|
||||
|
||||
:param mto: The recipient of the message.
|
||||
:param mbody: The main contents of the message.
|
||||
:param msubject: Optional subject for the message.
|
||||
:param mtype: The message's type, such as ``'chat'`` or
|
||||
``'groupchat'``.
|
||||
:param mhtml: Optional HTML body content in the form of a string.
|
||||
:param mfrom: The sender of the message. if sending from a client,
|
||||
be aware that some servers require that the full JID
|
||||
of the sender be used.
|
||||
:param mnick: Optional nickname of the sender.
|
||||
"""
|
||||
message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
|
||||
message['body'] = mbody
|
||||
message['subject'] = msubject
|
||||
if mnick is not None:
|
||||
message['nick'] = mnick
|
||||
if mhtml is not None:
|
||||
message['html']['body'] = mhtml
|
||||
return message
|
||||
|
||||
def make_presence(self, pshow=None, pstatus=None, ppriority=None,
|
||||
pto=None, ptype=None, pfrom=None, pnick=None):
|
||||
"""
|
||||
Create and initialize a new
|
||||
:class:`~slixmpp.stanza.presence.Presence` stanza.
|
||||
|
||||
:param pshow: The presence's show value.
|
||||
:param pstatus: The presence's status message.
|
||||
:param ppriority: This connection's priority.
|
||||
:param pto: The recipient of a directed presence.
|
||||
:param ptype: The type of presence, such as ``'subscribe'``.
|
||||
:param pfrom: The sender of the presence.
|
||||
:param pnick: Optional nickname of the presence's sender.
|
||||
"""
|
||||
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
|
||||
if pshow is not None:
|
||||
presence['type'] = pshow
|
||||
if pfrom is None and self.is_component:
|
||||
presence['from'] = self.boundjid.full
|
||||
presence['priority'] = ppriority
|
||||
presence['status'] = pstatus
|
||||
presence['nick'] = pnick
|
||||
return presence
|
||||
|
||||
def send_message(self, mto, mbody, msubject=None, mtype=None,
|
||||
mhtml=None, mfrom=None, mnick=None):
|
||||
"""
|
||||
Create, initialize, and send a new
|
||||
:class:`~slixmpp.stanza.message.Message` stanza.
|
||||
|
||||
:param mto: The recipient of the message.
|
||||
:param mbody: The main contents of the message.
|
||||
:param msubject: Optional subject for the message.
|
||||
:param mtype: The message's type, such as ``'chat'`` or
|
||||
``'groupchat'``.
|
||||
:param mhtml: Optional HTML body content in the form of a string.
|
||||
:param mfrom: The sender of the message. if sending from a client,
|
||||
be aware that some servers require that the full JID
|
||||
of the sender be used.
|
||||
:param mnick: Optional nickname of the sender.
|
||||
"""
|
||||
self.make_message(mto, mbody, msubject, mtype,
|
||||
mhtml, mfrom, mnick).send()
|
||||
|
||||
def send_presence(self, pshow=None, pstatus=None, ppriority=None,
|
||||
pto=None, pfrom=None, ptype=None, pnick=None):
|
||||
"""
|
||||
Create, initialize, and send a new
|
||||
:class:`~slixmpp.stanza.presence.Presence` stanza.
|
||||
|
||||
:param pshow: The presence's show value.
|
||||
:param pstatus: The presence's status message.
|
||||
:param ppriority: This connection's priority.
|
||||
:param pto: The recipient of a directed presence.
|
||||
:param ptype: The type of presence, such as ``'subscribe'``.
|
||||
:param pfrom: The sender of the presence.
|
||||
:param pnick: Optional nickname of the presence's sender.
|
||||
"""
|
||||
self.make_presence(pshow, pstatus, ppriority, pto,
|
||||
ptype, pfrom, pnick).send()
|
||||
|
||||
def send_presence_subscription(self, pto, pfrom=None,
|
||||
ptype='subscribe', pnick=None):
|
||||
"""
|
||||
Create, initialize, and send a new
|
||||
:class:`~slixmpp.stanza.presence.Presence` stanza of
|
||||
type ``'subscribe'``.
|
||||
|
||||
:param pto: The recipient of a directed presence.
|
||||
:param pfrom: The sender of the presence.
|
||||
:param ptype: The type of presence, such as ``'subscribe'``.
|
||||
:param pnick: Optional nickname of the presence's sender.
|
||||
"""
|
||||
self.make_presence(ptype=ptype,
|
||||
pfrom=pfrom,
|
||||
pto=JID(pto).bare,
|
||||
pnick=pnick).send()
|
||||
|
||||
@property
|
||||
def jid(self):
|
||||
"""Attribute accessor for bare jid"""
|
||||
log.warning("jid property deprecated. Use boundjid.bare")
|
||||
return self.boundjid.bare
|
||||
|
||||
@jid.setter
|
||||
def jid(self, value):
|
||||
log.warning("jid property deprecated. Use boundjid.bare")
|
||||
self.boundjid.bare = value
|
||||
|
||||
@property
|
||||
def fulljid(self):
|
||||
"""Attribute accessor for full jid"""
|
||||
log.warning("fulljid property deprecated. Use boundjid.full")
|
||||
return self.boundjid.full
|
||||
|
||||
@fulljid.setter
|
||||
def fulljid(self, value):
|
||||
log.warning("fulljid property deprecated. Use boundjid.full")
|
||||
self.boundjid.full = value
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
"""Attribute accessor for jid resource"""
|
||||
log.warning("resource property deprecated. Use boundjid.resource")
|
||||
return self.boundjid.resource
|
||||
|
||||
@resource.setter
|
||||
def resource(self, value):
|
||||
log.warning("fulljid property deprecated. Use boundjid.resource")
|
||||
self.boundjid.resource = value
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
"""Attribute accessor for jid usernode"""
|
||||
log.warning("username property deprecated. Use boundjid.user")
|
||||
return self.boundjid.user
|
||||
|
||||
@username.setter
|
||||
def username(self, value):
|
||||
log.warning("username property deprecated. Use boundjid.user")
|
||||
self.boundjid.user = value
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
"""Attribute accessor for jid host"""
|
||||
log.warning("server property deprecated. Use boundjid.host")
|
||||
return self.boundjid.server
|
||||
|
||||
@server.setter
|
||||
def server(self, value):
|
||||
log.warning("server property deprecated. Use boundjid.host")
|
||||
self.boundjid.server = value
|
||||
|
||||
@property
|
||||
def auto_authorize(self):
|
||||
"""Auto accept or deny subscription requests.
|
||||
|
||||
If ``True``, auto accept subscription requests.
|
||||
If ``False``, auto deny subscription requests.
|
||||
If ``None``, don't automatically respond.
|
||||
"""
|
||||
return self.roster.auto_authorize
|
||||
|
||||
@auto_authorize.setter
|
||||
def auto_authorize(self, value):
|
||||
self.roster.auto_authorize = value
|
||||
|
||||
@property
|
||||
def auto_subscribe(self):
|
||||
"""Auto send requests for mutual subscriptions.
|
||||
|
||||
If ``True``, auto send mutual subscription requests.
|
||||
"""
|
||||
return self.roster.auto_subscribe
|
||||
|
||||
@auto_subscribe.setter
|
||||
def auto_subscribe(self, value):
|
||||
self.roster.auto_subscribe = value
|
||||
|
||||
def set_jid(self, jid):
|
||||
"""Rip a JID apart and claim it as our own."""
|
||||
log.debug("setting jid to %s", jid)
|
||||
self.boundjid = JID(jid, cache_lock=True)
|
||||
|
||||
def getjidresource(self, fulljid):
|
||||
if '/' in fulljid:
|
||||
return fulljid.split('/', 1)[-1]
|
||||
else:
|
||||
return ''
|
||||
|
||||
def getjidbare(self, fulljid):
|
||||
return fulljid.split('/', 1)[0]
|
||||
|
||||
def _handle_session_start(self, event):
|
||||
"""Reset redirection attempt count."""
|
||||
self._redirect_attempts = 0
|
||||
|
||||
def _handle_disconnected(self, event):
|
||||
"""When disconnected, reset the roster"""
|
||||
self.roster.reset()
|
||||
self.session_bind_event.clear()
|
||||
|
||||
def _handle_stream_error(self, error):
|
||||
self.event('stream_error', error)
|
||||
|
||||
if error['condition'] == 'see-other-host':
|
||||
other_host = error['see_other_host']
|
||||
if not other_host:
|
||||
log.warning("No other host specified.")
|
||||
return
|
||||
|
||||
if self._redirect_attempts > self.max_redirects:
|
||||
log.error("Exceeded maximum number of redirection attempts.")
|
||||
return
|
||||
|
||||
self._redirect_attempts += 1
|
||||
|
||||
host = other_host
|
||||
port = 5222
|
||||
|
||||
if '[' in other_host and ']' in other_host:
|
||||
host = other_host.split(']')[0][1:]
|
||||
elif ':' in other_host:
|
||||
host = other_host.split(':')[0]
|
||||
|
||||
port_sec = other_host.split(']')[-1]
|
||||
if ':' in port_sec:
|
||||
port = int(port_sec.split(':')[1])
|
||||
|
||||
self.address = (host, port)
|
||||
self.default_domain = host
|
||||
self.dns_records = None
|
||||
self.reconnect_delay = None
|
||||
self.reconnect()
|
||||
|
||||
def _handle_message(self, msg):
|
||||
"""Process incoming message stanzas."""
|
||||
if not self.is_component and not msg['to'].bare:
|
||||
msg['to'] = self.boundjid
|
||||
self.event('message', msg)
|
||||
|
||||
def _handle_available(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_available(pres)
|
||||
|
||||
def _handle_unavailable(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_unavailable(pres)
|
||||
|
||||
def _handle_new_subscription(self, pres):
|
||||
"""Attempt to automatically handle subscription requests.
|
||||
|
||||
Subscriptions will be approved if the request is from
|
||||
a whitelisted JID, of :attr:`auto_authorize` is True. They
|
||||
will be rejected if :attr:`auto_authorize` is False. Setting
|
||||
:attr:`auto_authorize` to ``None`` will disable automatic
|
||||
subscription handling (except for whitelisted JIDs).
|
||||
|
||||
If a subscription is accepted, a request for a mutual
|
||||
subscription will be sent if :attr:`auto_subscribe` is ``True``.
|
||||
"""
|
||||
roster = self.roster[pres['to']]
|
||||
item = self.roster[pres['to']][pres['from']]
|
||||
if item['whitelisted']:
|
||||
item.authorize()
|
||||
if roster.auto_subscribe:
|
||||
item.subscribe()
|
||||
elif roster.auto_authorize:
|
||||
item.authorize()
|
||||
if roster.auto_subscribe:
|
||||
item.subscribe()
|
||||
elif roster.auto_authorize == False:
|
||||
item.unauthorize()
|
||||
|
||||
def _handle_removed_subscription(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_unauthorize(pres)
|
||||
|
||||
def _handle_subscribe(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_subscribe(pres)
|
||||
|
||||
def _handle_subscribed(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_subscribed(pres)
|
||||
|
||||
def _handle_unsubscribe(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_unsubscribe(pres)
|
||||
|
||||
def _handle_unsubscribed(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_unsubscribed(pres)
|
||||
|
||||
def _handle_presence(self, presence):
|
||||
"""Process incoming presence stanzas.
|
||||
|
||||
Update the roster with presence information.
|
||||
"""
|
||||
if not self.is_component and not presence['to'].bare:
|
||||
presence['to'] = self.boundjid
|
||||
|
||||
self.event('presence', presence)
|
||||
self.event('presence_%s' % presence['type'], presence)
|
||||
|
||||
# Check for changes in subscription state.
|
||||
if presence['type'] in ('subscribe', 'subscribed',
|
||||
'unsubscribe', 'unsubscribed'):
|
||||
self.event('changed_subscription', presence)
|
||||
return
|
||||
elif not presence['type'] in ('available', 'unavailable') and \
|
||||
not presence['type'] in presence.showtypes:
|
||||
return
|
||||
|
||||
def exception(self, exception):
|
||||
"""Process any uncaught exceptions, notably
|
||||
:class:`~slixmpp.exceptions.IqError` and
|
||||
:class:`~slixmpp.exceptions.IqTimeout` exceptions.
|
||||
|
||||
:param exception: An unhandled :class:`Exception` object.
|
||||
"""
|
||||
if isinstance(exception, IqError):
|
||||
iq = exception.iq
|
||||
log.error('%s: %s', iq['error']['condition'],
|
||||
iq['error']['text'])
|
||||
log.warning('You should catch IqError exceptions')
|
||||
elif isinstance(exception, IqTimeout):
|
||||
iq = exception.iq
|
||||
log.error('Request timed out: %s', iq)
|
||||
log.warning('You should catch IqTimeout exceptions')
|
||||
elif isinstance(exception, SyntaxError):
|
||||
# Hide stream parsing errors that occur when the
|
||||
# stream is disconnected (they've been handled, we
|
||||
# don't need to make a mess in the logs).
|
||||
pass
|
||||
else:
|
||||
log.exception(exception)
|
||||
|
||||
|
||||
# Restore the old, lowercased name for backwards compatibility.
|
||||
basexmpp = BaseXMPP
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
BaseXMPP.registerPlugin = BaseXMPP.register_plugin
|
||||
BaseXMPP.makeIq = BaseXMPP.make_iq
|
||||
BaseXMPP.makeIqGet = BaseXMPP.make_iq_get
|
||||
BaseXMPP.makeIqResult = BaseXMPP.make_iq_result
|
||||
BaseXMPP.makeIqSet = BaseXMPP.make_iq_set
|
||||
BaseXMPP.makeIqError = BaseXMPP.make_iq_error
|
||||
BaseXMPP.makeIqQuery = BaseXMPP.make_iq_query
|
||||
BaseXMPP.makeQueryRoster = BaseXMPP.make_query_roster
|
||||
BaseXMPP.makeMessage = BaseXMPP.make_message
|
||||
BaseXMPP.makePresence = BaseXMPP.make_presence
|
||||
BaseXMPP.sendMessage = BaseXMPP.send_message
|
||||
BaseXMPP.sendPresence = BaseXMPP.send_presence
|
||||
BaseXMPP.sendPresenceSubscription = BaseXMPP.send_presence_subscription
|
||||
333
slixmpp/clientxmpp.py
Normal file
333
slixmpp/clientxmpp.py
Normal file
@@ -0,0 +1,333 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
slixmpp.clientxmpp
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides XMPP functionality that
|
||||
is specific to client connections.
|
||||
|
||||
Part of Slixmpp: The Slick XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from slixmpp.stanza import StreamFeatures
|
||||
from slixmpp.basexmpp import BaseXMPP
|
||||
from slixmpp.exceptions import XMPPError
|
||||
from slixmpp.xmlstream import XMLStream
|
||||
from slixmpp.xmlstream.matcher import StanzaPath, MatchXPath
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
|
||||
# Flag indicating if DNS SRV records are available for use.
|
||||
try:
|
||||
import dns.resolver
|
||||
except ImportError:
|
||||
DNSPYTHON = False
|
||||
else:
|
||||
DNSPYTHON = True
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClientXMPP(BaseXMPP):
|
||||
|
||||
"""
|
||||
Slixmpp's client class. (Use only for good, not for evil.)
|
||||
|
||||
Typical use pattern:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
xmpp = ClientXMPP('user@server.tld/resource', 'password')
|
||||
# ... Register plugins and event handlers ...
|
||||
xmpp.connect()
|
||||
xmpp.process(block=False) # block=True will block the current
|
||||
# thread. By default, block=False
|
||||
|
||||
:param jid: The JID of the XMPP user account.
|
||||
:param password: The password for the XMPP user account.
|
||||
:param ssl: **Deprecated.**
|
||||
:param plugin_config: A dictionary of plugin configurations.
|
||||
:param plugin_whitelist: A list of approved plugins that
|
||||
will be loaded when calling
|
||||
:meth:`~slixmpp.basexmpp.BaseXMPP.register_plugins()`.
|
||||
:param escape_quotes: **Deprecated.**
|
||||
"""
|
||||
|
||||
def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[],
|
||||
escape_quotes=True, sasl_mech=None, lang='en'):
|
||||
BaseXMPP.__init__(self, jid, 'jabber:client')
|
||||
|
||||
self.escape_quotes = escape_quotes
|
||||
self.plugin_config = plugin_config
|
||||
self.plugin_whitelist = plugin_whitelist
|
||||
self.default_port = 5222
|
||||
self.default_lang = lang
|
||||
|
||||
self.credentials = {}
|
||||
|
||||
self.password = password
|
||||
|
||||
self.stream_header = "<stream:stream to='%s' %s %s %s %s>" % (
|
||||
self.boundjid.host,
|
||||
"xmlns:stream='%s'" % self.stream_ns,
|
||||
"xmlns='%s'" % self.default_ns,
|
||||
"xml:lang='%s'" % self.default_lang,
|
||||
"version='1.0'")
|
||||
self.stream_footer = "</stream:stream>"
|
||||
|
||||
self.features = set()
|
||||
self._stream_feature_handlers = {}
|
||||
self._stream_feature_order = []
|
||||
|
||||
self.dns_service = 'xmpp-client'
|
||||
|
||||
#TODO: Use stream state here
|
||||
self.authenticated = False
|
||||
self.sessionstarted = False
|
||||
self.bound = False
|
||||
self.bindfail = False
|
||||
|
||||
self.add_event_handler('connected', self._reset_connection_state)
|
||||
self.add_event_handler('session_bind', self._handle_session_bind)
|
||||
self.add_event_handler('roster_update', self._handle_roster)
|
||||
|
||||
self.register_stanza(StreamFeatures)
|
||||
|
||||
self.register_handler(
|
||||
Callback('Stream Features',
|
||||
MatchXPath('{%s}features' % self.stream_ns),
|
||||
self._handle_stream_features))
|
||||
self.register_handler(
|
||||
Callback('Roster Update',
|
||||
StanzaPath('iq@type=set/roster'),
|
||||
lambda iq: self.event('roster_update', iq)))
|
||||
|
||||
# Setup default stream features
|
||||
self.register_plugin('feature_starttls')
|
||||
self.register_plugin('feature_bind')
|
||||
self.register_plugin('feature_session')
|
||||
self.register_plugin('feature_rosterver')
|
||||
self.register_plugin('feature_preapproval')
|
||||
self.register_plugin('feature_mechanisms')
|
||||
|
||||
if sasl_mech:
|
||||
self['feature_mechanisms'].use_mech = sasl_mech
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self.credentials.get('password', '')
|
||||
|
||||
@password.setter
|
||||
def password(self, value):
|
||||
self.credentials['password'] = value
|
||||
|
||||
def connect(self, address=tuple(), reattempt=True,
|
||||
use_tls=True, use_ssl=False):
|
||||
"""Connect to the XMPP server.
|
||||
|
||||
When no address is given, a SRV lookup for the server will
|
||||
be attempted. If that fails, the server user in the JID
|
||||
will be used.
|
||||
|
||||
:param address: A tuple containing the server's host and port.
|
||||
:param reattempt: If ``True``, repeat attempting to connect if an
|
||||
error occurs. Defaults to ``True``.
|
||||
:param use_tls: Indicates if TLS should be used for the
|
||||
connection. Defaults to ``True``.
|
||||
:param use_ssl: Indicates if the older SSL connection method
|
||||
should be used. Defaults to ``False``.
|
||||
"""
|
||||
self.session_started_event.clear()
|
||||
|
||||
# If an address was provided, disable using DNS SRV lookup;
|
||||
# otherwise, use the domain from the client JID with the standard
|
||||
# XMPP client port and allow SRV lookup.
|
||||
if address:
|
||||
self.dns_service = None
|
||||
else:
|
||||
address = (self.boundjid.host, 5222)
|
||||
self.dns_service = 'xmpp-client'
|
||||
|
||||
return XMLStream.connect(self, address[0], address[1],
|
||||
use_tls=use_tls, use_ssl=use_ssl,
|
||||
reattempt=reattempt)
|
||||
|
||||
def register_feature(self, name, handler, restart=False, order=5000):
|
||||
"""Register a stream feature handler.
|
||||
|
||||
:param name: The name of the stream feature.
|
||||
:param handler: The function to execute if the feature is received.
|
||||
:param restart: Indicates if feature processing should halt with
|
||||
this feature. Defaults to ``False``.
|
||||
:param order: The relative ordering in which the feature should
|
||||
be negotiated. Lower values will be attempted
|
||||
earlier when available.
|
||||
"""
|
||||
self._stream_feature_handlers[name] = (handler, restart)
|
||||
self._stream_feature_order.append((order, name))
|
||||
self._stream_feature_order.sort()
|
||||
|
||||
def unregister_feature(self, name, order):
|
||||
if name in self._stream_feature_handlers:
|
||||
del self._stream_feature_handlers[name]
|
||||
self._stream_feature_order.remove((order, name))
|
||||
self._stream_feature_order.sort()
|
||||
|
||||
def update_roster(self, jid, **kwargs):
|
||||
"""Add or change a roster item.
|
||||
|
||||
:param jid: The JID of the entry to modify.
|
||||
:param name: The user's nickname for this JID.
|
||||
:param subscription: The subscription status. May be one of
|
||||
``'to'``, ``'from'``, ``'both'``, or
|
||||
``'none'``. If set to ``'remove'``,
|
||||
the entry will be deleted.
|
||||
:param groups: The roster groups that contain this item.
|
||||
:param block: Specify if the roster request will block
|
||||
until a response is received, or a timeout
|
||||
occurs. Defaults to ``True``.
|
||||
:param timeout: The length of time (in seconds) to wait
|
||||
for a response before continuing if blocking
|
||||
is used. Defaults to
|
||||
:attr:`~slixmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
|
||||
:param callback: Optional reference to a stream handler function.
|
||||
Will be executed when the roster is received.
|
||||
Implies ``block=False``.
|
||||
"""
|
||||
current = self.client_roster[jid]
|
||||
|
||||
name = kwargs.get('name', current['name'])
|
||||
subscription = kwargs.get('subscription', current['subscription'])
|
||||
groups = kwargs.get('groups', current['groups'])
|
||||
|
||||
block = kwargs.get('block', True)
|
||||
timeout = kwargs.get('timeout', None)
|
||||
callback = kwargs.get('callback', None)
|
||||
|
||||
return self.client_roster.update(jid, name, subscription, groups,
|
||||
block, timeout, callback)
|
||||
|
||||
def del_roster_item(self, jid):
|
||||
"""Remove an item from the roster.
|
||||
|
||||
This is done by setting its subscription status to ``'remove'``.
|
||||
|
||||
:param jid: The JID of the item to remove.
|
||||
"""
|
||||
return self.client_roster.remove(jid)
|
||||
|
||||
def get_roster(self, block=True, timeout=None, callback=None):
|
||||
"""Request the roster from the server.
|
||||
|
||||
:param block: Specify if the roster request will block until a
|
||||
response is received, or a timeout occurs.
|
||||
Defaults to ``True``.
|
||||
:param timeout: The length of time (in seconds) to wait for a response
|
||||
before continuing if blocking is used.
|
||||
Defaults to
|
||||
:attr:`~slixmpp.xmlstream.xmlstream.XMLStream.response_timeout`.
|
||||
:param callback: Optional reference to a stream handler function. Will
|
||||
be executed when the roster is received.
|
||||
Implies ``block=False``.
|
||||
"""
|
||||
iq = self.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq.enable('roster')
|
||||
if 'rosterver' in self.features:
|
||||
iq['roster']['ver'] = self.client_roster.version
|
||||
|
||||
|
||||
if not block or callback is not None:
|
||||
block = False
|
||||
if callback is None:
|
||||
callback = lambda resp: self.event('roster_update', resp)
|
||||
else:
|
||||
orig_cb = callback
|
||||
def wrapped(resp):
|
||||
self.event('roster_update', resp)
|
||||
orig_cb(resp)
|
||||
callback = wrapped
|
||||
|
||||
response = iq.send(block, timeout, callback)
|
||||
|
||||
if block:
|
||||
self.event('roster_update', response)
|
||||
return response
|
||||
|
||||
def _reset_connection_state(self, event=None):
|
||||
#TODO: Use stream state here
|
||||
self.authenticated = False
|
||||
self.sessionstarted = False
|
||||
self.bound = False
|
||||
self.bindfail = False
|
||||
self.features = set()
|
||||
|
||||
def _handle_stream_features(self, features):
|
||||
"""Process the received stream features.
|
||||
|
||||
:param features: The features stanza.
|
||||
"""
|
||||
for order, name in self._stream_feature_order:
|
||||
if name in features['features']:
|
||||
handler, restart = self._stream_feature_handlers[name]
|
||||
if handler(features) and restart:
|
||||
# Don't continue if the feature requires
|
||||
# restarting the XML stream.
|
||||
return True
|
||||
log.debug('Finished processing stream features.')
|
||||
self.event('stream_negotiated')
|
||||
|
||||
def _handle_roster(self, iq):
|
||||
"""Update the roster after receiving a roster stanza.
|
||||
|
||||
:param iq: The roster stanza.
|
||||
"""
|
||||
if iq['type'] == 'set':
|
||||
if iq['from'].bare and iq['from'].bare != self.boundjid.bare:
|
||||
raise XMPPError(condition='service-unavailable')
|
||||
|
||||
roster = self.client_roster
|
||||
if iq['roster']['ver']:
|
||||
roster.version = iq['roster']['ver']
|
||||
items = iq['roster']['items']
|
||||
|
||||
valid_subscriptions = ('to', 'from', 'both', 'none', 'remove')
|
||||
for jid, item in items.items():
|
||||
if item['subscription'] in valid_subscriptions:
|
||||
roster[jid]['name'] = item['name']
|
||||
roster[jid]['groups'] = item['groups']
|
||||
roster[jid]['from'] = item['subscription'] in ('from', 'both')
|
||||
roster[jid]['to'] = item['subscription'] in ('to', 'both')
|
||||
roster[jid]['pending_out'] = (item['ask'] == 'subscribe')
|
||||
|
||||
roster[jid].save(remove=(item['subscription'] == 'remove'))
|
||||
|
||||
if iq['type'] == 'set':
|
||||
resp = self.Iq(stype='result',
|
||||
sto=iq['from'],
|
||||
sid=iq['id'])
|
||||
resp.enable('roster')
|
||||
resp.send()
|
||||
|
||||
def _handle_session_bind(self, jid):
|
||||
"""Set the client roster to the JID set by the server.
|
||||
|
||||
:param :class:`slixmpp.xmlstream.jid.JID` jid: The bound JID as
|
||||
dictated by the server. The same as :attr:`boundjid`.
|
||||
"""
|
||||
self.client_roster = self.roster[jid]
|
||||
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
ClientXMPP.updateRoster = ClientXMPP.update_roster
|
||||
ClientXMPP.delRosterItem = ClientXMPP.del_roster_item
|
||||
ClientXMPP.getRoster = ClientXMPP.get_roster
|
||||
ClientXMPP.registerFeature = ClientXMPP.register_feature
|
||||
159
slixmpp/componentxmpp.py
Normal file
159
slixmpp/componentxmpp.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
slixmpp.clientxmpp
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides XMPP functionality that
|
||||
is specific to external server component connections.
|
||||
|
||||
Part of Slixmpp: The Slick XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import hashlib
|
||||
|
||||
from slixmpp.basexmpp import BaseXMPP
|
||||
from slixmpp.xmlstream import XMLStream
|
||||
from slixmpp.xmlstream import ET
|
||||
from slixmpp.xmlstream.matcher import MatchXPath
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ComponentXMPP(BaseXMPP):
|
||||
|
||||
"""
|
||||
Slixmpp's basic XMPP server component.
|
||||
|
||||
Use only for good, not for evil.
|
||||
|
||||
:param jid: The JID of the component.
|
||||
:param secret: The secret or password for the component.
|
||||
:param host: The server accepting the component.
|
||||
:param port: The port used to connect to the server.
|
||||
:param plugin_config: A dictionary of plugin configurations.
|
||||
:param plugin_whitelist: A list of approved plugins that
|
||||
will be loaded when calling
|
||||
:meth:`~slixmpp.basexmpp.BaseXMPP.register_plugins()`.
|
||||
:param use_jc_ns: Indicates if the ``'jabber:client'`` namespace
|
||||
should be used instead of the standard
|
||||
``'jabber:component:accept'`` namespace.
|
||||
Defaults to ``False``.
|
||||
"""
|
||||
|
||||
def __init__(self, jid, secret, host=None, port=None,
|
||||
plugin_config={}, plugin_whitelist=[], use_jc_ns=False):
|
||||
if use_jc_ns:
|
||||
default_ns = 'jabber:client'
|
||||
else:
|
||||
default_ns = 'jabber:component:accept'
|
||||
BaseXMPP.__init__(self, jid, default_ns)
|
||||
|
||||
self.auto_authorize = None
|
||||
self.stream_header = "<stream:stream %s %s to='%s'>" % (
|
||||
'xmlns="jabber:component:accept"',
|
||||
'xmlns:stream="%s"' % self.stream_ns,
|
||||
jid)
|
||||
self.stream_footer = "</stream:stream>"
|
||||
self.server_host = host
|
||||
self.server_port = port
|
||||
self.secret = secret
|
||||
|
||||
self.plugin_config = plugin_config
|
||||
self.plugin_whitelist = plugin_whitelist
|
||||
self.is_component = True
|
||||
|
||||
self.register_handler(
|
||||
Callback('Handshake',
|
||||
MatchXPath('{jabber:component:accept}handshake'),
|
||||
self._handle_handshake))
|
||||
self.add_event_handler('presence_probe',
|
||||
self._handle_probe)
|
||||
|
||||
def connect(self, host=None, port=None, use_ssl=False,
|
||||
use_tls=False, reattempt=True):
|
||||
"""Connect to the server.
|
||||
|
||||
Setting ``reattempt`` to ``True`` will cause connection attempts to
|
||||
be made every second until a successful connection is established.
|
||||
|
||||
:param host: The name of the desired server for the connection.
|
||||
Defaults to :attr:`server_host`.
|
||||
:param port: Port to connect to on the server.
|
||||
Defauts to :attr:`server_port`.
|
||||
:param use_ssl: Flag indicating if SSL should be used by connecting
|
||||
directly to a port using SSL.
|
||||
:param use_tls: Flag indicating if TLS should be used, allowing for
|
||||
connecting to a port without using SSL immediately and
|
||||
later upgrading the connection.
|
||||
:param reattempt: Flag indicating if the socket should reconnect
|
||||
after disconnections.
|
||||
"""
|
||||
if host is None:
|
||||
host = self.server_host
|
||||
if port is None:
|
||||
port = self.server_port
|
||||
|
||||
self.server_name = self.boundjid.host
|
||||
|
||||
if use_tls:
|
||||
log.info("XEP-0114 components can not use TLS")
|
||||
|
||||
log.debug("Connecting to %s:%s", host, port)
|
||||
return XMLStream.connect(self, host=host, port=port,
|
||||
use_ssl=use_ssl,
|
||||
use_tls=False,
|
||||
reattempt=reattempt)
|
||||
|
||||
def incoming_filter(self, xml):
|
||||
"""
|
||||
Pre-process incoming XML stanzas by converting any
|
||||
``'jabber:client'`` namespaced elements to the component's
|
||||
default namespace.
|
||||
|
||||
:param xml: The XML stanza to pre-process.
|
||||
"""
|
||||
if xml.tag.startswith('{jabber:client}'):
|
||||
xml.tag = xml.tag.replace('jabber:client', self.default_ns)
|
||||
return xml
|
||||
|
||||
def start_stream_handler(self, xml):
|
||||
"""
|
||||
Once the streams are established, attempt to handshake
|
||||
with the server to be accepted as a component.
|
||||
|
||||
:param xml: The incoming stream's root element.
|
||||
"""
|
||||
BaseXMPP.start_stream_handler(self, xml)
|
||||
|
||||
# Construct a hash of the stream ID and the component secret.
|
||||
sid = xml.get('id', '')
|
||||
pre_hash = '%s%s' % (sid, self.secret)
|
||||
if sys.version_info >= (3, 0):
|
||||
# Handle Unicode byte encoding in Python 3.
|
||||
pre_hash = bytes(pre_hash, 'utf-8')
|
||||
|
||||
handshake = ET.Element('{jabber:component:accept}handshake')
|
||||
handshake.text = hashlib.sha1(pre_hash).hexdigest().lower()
|
||||
self.send_xml(handshake, now=True)
|
||||
|
||||
def _handle_handshake(self, xml):
|
||||
"""The handshake has been accepted.
|
||||
|
||||
:param xml: The reply handshake stanza.
|
||||
"""
|
||||
self.session_bind_event.set()
|
||||
self.session_started_event.set()
|
||||
self.event('session_bind', self.boundjid, direct=True)
|
||||
self.event('session_start')
|
||||
|
||||
def _handle_probe(self, pres):
|
||||
self.roster[pres['to']][pres['from']].handle_probe(pres)
|
||||
91
slixmpp/exceptions.py
Normal file
91
slixmpp/exceptions.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
slixmpp.exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Part of Slixmpp: The Slick XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
|
||||
class XMPPError(Exception):
|
||||
|
||||
"""
|
||||
A generic exception that may be raised while processing an XMPP stanza
|
||||
to indicate that an error response stanza should be sent.
|
||||
|
||||
The exception method for stanza objects extending
|
||||
:class:`~slixmpp.stanza.rootstanza.RootStanza` will create an error
|
||||
stanza and initialize any additional substanzas using the extension
|
||||
information included in the exception.
|
||||
|
||||
Meant for use in Slixmpp plugins and applications using Slixmpp.
|
||||
|
||||
Extension information can be included to add additional XML elements
|
||||
to the generated error stanza.
|
||||
|
||||
:param condition: The XMPP defined error condition.
|
||||
Defaults to ``'undefined-condition'``.
|
||||
:param text: Human readable text describing the error.
|
||||
:param etype: The XMPP error type, such as ``'cancel'`` or ``'modify'``.
|
||||
Defaults to ``'cancel'``.
|
||||
:param extension: Tag name of the extension's XML content.
|
||||
:param extension_ns: XML namespace of the extensions' XML content.
|
||||
:param extension_args: Content and attributes for the extension
|
||||
element. Same as the additional arguments to
|
||||
the :class:`~xml.etree.ElementTree.Element`
|
||||
constructor.
|
||||
:param clear: Indicates if the stanza's contents should be
|
||||
removed before replying with an error.
|
||||
Defaults to ``True``.
|
||||
"""
|
||||
|
||||
def __init__(self, condition='undefined-condition', text='',
|
||||
etype='cancel', extension=None, extension_ns=None,
|
||||
extension_args=None, clear=True):
|
||||
if extension_args is None:
|
||||
extension_args = {}
|
||||
|
||||
self.condition = condition
|
||||
self.text = text
|
||||
self.etype = etype
|
||||
self.clear = clear
|
||||
self.extension = extension
|
||||
self.extension_ns = extension_ns
|
||||
self.extension_args = extension_args
|
||||
|
||||
|
||||
class IqTimeout(XMPPError):
|
||||
|
||||
"""
|
||||
An exception which indicates that an IQ request response has not been
|
||||
received within the alloted time window.
|
||||
"""
|
||||
|
||||
def __init__(self, iq):
|
||||
super(IqTimeout, self).__init__(
|
||||
condition='remote-server-timeout',
|
||||
etype='cancel')
|
||||
|
||||
#: The :class:`~slixmpp.stanza.iq.Iq` stanza whose response
|
||||
#: did not arrive before the timeout expired.
|
||||
self.iq = iq
|
||||
|
||||
|
||||
class IqError(XMPPError):
|
||||
|
||||
"""
|
||||
An exception raised when an Iq stanza of type 'error' is received
|
||||
after making a blocking send call.
|
||||
"""
|
||||
|
||||
def __init__(self, iq):
|
||||
super(IqError, self).__init__(
|
||||
condition=iq['error']['condition'],
|
||||
text=iq['error']['text'],
|
||||
etype=iq['error']['type'])
|
||||
|
||||
#: The :class:`~slixmpp.stanza.iq.Iq` error result stanza.
|
||||
self.iq = iq
|
||||
16
slixmpp/features/__init__.py
Normal file
16
slixmpp/features/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2011 Nathanael C. Fritz
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
'feature_starttls',
|
||||
'feature_mechanisms',
|
||||
'feature_bind',
|
||||
'feature_session',
|
||||
'feature_rosterver',
|
||||
'feature_preapproval'
|
||||
]
|
||||
19
slixmpp/features/feature_bind/__init__.py
Normal file
19
slixmpp/features/feature_bind/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
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.base import register_plugin
|
||||
|
||||
from slixmpp.features.feature_bind.bind import FeatureBind
|
||||
from slixmpp.features.feature_bind.stanza import Bind
|
||||
|
||||
|
||||
register_plugin(FeatureBind)
|
||||
|
||||
|
||||
# Retain some backwards compatibility
|
||||
feature_bind = FeatureBind
|
||||
65
slixmpp/features/feature_bind/bind.py
Normal file
65
slixmpp/features/feature_bind/bind.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
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.jid import JID
|
||||
from slixmpp.stanza import Iq, StreamFeatures
|
||||
from slixmpp.features.feature_bind import stanza
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins import BasePlugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureBind(BasePlugin):
|
||||
|
||||
name = 'feature_bind'
|
||||
description = 'RFC 6120: Stream Feature: Resource Binding'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
self.xmpp.register_feature('bind',
|
||||
self._handle_bind_resource,
|
||||
restart=False,
|
||||
order=10000)
|
||||
|
||||
register_stanza_plugin(Iq, stanza.Bind)
|
||||
register_stanza_plugin(StreamFeatures, stanza.Bind)
|
||||
|
||||
def _handle_bind_resource(self, features):
|
||||
"""
|
||||
Handle requesting a specific resource.
|
||||
|
||||
Arguments:
|
||||
features -- The stream features stanza.
|
||||
"""
|
||||
log.debug("Requesting resource: %s", self.xmpp.requested_jid.resource)
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq.enable('bind')
|
||||
if self.xmpp.requested_jid.resource:
|
||||
iq['bind']['resource'] = self.xmpp.requested_jid.resource
|
||||
response = iq.send(now=True)
|
||||
|
||||
self.xmpp.boundjid = JID(response['bind']['jid'], cache_lock=True)
|
||||
self.xmpp.bound = True
|
||||
self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True)
|
||||
self.xmpp.session_bind_event.set()
|
||||
|
||||
self.xmpp.features.add('bind')
|
||||
|
||||
log.info("JID set to: %s", self.xmpp.boundjid.full)
|
||||
|
||||
if 'session' not in features['features']:
|
||||
log.debug("Established Session")
|
||||
self.xmpp.sessionstarted = True
|
||||
self.xmpp.session_started_event.set()
|
||||
self.xmpp.event('session_start')
|
||||
21
slixmpp/features/feature_bind/stanza.py
Normal file
21
slixmpp/features/feature_bind/stanza.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class Bind(ElementBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'bind'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-bind'
|
||||
interfaces = set(('resource', 'jid'))
|
||||
sub_interfaces = interfaces
|
||||
plugin_attrib = 'bind'
|
||||
22
slixmpp/features/feature_mechanisms/__init__.py
Normal file
22
slixmpp/features/feature_mechanisms/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
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.base import register_plugin
|
||||
|
||||
from slixmpp.features.feature_mechanisms.mechanisms import FeatureMechanisms
|
||||
from slixmpp.features.feature_mechanisms.stanza import Mechanisms
|
||||
from slixmpp.features.feature_mechanisms.stanza import Auth
|
||||
from slixmpp.features.feature_mechanisms.stanza import Success
|
||||
from slixmpp.features.feature_mechanisms.stanza import Failure
|
||||
|
||||
|
||||
register_plugin(FeatureMechanisms)
|
||||
|
||||
|
||||
# Retain some backwards compatibility
|
||||
feature_mechanisms = FeatureMechanisms
|
||||
244
slixmpp/features/feature_mechanisms/mechanisms.py
Normal file
244
slixmpp/features/feature_mechanisms/mechanisms.py
Normal file
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
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 ssl
|
||||
import logging
|
||||
|
||||
from slixmpp.util import sasl
|
||||
from slixmpp.util.stringprep_profiles import StringPrepError
|
||||
from slixmpp.stanza import StreamFeatures
|
||||
from slixmpp.xmlstream import RestartStream, register_stanza_plugin
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.xmlstream.matcher import MatchXPath
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.features.feature_mechanisms import stanza
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureMechanisms(BasePlugin):
|
||||
|
||||
name = 'feature_mechanisms'
|
||||
description = 'RFC 6120: Stream Feature: SASL'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
default_config = {
|
||||
'use_mech': None,
|
||||
'use_mechs': None,
|
||||
'min_mech': None,
|
||||
'sasl_callback': None,
|
||||
'security_callback': None,
|
||||
'encrypted_plain': True,
|
||||
'unencrypted_plain': False,
|
||||
'unencrypted_digest': False,
|
||||
'unencrypted_cram': False,
|
||||
'unencrypted_scram': True,
|
||||
'order': 100
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
if self.sasl_callback is None:
|
||||
self.sasl_callback = self._default_credentials
|
||||
|
||||
if self.security_callback is None:
|
||||
self.security_callback = self._default_security
|
||||
|
||||
creds = self.sasl_callback(set(['username']), set())
|
||||
if not self.use_mech and not creds['username']:
|
||||
self.use_mech = 'ANONYMOUS'
|
||||
|
||||
self.mech = None
|
||||
self.mech_list = set()
|
||||
self.attempted_mechs = set()
|
||||
|
||||
register_stanza_plugin(StreamFeatures, stanza.Mechanisms)
|
||||
|
||||
self.xmpp.register_stanza(stanza.Success)
|
||||
self.xmpp.register_stanza(stanza.Failure)
|
||||
self.xmpp.register_stanza(stanza.Auth)
|
||||
self.xmpp.register_stanza(stanza.Challenge)
|
||||
self.xmpp.register_stanza(stanza.Response)
|
||||
self.xmpp.register_stanza(stanza.Abort)
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('SASL Success',
|
||||
MatchXPath(stanza.Success.tag_name()),
|
||||
self._handle_success,
|
||||
instream=True))
|
||||
self.xmpp.register_handler(
|
||||
Callback('SASL Failure',
|
||||
MatchXPath(stanza.Failure.tag_name()),
|
||||
self._handle_fail,
|
||||
instream=True))
|
||||
self.xmpp.register_handler(
|
||||
Callback('SASL Challenge',
|
||||
MatchXPath(stanza.Challenge.tag_name()),
|
||||
self._handle_challenge))
|
||||
|
||||
self.xmpp.register_feature('mechanisms',
|
||||
self._handle_sasl_auth,
|
||||
restart=True,
|
||||
order=self.order)
|
||||
|
||||
def _default_credentials(self, required_values, optional_values):
|
||||
creds = self.xmpp.credentials
|
||||
result = {}
|
||||
values = required_values.union(optional_values)
|
||||
for value in values:
|
||||
if value == 'username':
|
||||
result[value] = creds.get('username', self.xmpp.requested_jid.user)
|
||||
elif value == 'email':
|
||||
jid = self.xmpp.requested_jid.bare
|
||||
result[value] = creds.get('email', jid)
|
||||
elif value == 'channel_binding':
|
||||
if hasattr(self.xmpp.socket, 'get_channel_binding'):
|
||||
result[value] = self.xmpp.socket.get_channel_binding()
|
||||
else:
|
||||
log.debug("Channel binding not supported.")
|
||||
log.debug("Use Python 3.3+ for channel binding and " + \
|
||||
"SCRAM-SHA-1-PLUS support")
|
||||
result[value] = None
|
||||
elif value == 'host':
|
||||
result[value] = creds.get('host', self.xmpp.requested_jid.domain)
|
||||
elif value == 'realm':
|
||||
result[value] = creds.get('realm', self.xmpp.requested_jid.domain)
|
||||
elif value == 'service-name':
|
||||
result[value] = creds.get('service-name', self.xmpp._service_name)
|
||||
elif value == 'service':
|
||||
result[value] = creds.get('service', 'xmpp')
|
||||
elif value in creds:
|
||||
result[value] = creds[value]
|
||||
return result
|
||||
|
||||
def _default_security(self, values):
|
||||
result = {}
|
||||
for value in values:
|
||||
if value == 'encrypted':
|
||||
if 'starttls' in self.xmpp.features:
|
||||
result[value] = True
|
||||
elif isinstance(self.xmpp.socket, ssl.SSLSocket):
|
||||
result[value] = True
|
||||
else:
|
||||
result[value] = False
|
||||
else:
|
||||
result[value] = self.config.get(value, False)
|
||||
return result
|
||||
|
||||
def _handle_sasl_auth(self, features):
|
||||
"""
|
||||
Handle authenticating using SASL.
|
||||
|
||||
Arguments:
|
||||
features -- The stream features stanza.
|
||||
"""
|
||||
if 'mechanisms' in self.xmpp.features:
|
||||
# SASL authentication has already succeeded, but the
|
||||
# server has incorrectly offered it again.
|
||||
return False
|
||||
|
||||
enforce_limit = False
|
||||
limited_mechs = self.use_mechs
|
||||
|
||||
if limited_mechs is None:
|
||||
limited_mechs = set()
|
||||
elif limited_mechs and not isinstance(limited_mechs, set):
|
||||
limited_mechs = set(limited_mechs)
|
||||
enforce_limit = True
|
||||
|
||||
if self.use_mech:
|
||||
limited_mechs.add(self.use_mech)
|
||||
enforce_limit = True
|
||||
|
||||
if enforce_limit:
|
||||
self.use_mechs = limited_mechs
|
||||
|
||||
self.mech_list = set(features['mechanisms'])
|
||||
|
||||
return self._send_auth()
|
||||
|
||||
def _send_auth(self):
|
||||
mech_list = self.mech_list - self.attempted_mechs
|
||||
try:
|
||||
self.mech = sasl.choose(mech_list,
|
||||
self.sasl_callback,
|
||||
self.security_callback,
|
||||
limit=self.use_mechs,
|
||||
min_mech=self.min_mech)
|
||||
except sasl.SASLNoAppropriateMechanism:
|
||||
log.error("No appropriate login method.")
|
||||
self.xmpp.event("no_auth", direct=True)
|
||||
self.xmpp.event("failed_auth", direct=True)
|
||||
self.attempted_mechs = set()
|
||||
return self.xmpp.disconnect()
|
||||
except StringPrepError:
|
||||
log.exception("A credential value did not pass SASLprep.")
|
||||
self.xmpp.disconnect()
|
||||
|
||||
resp = stanza.Auth(self.xmpp)
|
||||
resp['mechanism'] = self.mech.name
|
||||
try:
|
||||
resp['value'] = self.mech.process()
|
||||
except sasl.SASLCancelled:
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self._send_auth()
|
||||
except sasl.SASLFailed:
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self._send_auth()
|
||||
except sasl.SASLMutualAuthFailed:
|
||||
log.error("Mutual authentication failed! " + \
|
||||
"A security breach is possible.")
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self.xmpp.disconnect()
|
||||
else:
|
||||
resp.send(now=True)
|
||||
|
||||
return True
|
||||
|
||||
def _handle_challenge(self, stanza):
|
||||
"""SASL challenge received. Process and send response."""
|
||||
resp = self.stanza.Response(self.xmpp)
|
||||
try:
|
||||
resp['value'] = self.mech.process(stanza['value'])
|
||||
except sasl.SASLCancelled:
|
||||
self.stanza.Abort(self.xmpp).send()
|
||||
except sasl.SASLFailed:
|
||||
self.stanza.Abort(self.xmpp).send()
|
||||
except sasl.SASLMutualAuthFailed:
|
||||
log.error("Mutual authentication failed! " + \
|
||||
"A security breach is possible.")
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self.xmpp.disconnect()
|
||||
else:
|
||||
if resp.get_value() == '':
|
||||
resp.del_value()
|
||||
resp.send(now=True)
|
||||
|
||||
def _handle_success(self, stanza):
|
||||
"""SASL authentication succeeded. Restart the stream."""
|
||||
try:
|
||||
final = self.mech.process(stanza['value'])
|
||||
except sasl.SASLMutualAuthFailed:
|
||||
log.error("Mutual authentication failed! " + \
|
||||
"A security breach is possible.")
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
self.xmpp.disconnect()
|
||||
else:
|
||||
self.attempted_mechs = set()
|
||||
self.xmpp.authenticated = True
|
||||
self.xmpp.features.add('mechanisms')
|
||||
self.xmpp.event('auth_success', stanza, direct=True)
|
||||
raise RestartStream()
|
||||
|
||||
def _handle_fail(self, stanza):
|
||||
"""SASL authentication failed. Disconnect and shutdown."""
|
||||
self.attempted_mechs.add(self.mech.name)
|
||||
log.info("Authentication failed: %s", stanza['condition'])
|
||||
self.xmpp.event("failed_auth", stanza, direct=True)
|
||||
self._send_auth()
|
||||
return True
|
||||
16
slixmpp/features/feature_mechanisms/stanza/__init__.py
Normal file
16
slixmpp/features/feature_mechanisms/stanza/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
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.features.feature_mechanisms.stanza.mechanisms import Mechanisms
|
||||
from slixmpp.features.feature_mechanisms.stanza.auth import Auth
|
||||
from slixmpp.features.feature_mechanisms.stanza.success import Success
|
||||
from slixmpp.features.feature_mechanisms.stanza.failure import Failure
|
||||
from slixmpp.features.feature_mechanisms.stanza.challenge import Challenge
|
||||
from slixmpp.features.feature_mechanisms.stanza.response import Response
|
||||
from slixmpp.features.feature_mechanisms.stanza.abort import Abort
|
||||
24
slixmpp/features/feature_mechanisms/stanza/abort.py
Normal file
24
slixmpp/features/feature_mechanisms/stanza/abort.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
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 StanzaBase
|
||||
|
||||
|
||||
class Abort(StanzaBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'abort'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||
interfaces = set()
|
||||
plugin_attrib = name
|
||||
|
||||
def setup(self, xml):
|
||||
StanzaBase.setup(self, xml)
|
||||
self.xml.tag = self.tag_name()
|
||||
49
slixmpp/features/feature_mechanisms/stanza/auth.py
Normal file
49
slixmpp/features/feature_mechanisms/stanza/auth.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
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 base64
|
||||
|
||||
from slixmpp.util import bytes
|
||||
from slixmpp.xmlstream import StanzaBase
|
||||
|
||||
|
||||
class Auth(StanzaBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'auth'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||
interfaces = set(('mechanism', 'value'))
|
||||
plugin_attrib = name
|
||||
|
||||
#: Some SASL mechs require sending values as is,
|
||||
#: without converting base64.
|
||||
plain_mechs = set(['X-MESSENGER-OAUTH2'])
|
||||
|
||||
def setup(self, xml):
|
||||
StanzaBase.setup(self, xml)
|
||||
self.xml.tag = self.tag_name()
|
||||
|
||||
def get_value(self):
|
||||
if not self['mechanism'] in self.plain_mechs:
|
||||
return base64.b64decode(bytes(self.xml.text))
|
||||
else:
|
||||
return self.xml.text
|
||||
|
||||
def set_value(self, values):
|
||||
if not self['mechanism'] in self.plain_mechs:
|
||||
if values:
|
||||
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
|
||||
elif values == b'':
|
||||
self.xml.text = '='
|
||||
else:
|
||||
self.xml.text = bytes(values).decode('utf-8')
|
||||
|
||||
def del_value(self):
|
||||
self.xml.text = ''
|
||||
39
slixmpp/features/feature_mechanisms/stanza/challenge.py
Normal file
39
slixmpp/features/feature_mechanisms/stanza/challenge.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
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 base64
|
||||
|
||||
from slixmpp.util import bytes
|
||||
from slixmpp.xmlstream import StanzaBase
|
||||
|
||||
|
||||
class Challenge(StanzaBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'challenge'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||
interfaces = set(('value',))
|
||||
plugin_attrib = name
|
||||
|
||||
def setup(self, xml):
|
||||
StanzaBase.setup(self, xml)
|
||||
self.xml.tag = self.tag_name()
|
||||
|
||||
def get_value(self):
|
||||
return base64.b64decode(bytes(self.xml.text))
|
||||
|
||||
def set_value(self, values):
|
||||
if values:
|
||||
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
|
||||
else:
|
||||
self.xml.text = '='
|
||||
|
||||
def del_value(self):
|
||||
self.xml.text = ''
|
||||
76
slixmpp/features/feature_mechanisms/stanza/failure.py
Normal file
76
slixmpp/features/feature_mechanisms/stanza/failure.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
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 StanzaBase, ET
|
||||
|
||||
|
||||
class Failure(StanzaBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'failure'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||
interfaces = set(('condition', 'text'))
|
||||
plugin_attrib = name
|
||||
sub_interfaces = set(('text',))
|
||||
conditions = set(('aborted', 'account-disabled', 'credentials-expired',
|
||||
'encryption-required', 'incorrect-encoding', 'invalid-authzid',
|
||||
'invalid-mechanism', 'malformed-request', 'mechansism-too-weak',
|
||||
'not-authorized', 'temporary-auth-failure'))
|
||||
|
||||
def setup(self, xml=None):
|
||||
"""
|
||||
Populate the stanza object using an optional XML object.
|
||||
|
||||
Overrides ElementBase.setup.
|
||||
|
||||
Sets a default error type and condition, and changes the
|
||||
parent stanza's type to 'error'.
|
||||
|
||||
Arguments:
|
||||
xml -- Use an existing XML object for the stanza's values.
|
||||
"""
|
||||
# StanzaBase overrides self.namespace
|
||||
self.namespace = Failure.namespace
|
||||
|
||||
if StanzaBase.setup(self, xml):
|
||||
#If we had to generate XML then set default values.
|
||||
self['condition'] = 'not-authorized'
|
||||
|
||||
self.xml.tag = self.tag_name()
|
||||
|
||||
def get_condition(self):
|
||||
"""Return the condition element's name."""
|
||||
for child in self.xml:
|
||||
if "{%s}" % self.namespace in child.tag:
|
||||
cond = child.tag.split('}', 1)[-1]
|
||||
if cond in self.conditions:
|
||||
return cond
|
||||
return 'not-authorized'
|
||||
|
||||
def set_condition(self, value):
|
||||
"""
|
||||
Set the tag name of the condition element.
|
||||
|
||||
Arguments:
|
||||
value -- The tag name of the condition element.
|
||||
"""
|
||||
if value in self.conditions:
|
||||
del self['condition']
|
||||
self.xml.append(ET.Element("{%s}%s" % (self.namespace, value)))
|
||||
return self
|
||||
|
||||
def del_condition(self):
|
||||
"""Remove the condition element."""
|
||||
for child in self.xml:
|
||||
if "{%s}" % self.condition_ns in child.tag:
|
||||
tag = child.tag.split('}', 1)[-1]
|
||||
if tag in self.conditions:
|
||||
self.xml.remove(child)
|
||||
return self
|
||||
53
slixmpp/features/feature_mechanisms/stanza/mechanisms.py
Normal file
53
slixmpp/features/feature_mechanisms/stanza/mechanisms.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class Mechanisms(ElementBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'mechanisms'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||
interfaces = set(('mechanisms', 'required'))
|
||||
plugin_attrib = name
|
||||
is_extension = True
|
||||
|
||||
def get_required(self):
|
||||
"""
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_mechanisms(self):
|
||||
"""
|
||||
"""
|
||||
results = []
|
||||
mechs = self.findall('{%s}mechanism' % self.namespace)
|
||||
if mechs:
|
||||
for mech in mechs:
|
||||
results.append(mech.text)
|
||||
return results
|
||||
|
||||
def set_mechanisms(self, values):
|
||||
"""
|
||||
"""
|
||||
self.del_mechanisms()
|
||||
for val in values:
|
||||
mech = ET.Element('{%s}mechanism' % self.namespace)
|
||||
mech.text = val
|
||||
self.append(mech)
|
||||
|
||||
def del_mechanisms(self):
|
||||
"""
|
||||
"""
|
||||
mechs = self.findall('{%s}mechanism' % self.namespace)
|
||||
if mechs:
|
||||
for mech in mechs:
|
||||
self.xml.remove(mech)
|
||||
39
slixmpp/features/feature_mechanisms/stanza/response.py
Normal file
39
slixmpp/features/feature_mechanisms/stanza/response.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
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 base64
|
||||
|
||||
from slixmpp.util import bytes
|
||||
from slixmpp.xmlstream import StanzaBase
|
||||
|
||||
|
||||
class Response(StanzaBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'response'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||
interfaces = set(('value',))
|
||||
plugin_attrib = name
|
||||
|
||||
def setup(self, xml):
|
||||
StanzaBase.setup(self, xml)
|
||||
self.xml.tag = self.tag_name()
|
||||
|
||||
def get_value(self):
|
||||
return base64.b64decode(bytes(self.xml.text))
|
||||
|
||||
def set_value(self, values):
|
||||
if values:
|
||||
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
|
||||
else:
|
||||
self.xml.text = '='
|
||||
|
||||
def del_value(self):
|
||||
self.xml.text = ''
|
||||
38
slixmpp/features/feature_mechanisms/stanza/success.py
Normal file
38
slixmpp/features/feature_mechanisms/stanza/success.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
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 base64
|
||||
|
||||
from slixmpp.util import bytes
|
||||
from slixmpp.xmlstream import StanzaBase
|
||||
|
||||
class Success(StanzaBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'success'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-sasl'
|
||||
interfaces = set(['value'])
|
||||
plugin_attrib = name
|
||||
|
||||
def setup(self, xml):
|
||||
StanzaBase.setup(self, xml)
|
||||
self.xml.tag = self.tag_name()
|
||||
|
||||
def get_value(self):
|
||||
return base64.b64decode(bytes(self.xml.text))
|
||||
|
||||
def set_value(self, values):
|
||||
if values:
|
||||
self.xml.text = bytes(base64.b64encode(values)).decode('utf-8')
|
||||
else:
|
||||
self.xml.text = '='
|
||||
|
||||
def del_value(self):
|
||||
self.xml.text = ''
|
||||
15
slixmpp/features/feature_preapproval/__init__.py
Normal file
15
slixmpp/features/feature_preapproval/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
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.plugins.base import register_plugin
|
||||
|
||||
from slixmpp.features.feature_preapproval.preapproval import FeaturePreApproval
|
||||
from slixmpp.features.feature_preapproval.stanza import PreApproval
|
||||
|
||||
|
||||
register_plugin(FeaturePreApproval)
|
||||
42
slixmpp/features/feature_preapproval/preapproval.py
Normal file
42
slixmpp/features/feature_preapproval/preapproval.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2012 Nathanael C. Fritz
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from slixmpp.stanza import StreamFeatures
|
||||
from slixmpp.features.feature_preapproval import stanza
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins.base import BasePlugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeaturePreApproval(BasePlugin):
|
||||
|
||||
name = 'feature_preapproval'
|
||||
description = 'RFC 6121: Stream Feature: Subscription Pre-Approval'
|
||||
dependences = set()
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
self.xmpp.register_feature('preapproval',
|
||||
self._handle_preapproval,
|
||||
restart=False,
|
||||
order=9001)
|
||||
|
||||
register_stanza_plugin(StreamFeatures, stanza.PreApproval)
|
||||
|
||||
def _handle_preapproval(self, features):
|
||||
"""Save notice that the server support subscription pre-approvals.
|
||||
|
||||
Arguments:
|
||||
features -- The stream features stanza.
|
||||
"""
|
||||
log.debug("Server supports subscription pre-approvals.")
|
||||
self.xmpp.features.add('preapproval')
|
||||
17
slixmpp/features/feature_preapproval/stanza.py
Normal file
17
slixmpp/features/feature_preapproval/stanza.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
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.xmlstream import ElementBase
|
||||
|
||||
|
||||
class PreApproval(ElementBase):
|
||||
|
||||
name = 'sub'
|
||||
namespace = 'urn:xmpp:features:pre-approval'
|
||||
interfaces = set()
|
||||
plugin_attrib = 'preapproval'
|
||||
19
slixmpp/features/feature_rosterver/__init__.py
Normal file
19
slixmpp/features/feature_rosterver/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
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.plugins.base import register_plugin
|
||||
|
||||
from slixmpp.features.feature_rosterver.rosterver import FeatureRosterVer
|
||||
from slixmpp.features.feature_rosterver.stanza import RosterVer
|
||||
|
||||
|
||||
register_plugin(FeatureRosterVer)
|
||||
|
||||
|
||||
# Retain some backwards compatibility
|
||||
feature_rosterver = FeatureRosterVer
|
||||
42
slixmpp/features/feature_rosterver/rosterver.py
Normal file
42
slixmpp/features/feature_rosterver/rosterver.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2012 Nathanael C. Fritz
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from slixmpp.stanza import StreamFeatures
|
||||
from slixmpp.features.feature_rosterver import stanza
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins.base import BasePlugin
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureRosterVer(BasePlugin):
|
||||
|
||||
name = 'feature_rosterver'
|
||||
description = 'RFC 6121: Stream Feature: Roster Versioning'
|
||||
dependences = set()
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
self.xmpp.register_feature('rosterver',
|
||||
self._handle_rosterver,
|
||||
restart=False,
|
||||
order=9000)
|
||||
|
||||
register_stanza_plugin(StreamFeatures, stanza.RosterVer)
|
||||
|
||||
def _handle_rosterver(self, features):
|
||||
"""Enable using roster versioning.
|
||||
|
||||
Arguments:
|
||||
features -- The stream features stanza.
|
||||
"""
|
||||
log.debug("Enabling roster versioning.")
|
||||
self.xmpp.features.add('rosterver')
|
||||
17
slixmpp/features/feature_rosterver/stanza.py
Normal file
17
slixmpp/features/feature_rosterver/stanza.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
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.xmlstream import ElementBase
|
||||
|
||||
|
||||
class RosterVer(ElementBase):
|
||||
|
||||
name = 'ver'
|
||||
namespace = 'urn:xmpp:features:rosterver'
|
||||
interfaces = set()
|
||||
plugin_attrib = 'rosterver'
|
||||
19
slixmpp/features/feature_session/__init__.py
Normal file
19
slixmpp/features/feature_session/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
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.base import register_plugin
|
||||
|
||||
from slixmpp.features.feature_session.session import FeatureSession
|
||||
from slixmpp.features.feature_session.stanza import Session
|
||||
|
||||
|
||||
register_plugin(FeatureSession)
|
||||
|
||||
|
||||
# Retain some backwards compatibility
|
||||
feature_session = FeatureSession
|
||||
54
slixmpp/features/feature_session/session.py
Normal file
54
slixmpp/features/feature_session/session.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
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.stanza import Iq, StreamFeatures
|
||||
from slixmpp.xmlstream import register_stanza_plugin
|
||||
from slixmpp.plugins import BasePlugin
|
||||
|
||||
from slixmpp.features.feature_session import stanza
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureSession(BasePlugin):
|
||||
|
||||
name = 'feature_session'
|
||||
description = 'RFC 3920: Stream Feature: Start Session'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
self.xmpp.register_feature('session',
|
||||
self._handle_start_session,
|
||||
restart=False,
|
||||
order=10001)
|
||||
|
||||
register_stanza_plugin(Iq, stanza.Session)
|
||||
register_stanza_plugin(StreamFeatures, stanza.Session)
|
||||
|
||||
def _handle_start_session(self, features):
|
||||
"""
|
||||
Handle the start of the session.
|
||||
|
||||
Arguments:
|
||||
feature -- The stream features element.
|
||||
"""
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'set'
|
||||
iq.enable('session')
|
||||
iq.send(now=True)
|
||||
|
||||
self.xmpp.features.add('session')
|
||||
|
||||
log.debug("Established Session")
|
||||
self.xmpp.sessionstarted = True
|
||||
self.xmpp.session_started_event.set()
|
||||
self.xmpp.event('session_start')
|
||||
20
slixmpp/features/feature_session/stanza.py
Normal file
20
slixmpp/features/feature_session/stanza.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class Session(ElementBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'session'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-session'
|
||||
interfaces = set()
|
||||
plugin_attrib = 'session'
|
||||
19
slixmpp/features/feature_starttls/__init__.py
Normal file
19
slixmpp/features/feature_starttls/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
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.base import register_plugin
|
||||
|
||||
from slixmpp.features.feature_starttls.starttls import FeatureSTARTTLS
|
||||
from slixmpp.features.feature_starttls.stanza import *
|
||||
|
||||
|
||||
register_plugin(FeatureSTARTTLS)
|
||||
|
||||
|
||||
# Retain some backwards compatibility
|
||||
feature_starttls = FeatureSTARTTLS
|
||||
45
slixmpp/features/feature_starttls/stanza.py
Normal file
45
slixmpp/features/feature_starttls/stanza.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
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 StanzaBase, ElementBase
|
||||
|
||||
|
||||
class STARTTLS(ElementBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'starttls'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
|
||||
interfaces = set(('required',))
|
||||
plugin_attrib = name
|
||||
|
||||
def get_required(self):
|
||||
"""
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class Proceed(StanzaBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'proceed'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
|
||||
interfaces = set()
|
||||
|
||||
|
||||
class Failure(StanzaBase):
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
name = 'failure'
|
||||
namespace = 'urn:ietf:params:xml:ns:xmpp-tls'
|
||||
interfaces = set()
|
||||
66
slixmpp/features/feature_starttls/starttls.py
Normal file
66
slixmpp/features/feature_starttls/starttls.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""
|
||||
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.stanza import StreamFeatures
|
||||
from slixmpp.xmlstream import RestartStream, register_stanza_plugin
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.xmlstream.matcher import MatchXPath
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.features.feature_starttls import stanza
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FeatureSTARTTLS(BasePlugin):
|
||||
|
||||
name = 'feature_starttls'
|
||||
description = 'RFC 6120: Stream Feature: STARTTLS'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
|
||||
def plugin_init(self):
|
||||
self.xmpp.register_handler(
|
||||
Callback('STARTTLS Proceed',
|
||||
MatchXPath(stanza.Proceed.tag_name()),
|
||||
self._handle_starttls_proceed,
|
||||
instream=True))
|
||||
self.xmpp.register_feature('starttls',
|
||||
self._handle_starttls,
|
||||
restart=True,
|
||||
order=self.config.get('order', 0))
|
||||
|
||||
self.xmpp.register_stanza(stanza.Proceed)
|
||||
self.xmpp.register_stanza(stanza.Failure)
|
||||
register_stanza_plugin(StreamFeatures, stanza.STARTTLS)
|
||||
|
||||
def _handle_starttls(self, features):
|
||||
"""
|
||||
Handle notification that the server supports TLS.
|
||||
|
||||
Arguments:
|
||||
features -- The stream:features element.
|
||||
"""
|
||||
if 'starttls' in self.xmpp.features:
|
||||
# We have already negotiated TLS, but the server is
|
||||
# offering it again, against spec.
|
||||
return False
|
||||
elif not self.xmpp.use_tls:
|
||||
return False
|
||||
else:
|
||||
self.xmpp.send(features['starttls'], now=True)
|
||||
return True
|
||||
|
||||
def _handle_starttls_proceed(self, proceed):
|
||||
"""Restart the XML stream when TLS is accepted."""
|
||||
log.debug("Starting TLS")
|
||||
if self.xmpp.start_tls():
|
||||
self.xmpp.features.add('starttls')
|
||||
raise RestartStream()
|
||||
638
slixmpp/jid.py
Normal file
638
slixmpp/jid.py
Normal file
@@ -0,0 +1,638 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
slixmpp.jid
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module allows for working with Jabber IDs (JIDs).
|
||||
|
||||
Part of Slixmpp: The Slick XMPP Library
|
||||
|
||||
:copyright: (c) 2011 Nathanael C. Fritz
|
||||
:license: MIT, see LICENSE for more details
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import socket
|
||||
import stringprep
|
||||
import threading
|
||||
import encodings.idna
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
from slixmpp.util import stringprep_profiles
|
||||
from slixmpp.thirdparty import OrderedDict
|
||||
|
||||
#: These characters are not allowed to appear in a JID.
|
||||
ILLEGAL_CHARS = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r' + \
|
||||
'\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19' + \
|
||||
'\x1a\x1b\x1c\x1d\x1e\x1f' + \
|
||||
' !"#$%&\'()*+,./:;<=>?@[\\]^_`{|}~\x7f'
|
||||
|
||||
#: The basic regex pattern that a JID must match in order to determine
|
||||
#: the local, domain, and resource parts. This regex does NOT do any
|
||||
#: validation, which requires application of nodeprep, resourceprep, etc.
|
||||
JID_PATTERN = re.compile(
|
||||
"^(?:([^\"&'/:<>@]{1,1023})@)?([^/@]{1,1023})(?:/(.{1,1023}))?$"
|
||||
)
|
||||
|
||||
#: The set of escape sequences for the characters not allowed by nodeprep.
|
||||
JID_ESCAPE_SEQUENCES = set(['\\20', '\\22', '\\26', '\\27', '\\2f',
|
||||
'\\3a', '\\3c', '\\3e', '\\40', '\\5c'])
|
||||
|
||||
#: A mapping of unallowed characters to their escape sequences. An escape
|
||||
#: sequence for '\' is also included since it must also be escaped in
|
||||
#: certain situations.
|
||||
JID_ESCAPE_TRANSFORMATIONS = {' ': '\\20',
|
||||
'"': '\\22',
|
||||
'&': '\\26',
|
||||
"'": '\\27',
|
||||
'/': '\\2f',
|
||||
':': '\\3a',
|
||||
'<': '\\3c',
|
||||
'>': '\\3e',
|
||||
'@': '\\40',
|
||||
'\\': '\\5c'}
|
||||
|
||||
#: The reverse mapping of escape sequences to their original forms.
|
||||
JID_UNESCAPE_TRANSFORMATIONS = {'\\20': ' ',
|
||||
'\\22': '"',
|
||||
'\\26': '&',
|
||||
'\\27': "'",
|
||||
'\\2f': '/',
|
||||
'\\3a': ':',
|
||||
'\\3c': '<',
|
||||
'\\3e': '>',
|
||||
'\\40': '@',
|
||||
'\\5c': '\\'}
|
||||
|
||||
JID_CACHE = OrderedDict()
|
||||
JID_CACHE_LOCK = threading.Lock()
|
||||
JID_CACHE_MAX_SIZE = 1024
|
||||
|
||||
def _cache(key, parts, locked):
|
||||
JID_CACHE[key] = (parts, locked)
|
||||
if len(JID_CACHE) > JID_CACHE_MAX_SIZE:
|
||||
with JID_CACHE_LOCK:
|
||||
while len(JID_CACHE) > JID_CACHE_MAX_SIZE:
|
||||
found = None
|
||||
for key, item in JID_CACHE.items():
|
||||
if not item[1]: # if not locked
|
||||
found = key
|
||||
break
|
||||
if not found: # more than MAX_SIZE locked
|
||||
# warn?
|
||||
break
|
||||
del JID_CACHE[found]
|
||||
|
||||
# pylint: disable=c0103
|
||||
#: The nodeprep profile of stringprep used to validate the local,
|
||||
#: or username, portion of a JID.
|
||||
nodeprep = stringprep_profiles.create(
|
||||
nfkc=True,
|
||||
bidi=True,
|
||||
mappings=[
|
||||
stringprep_profiles.b1_mapping,
|
||||
stringprep.map_table_b2],
|
||||
prohibited=[
|
||||
stringprep.in_table_c11,
|
||||
stringprep.in_table_c12,
|
||||
stringprep.in_table_c21,
|
||||
stringprep.in_table_c22,
|
||||
stringprep.in_table_c3,
|
||||
stringprep.in_table_c4,
|
||||
stringprep.in_table_c5,
|
||||
stringprep.in_table_c6,
|
||||
stringprep.in_table_c7,
|
||||
stringprep.in_table_c8,
|
||||
stringprep.in_table_c9,
|
||||
lambda c: c in ' \'"&/:<>@'],
|
||||
unassigned=[stringprep.in_table_a1])
|
||||
|
||||
# pylint: disable=c0103
|
||||
#: The resourceprep profile of stringprep, which is used to validate
|
||||
#: the resource portion of a JID.
|
||||
resourceprep = stringprep_profiles.create(
|
||||
nfkc=True,
|
||||
bidi=True,
|
||||
mappings=[stringprep_profiles.b1_mapping],
|
||||
prohibited=[
|
||||
stringprep.in_table_c12,
|
||||
stringprep.in_table_c21,
|
||||
stringprep.in_table_c22,
|
||||
stringprep.in_table_c3,
|
||||
stringprep.in_table_c4,
|
||||
stringprep.in_table_c5,
|
||||
stringprep.in_table_c6,
|
||||
stringprep.in_table_c7,
|
||||
stringprep.in_table_c8,
|
||||
stringprep.in_table_c9],
|
||||
unassigned=[stringprep.in_table_a1])
|
||||
|
||||
|
||||
def _parse_jid(data):
|
||||
"""
|
||||
Parse string data into the node, domain, and resource
|
||||
components of a JID, if possible.
|
||||
|
||||
:param string data: A string that is potentially a JID.
|
||||
|
||||
:raises InvalidJID:
|
||||
|
||||
:returns: tuple of the validated local, domain, and resource strings
|
||||
"""
|
||||
match = JID_PATTERN.match(data)
|
||||
if not match:
|
||||
raise InvalidJID('JID could not be parsed')
|
||||
|
||||
(node, domain, resource) = match.groups()
|
||||
|
||||
node = _validate_node(node)
|
||||
domain = _validate_domain(domain)
|
||||
resource = _validate_resource(resource)
|
||||
|
||||
return node, domain, resource
|
||||
|
||||
|
||||
def _validate_node(node):
|
||||
"""Validate the local, or username, portion of a JID.
|
||||
|
||||
:raises InvalidJID:
|
||||
|
||||
:returns: The local portion of a JID, as validated by nodeprep.
|
||||
"""
|
||||
try:
|
||||
if node is not None:
|
||||
node = nodeprep(node)
|
||||
|
||||
if not node:
|
||||
raise InvalidJID('Localpart must not be 0 bytes')
|
||||
if len(node) > 1023:
|
||||
raise InvalidJID('Localpart must be less than 1024 bytes')
|
||||
return node
|
||||
except stringprep_profiles.StringPrepError:
|
||||
raise InvalidJID('Invalid local part')
|
||||
|
||||
|
||||
def _validate_domain(domain):
|
||||
"""Validate the domain portion of a JID.
|
||||
|
||||
IP literal addresses are left as-is, if valid. Domain names
|
||||
are stripped of any trailing label separators (`.`), and are
|
||||
checked with the nameprep profile of stringprep. If the given
|
||||
domain is actually a punyencoded version of a domain name, it
|
||||
is converted back into its original Unicode form. Domains must
|
||||
also not start or end with a dash (`-`).
|
||||
|
||||
:raises InvalidJID:
|
||||
|
||||
:returns: The validated domain name
|
||||
"""
|
||||
ip_addr = False
|
||||
|
||||
# First, check if this is an IPv4 address
|
||||
try:
|
||||
socket.inet_aton(domain)
|
||||
ip_addr = True
|
||||
except socket.error:
|
||||
pass
|
||||
|
||||
# Check if this is an IPv6 address
|
||||
if not ip_addr and hasattr(socket, 'inet_pton'):
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, domain.strip('[]'))
|
||||
domain = '[%s]' % domain.strip('[]')
|
||||
ip_addr = True
|
||||
except (socket.error, ValueError):
|
||||
pass
|
||||
|
||||
if not ip_addr:
|
||||
# This is a domain name, which must be checked further
|
||||
|
||||
if domain and domain[-1] == '.':
|
||||
domain = domain[:-1]
|
||||
|
||||
domain_parts = []
|
||||
for label in domain.split('.'):
|
||||
try:
|
||||
label = encodings.idna.nameprep(label)
|
||||
encodings.idna.ToASCII(label)
|
||||
pass_nameprep = True
|
||||
except UnicodeError:
|
||||
pass_nameprep = False
|
||||
|
||||
if not pass_nameprep:
|
||||
raise InvalidJID('Could not encode domain as ASCII')
|
||||
|
||||
if label.startswith('xn--'):
|
||||
label = encodings.idna.ToUnicode(label)
|
||||
|
||||
for char in label:
|
||||
if char in ILLEGAL_CHARS:
|
||||
raise InvalidJID('Domain contains illegal characters')
|
||||
|
||||
if '-' in (label[0], label[-1]):
|
||||
raise InvalidJID('Domain started or ended with -')
|
||||
|
||||
domain_parts.append(label)
|
||||
domain = '.'.join(domain_parts)
|
||||
|
||||
if not domain:
|
||||
raise InvalidJID('Domain must not be 0 bytes')
|
||||
if len(domain) > 1023:
|
||||
raise InvalidJID('Domain must be less than 1024 bytes')
|
||||
|
||||
return domain
|
||||
|
||||
|
||||
def _validate_resource(resource):
|
||||
"""Validate the resource portion of a JID.
|
||||
|
||||
:raises InvalidJID:
|
||||
|
||||
:returns: The local portion of a JID, as validated by resourceprep.
|
||||
"""
|
||||
try:
|
||||
if resource is not None:
|
||||
resource = resourceprep(resource)
|
||||
|
||||
if not resource:
|
||||
raise InvalidJID('Resource must not be 0 bytes')
|
||||
if len(resource) > 1023:
|
||||
raise InvalidJID('Resource must be less than 1024 bytes')
|
||||
return resource
|
||||
except stringprep_profiles.StringPrepError:
|
||||
raise InvalidJID('Invalid resource')
|
||||
|
||||
|
||||
def _escape_node(node):
|
||||
"""Escape the local portion of a JID."""
|
||||
result = []
|
||||
|
||||
for i, char in enumerate(node):
|
||||
if char == '\\':
|
||||
if ''.join((node[i:i+3])) in JID_ESCAPE_SEQUENCES:
|
||||
result.append('\\5c')
|
||||
continue
|
||||
result.append(char)
|
||||
|
||||
for i, char in enumerate(result):
|
||||
if char != '\\':
|
||||
result[i] = JID_ESCAPE_TRANSFORMATIONS.get(char, char)
|
||||
|
||||
escaped = ''.join(result)
|
||||
|
||||
if escaped.startswith('\\20') or escaped.endswith('\\20'):
|
||||
raise InvalidJID('Escaped local part starts or ends with "\\20"')
|
||||
|
||||
_validate_node(escaped)
|
||||
|
||||
return escaped
|
||||
|
||||
|
||||
def _unescape_node(node):
|
||||
"""Unescape a local portion of a JID.
|
||||
|
||||
.. note::
|
||||
The unescaped local portion is meant ONLY for presentation,
|
||||
and should not be used for other purposes.
|
||||
"""
|
||||
unescaped = []
|
||||
seq = ''
|
||||
for i, char in enumerate(node):
|
||||
if char == '\\':
|
||||
seq = node[i:i+3]
|
||||
if seq not in JID_ESCAPE_SEQUENCES:
|
||||
seq = ''
|
||||
if seq:
|
||||
if len(seq) == 3:
|
||||
unescaped.append(JID_UNESCAPE_TRANSFORMATIONS.get(seq, char))
|
||||
|
||||
# Pop character off the escape sequence, and ignore it
|
||||
seq = seq[1:]
|
||||
else:
|
||||
unescaped.append(char)
|
||||
unescaped = ''.join(unescaped)
|
||||
|
||||
return unescaped
|
||||
|
||||
|
||||
def _format_jid(local=None, domain=None, resource=None):
|
||||
"""Format the given JID components into a full or bare JID.
|
||||
|
||||
:param string local: Optional. The local portion of the JID.
|
||||
:param string domain: Required. The domain name portion of the JID.
|
||||
:param strin resource: Optional. The resource portion of the JID.
|
||||
|
||||
:return: A full or bare JID string.
|
||||
"""
|
||||
result = []
|
||||
if local:
|
||||
result.append(local)
|
||||
result.append('@')
|
||||
if domain:
|
||||
result.append(domain)
|
||||
if resource:
|
||||
result.append('/')
|
||||
result.append(resource)
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
class InvalidJID(ValueError):
|
||||
"""
|
||||
Raised when attempting to create a JID that does not pass validation.
|
||||
|
||||
It can also be raised if modifying an existing JID in such a way as
|
||||
to make it invalid, such trying to remove the domain from an existing
|
||||
full JID while the local and resource portions still exist.
|
||||
"""
|
||||
|
||||
# pylint: disable=R0903
|
||||
class UnescapedJID(object):
|
||||
|
||||
"""
|
||||
.. versionadded:: 1.1.10
|
||||
"""
|
||||
|
||||
def __init__(self, local, domain, resource):
|
||||
self._jid = (local, domain, resource)
|
||||
|
||||
# pylint: disable=R0911
|
||||
def __getattr__(self, name):
|
||||
"""Retrieve the given JID component.
|
||||
|
||||
:param name: one of: user, server, domain, resource,
|
||||
full, or bare.
|
||||
"""
|
||||
if name == 'resource':
|
||||
return self._jid[2] or ''
|
||||
elif name in ('user', 'username', 'local', 'node'):
|
||||
return self._jid[0] or ''
|
||||
elif name in ('server', 'domain', 'host'):
|
||||
return self._jid[1] or ''
|
||||
elif name in ('full', 'jid'):
|
||||
return _format_jid(*self._jid)
|
||||
elif name == 'bare':
|
||||
return _format_jid(self._jid[0], self._jid[1])
|
||||
elif name == '_jid':
|
||||
return getattr(super(JID, self), '_jid')
|
||||
else:
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
"""Use the full JID as the string value."""
|
||||
return _format_jid(*self._jid)
|
||||
|
||||
def __repr__(self):
|
||||
"""Use the full JID as the representation."""
|
||||
return self.__str__()
|
||||
|
||||
|
||||
class JID(object):
|
||||
|
||||
"""
|
||||
A representation of a Jabber ID, or JID.
|
||||
|
||||
Each JID may have three components: a user, a domain, and an optional
|
||||
resource. For example: user@domain/resource
|
||||
|
||||
When a resource is not used, the JID is called a bare JID.
|
||||
The JID is a full JID otherwise.
|
||||
|
||||
**JID Properties:**
|
||||
:jid: Alias for ``full``.
|
||||
:full: The string value of the full JID.
|
||||
:bare: The string value of the bare JID.
|
||||
:user: The username portion of the JID.
|
||||
:username: Alias for ``user``.
|
||||
:local: Alias for ``user``.
|
||||
:node: Alias for ``user``.
|
||||
:domain: The domain name portion of the JID.
|
||||
:server: Alias for ``domain``.
|
||||
:host: Alias for ``domain``.
|
||||
:resource: The resource portion of the JID.
|
||||
|
||||
:param string jid:
|
||||
A string of the form ``'[user@]domain[/resource]'``.
|
||||
:param string local:
|
||||
Optional. Specify the local, or username, portion
|
||||
of the JID. If provided, it will override the local
|
||||
value provided by the `jid` parameter. The given
|
||||
local value will also be escaped if necessary.
|
||||
:param string domain:
|
||||
Optional. Specify the domain of the JID. If
|
||||
provided, it will override the domain given by
|
||||
the `jid` parameter.
|
||||
:param string resource:
|
||||
Optional. Specify the resource value of the JID.
|
||||
If provided, it will override the domain given
|
||||
by the `jid` parameter.
|
||||
|
||||
:raises InvalidJID:
|
||||
"""
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __init__(self, jid=None, **kwargs):
|
||||
locked = kwargs.get('cache_lock', False)
|
||||
in_local = kwargs.get('local', None)
|
||||
in_domain = kwargs.get('domain', None)
|
||||
in_resource = kwargs.get('resource', None)
|
||||
parts = None
|
||||
if in_local or in_domain or in_resource:
|
||||
parts = (in_local, in_domain, in_resource)
|
||||
|
||||
# only check cache if there is a jid string, or parts, not if there
|
||||
# are both
|
||||
self._jid = None
|
||||
key = None
|
||||
if (jid is not None) and (parts is None):
|
||||
if isinstance(jid, JID):
|
||||
# it's already good to go, and there are no additions
|
||||
self._jid = jid._jid
|
||||
return
|
||||
key = jid
|
||||
self._jid, locked = JID_CACHE.get(jid, (None, locked))
|
||||
elif jid is None and parts is not None:
|
||||
key = parts
|
||||
self._jid, locked = JID_CACHE.get(parts, (None, locked))
|
||||
if not self._jid:
|
||||
if not jid:
|
||||
parsed_jid = (None, None, None)
|
||||
elif not isinstance(jid, JID):
|
||||
parsed_jid = _parse_jid(jid)
|
||||
else:
|
||||
parsed_jid = jid._jid
|
||||
|
||||
local, domain, resource = parsed_jid
|
||||
|
||||
if 'local' in kwargs:
|
||||
local = _escape_node(in_local)
|
||||
if 'domain' in kwargs:
|
||||
domain = _validate_domain(in_domain)
|
||||
if 'resource' in kwargs:
|
||||
resource = _validate_resource(in_resource)
|
||||
|
||||
self._jid = (local, domain, resource)
|
||||
if key:
|
||||
_cache(key, self._jid, locked)
|
||||
|
||||
def unescape(self):
|
||||
"""Return an unescaped JID object.
|
||||
|
||||
Using an unescaped JID is preferred for displaying JIDs
|
||||
to humans, and they should NOT be used for any other
|
||||
purposes than for presentation.
|
||||
|
||||
:return: :class:`UnescapedJID`
|
||||
|
||||
.. versionadded:: 1.1.10
|
||||
"""
|
||||
return UnescapedJID(_unescape_node(self._jid[0]),
|
||||
self._jid[1],
|
||||
self._jid[2])
|
||||
|
||||
def regenerate(self):
|
||||
"""No-op
|
||||
|
||||
.. deprecated:: 1.1.10
|
||||
"""
|
||||
pass
|
||||
|
||||
def reset(self, data):
|
||||
"""Start fresh from a new JID string.
|
||||
|
||||
:param string data: A string of the form ``'[user@]domain[/resource]'``.
|
||||
|
||||
.. deprecated:: 1.1.10
|
||||
"""
|
||||
self._jid = JID(data)._jid
|
||||
|
||||
@property
|
||||
def resource(self):
|
||||
return self._jid[2] or ''
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self._jid[0] or ''
|
||||
|
||||
@property
|
||||
def local(self):
|
||||
return self._jid[0] or ''
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
return self._jid[0] or ''
|
||||
|
||||
@property
|
||||
def username(self):
|
||||
return self._jid[0] or ''
|
||||
|
||||
@property
|
||||
def bare(self):
|
||||
return _format_jid(self._jid[0], self._jid[1])
|
||||
|
||||
@property
|
||||
def server(self):
|
||||
return self._jid[1] or ''
|
||||
|
||||
@property
|
||||
def domain(self):
|
||||
return self._jid[1] or ''
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self._jid[1] or ''
|
||||
|
||||
@property
|
||||
def full(self):
|
||||
return _format_jid(*self._jid)
|
||||
|
||||
@property
|
||||
def jid(self):
|
||||
return _format_jid(*self._jid)
|
||||
|
||||
@property
|
||||
def bare(self):
|
||||
return _format_jid(self._jid[0], self._jid[1])
|
||||
|
||||
|
||||
@resource.setter
|
||||
def resource(self, value):
|
||||
self._jid = JID(self, resource=value)._jid
|
||||
|
||||
@user.setter
|
||||
def user(self, value):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
|
||||
@username.setter
|
||||
def username(self, value):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
|
||||
@local.setter
|
||||
def local(self, value):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
|
||||
@node.setter
|
||||
def node(self, value):
|
||||
self._jid = JID(self, local=value)._jid
|
||||
|
||||
@server.setter
|
||||
def server(self, value):
|
||||
self._jid = JID(self, domain=value)._jid
|
||||
|
||||
@domain.setter
|
||||
def domain(self, value):
|
||||
self._jid = JID(self, domain=value)._jid
|
||||
|
||||
@host.setter
|
||||
def host(self, value):
|
||||
self._jid = JID(self, domain=value)._jid
|
||||
|
||||
@full.setter
|
||||
def full(self, value):
|
||||
self._jid = JID(value)._jid
|
||||
|
||||
@jid.setter
|
||||
def jid(self, value):
|
||||
self._jid = JID(value)._jid
|
||||
|
||||
@bare.setter
|
||||
def bare(self, value):
|
||||
parsed = JID(value)._jid
|
||||
self._jid = (parsed[0], parsed[1], self._jid[2])
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"""Use the full JID as the string value."""
|
||||
return _format_jid(*self._jid)
|
||||
|
||||
def __repr__(self):
|
||||
"""Use the full JID as the representation."""
|
||||
return self.__str__()
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __eq__(self, other):
|
||||
"""Two JIDs are equal if they have the same full JID value."""
|
||||
if isinstance(other, UnescapedJID):
|
||||
return False
|
||||
|
||||
other = JID(other)
|
||||
return self._jid == other._jid
|
||||
|
||||
# pylint: disable=W0212
|
||||
def __ne__(self, other):
|
||||
"""Two JIDs are considered unequal if they are not equal."""
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
"""Hash a JID based on the string version of its full JID."""
|
||||
return hash(self.__str__())
|
||||
|
||||
def __copy__(self):
|
||||
"""Generate a duplicate JID."""
|
||||
return JID(self)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
"""Generate a duplicate JID."""
|
||||
return JID(deepcopy(str(self), memo))
|
||||
86
slixmpp/plugins/__init__.py
Normal file
86
slixmpp/plugins/__init__.py
Normal 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
slixmpp/plugins/base.py
Normal file
360
slixmpp/plugins/base.py
Normal 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
slixmpp/plugins/gmail_notify.py
Normal file
149
slixmpp/plugins/gmail_notify.py
Normal 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
slixmpp/plugins/google/__init__.py
Normal file
47
slixmpp/plugins/google/__init__.py
Normal 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
slixmpp/plugins/google/auth/__init__.py
Normal file
10
slixmpp/plugins/google/auth/__init__.py
Normal 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
slixmpp/plugins/google/auth/auth.py
Normal file
52
slixmpp/plugins/google/auth/auth.py
Normal 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
slixmpp/plugins/google/auth/stanza.py
Normal file
49
slixmpp/plugins/google/auth/stanza.py
Normal 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
slixmpp/plugins/google/gmail/__init__.py
Normal file
10
slixmpp/plugins/google/gmail/__init__.py
Normal 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
|
||||
96
slixmpp/plugins/google/gmail/notifications.py
Normal file
96
slixmpp/plugins/google/gmail/notifications.py
Normal 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.
|
||||
"""
|
||||
|
||||
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
slixmpp/plugins/google/gmail/stanza.py
Normal file
101
slixmpp/plugins/google/gmail/stanza.py
Normal 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
slixmpp/plugins/google/nosave/__init__.py
Normal file
10
slixmpp/plugins/google/nosave/__init__.py
Normal 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
slixmpp/plugins/google/nosave/nosave.py
Normal file
83
slixmpp/plugins/google/nosave/nosave.py
Normal 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
slixmpp/plugins/google/nosave/stanza.py
Normal file
59
slixmpp/plugins/google/nosave/stanza.py
Normal 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)
|
||||
10
slixmpp/plugins/google/settings/__init__.py
Normal file
10
slixmpp/plugins/google/settings/__init__.py
Normal 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.settings import stanza
|
||||
from slixmpp.plugins.google.settings.settings import GoogleSettings
|
||||
65
slixmpp/plugins/google/settings/settings.py
Normal file
65
slixmpp/plugins/google/settings/settings.py
Normal file
@@ -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
slixmpp/plugins/google/settings/stanza.py
Normal file
110
slixmpp/plugins/google/settings/stanza.py
Normal 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
slixmpp/plugins/xep_0004/__init__.py
Normal file
22
slixmpp/plugins/xep_0004/__init__.py
Normal 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
slixmpp/plugins/xep_0004/dataforms.py
Normal file
57
slixmpp/plugins/xep_0004/dataforms.py
Normal 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)
|
||||
10
slixmpp/plugins/xep_0004/stanza/__init__.py
Normal file
10
slixmpp/plugins/xep_0004/stanza/__init__.py
Normal file
@@ -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
slixmpp/plugins/xep_0004/stanza/field.py
Normal file
183
slixmpp/plugins/xep_0004/stanza/field.py
Normal 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
slixmpp/plugins/xep_0004/stanza/form.py
Normal file
257
slixmpp/plugins/xep_0004/stanza/form.py
Normal 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
slixmpp/plugins/xep_0009/__init__.py
Normal file
20
slixmpp/plugins/xep_0009/__init__.py
Normal 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
slixmpp/plugins/xep_0009/binding.py
Normal file
173
slixmpp/plugins/xep_0009/binding.py
Normal 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
slixmpp/plugins/xep_0009/remote.py
Normal file
742
slixmpp/plugins/xep_0009/remote.py
Normal 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
slixmpp/plugins/xep_0009/rpc.py
Normal file
218
slixmpp/plugins/xep_0009/rpc.py
Normal 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
slixmpp/plugins/xep_0009/stanza/RPC.py
Normal file
64
slixmpp/plugins/xep_0009/stanza/RPC.py
Normal 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)
|
||||
9
slixmpp/plugins/xep_0009/stanza/__init__.py
Normal file
9
slixmpp/plugins/xep_0009/stanza/__init__.py
Normal file
@@ -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
slixmpp/plugins/xep_0012/__init__.py
Normal file
19
slixmpp/plugins/xep_0012/__init__.py
Normal 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
slixmpp/plugins/xep_0012/last_activity.py
Normal file
157
slixmpp/plugins/xep_0012/last_activity.py
Normal 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
slixmpp/plugins/xep_0012/stanza.py
Normal file
32
slixmpp/plugins/xep_0012/stanza.py
Normal 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
slixmpp/plugins/xep_0013/__init__.py
Normal file
15
slixmpp/plugins/xep_0013/__init__.py
Normal 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
slixmpp/plugins/xep_0013/offline.py
Normal file
134
slixmpp/plugins/xep_0013/offline.py
Normal 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
slixmpp/plugins/xep_0013/stanza.py
Normal file
53
slixmpp/plugins/xep_0013/stanza.py
Normal 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
slixmpp/plugins/xep_0016/__init__.py
Normal file
16
slixmpp/plugins/xep_0016/__init__.py
Normal 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
slixmpp/plugins/xep_0016/privacy.py
Normal file
110
slixmpp/plugins/xep_0016/privacy.py
Normal 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
slixmpp/plugins/xep_0016/stanza.py
Normal file
103
slixmpp/plugins/xep_0016/stanza.py
Normal 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
slixmpp/plugins/xep_0020/__init__.py
Normal file
16
slixmpp/plugins/xep_0020/__init__.py
Normal 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)
|
||||
36
slixmpp/plugins/xep_0020/feature_negotiation.py
Normal file
36
slixmpp/plugins/xep_0020/feature_negotiation.py
Normal file
@@ -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
slixmpp/plugins/xep_0020/stanza.py
Normal file
17
slixmpp/plugins/xep_0020/stanza.py
Normal 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
slixmpp/plugins/xep_0027/__init__.py
Normal file
15
slixmpp/plugins/xep_0027/__init__.py
Normal 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
slixmpp/plugins/xep_0027/gpg.py
Normal file
170
slixmpp/plugins/xep_0027/gpg.py
Normal 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
slixmpp/plugins/xep_0027/stanza.py
Normal file
53
slixmpp/plugins/xep_0027/stanza.py
Normal 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
slixmpp/plugins/xep_0030/__init__.py
Normal file
23
slixmpp/plugins/xep_0030/__init__.py
Normal 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
slixmpp/plugins/xep_0030/disco.py
Normal file
740
slixmpp/plugins/xep_0030/disco.py
Normal 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
|
||||
10
slixmpp/plugins/xep_0030/stanza/__init__.py
Normal file
10
slixmpp/plugins/xep_0030/stanza/__init__.py
Normal file
@@ -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
slixmpp/plugins/xep_0030/stanza/info.py
Normal file
276
slixmpp/plugins/xep_0030/stanza/info.py
Normal 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
slixmpp/plugins/xep_0030/stanza/items.py
Normal file
152
slixmpp/plugins/xep_0030/stanza/items.py
Normal 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
slixmpp/plugins/xep_0030/static.py
Normal file
430
slixmpp/plugins/xep_0030/static.py
Normal 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
slixmpp/plugins/xep_0033/__init__.py
Normal file
20
slixmpp/plugins/xep_0033/__init__.py
Normal 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
slixmpp/plugins/xep_0033/addresses.py
Normal file
37
slixmpp/plugins/xep_0033/addresses.py
Normal 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
slixmpp/plugins/xep_0033/stanza.py
Normal file
131
slixmpp/plugins/xep_0033/stanza.py
Normal 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
slixmpp/plugins/xep_0045.py
Normal file
402
slixmpp/plugins/xep_0045.py
Normal 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
slixmpp/plugins/xep_0047/__init__.py
Normal file
21
slixmpp/plugins/xep_0047/__init__.py
Normal 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
slixmpp/plugins/xep_0047/ibb.py
Normal file
215
slixmpp/plugins/xep_0047/ibb.py
Normal 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
slixmpp/plugins/xep_0047/stanza.py
Normal file
67
slixmpp/plugins/xep_0047/stanza.py
Normal 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
slixmpp/plugins/xep_0047/stream.py
Normal file
148
slixmpp/plugins/xep_0047/stream.py
Normal 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
slixmpp/plugins/xep_0048/__init__.py
Normal file
15
slixmpp/plugins/xep_0048/__init__.py
Normal 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
slixmpp/plugins/xep_0048/bookmarks.py
Normal file
76
slixmpp/plugins/xep_0048/bookmarks.py
Normal 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
slixmpp/plugins/xep_0048/stanza.py
Normal file
65
slixmpp/plugins/xep_0048/stanza.py
Normal 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
slixmpp/plugins/xep_0049/__init__.py
Normal file
15
slixmpp/plugins/xep_0049/__init__.py
Normal 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)
|
||||
53
slixmpp/plugins/xep_0049/private_storage.py
Normal file
53
slixmpp/plugins/xep_0049/private_storage.py
Normal 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.
|
||||
"""
|
||||
|
||||
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
slixmpp/plugins/xep_0049/stanza.py
Normal file
17
slixmpp/plugins/xep_0049/stanza.py
Normal 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
slixmpp/plugins/xep_0050/__init__.py
Normal file
19
slixmpp/plugins/xep_0050/__init__.py
Normal 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
slixmpp/plugins/xep_0050/adhoc.py
Normal file
688
slixmpp/plugins/xep_0050/adhoc.py
Normal 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
slixmpp/plugins/xep_0050/stanza.py
Normal file
185
slixmpp/plugins/xep_0050/stanza.py
Normal 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
slixmpp/plugins/xep_0054/__init__.py
Normal file
15
slixmpp/plugins/xep_0054/__init__.py
Normal 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
slixmpp/plugins/xep_0054/stanza.py
Normal file
561
slixmpp/plugins/xep_0054/stanza.py
Normal 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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user