Rename to slixmpp
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,740 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from slixmpp import Iq
|
||||
from slixmpp.plugins import BasePlugin
|
||||
from slixmpp.xmlstream.handler import Callback
|
||||
from slixmpp.xmlstream.matcher import StanzaPath
|
||||
from slixmpp.xmlstream import register_stanza_plugin, JID
|
||||
from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems
|
||||
from slixmpp.plugins.xep_0030 import StaticDisco
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class XEP_0030(BasePlugin):
|
||||
|
||||
"""
|
||||
XEP-0030: Service Discovery
|
||||
|
||||
Service discovery in XMPP allows entities to discover information about
|
||||
other agents in the network, such as the feature sets supported by a
|
||||
client, or signposts to other, related entities.
|
||||
|
||||
Also see <http://www.xmpp.org/extensions/xep-0030.html>.
|
||||
|
||||
The XEP-0030 plugin works using a hierarchy of dynamic
|
||||
node handlers, ranging from global handlers to specific
|
||||
JID+node handlers. The default set of handlers operate
|
||||
in a static manner, storing disco information in memory.
|
||||
However, custom handlers may use any available backend
|
||||
storage mechanism desired, such as SQLite or Redis.
|
||||
|
||||
Node handler hierarchy:
|
||||
JID | Node | Level
|
||||
---------------------
|
||||
None | None | Global
|
||||
Given | None | All nodes for the JID
|
||||
None | Given | Node on self.xmpp.boundjid
|
||||
Given | Given | A single node
|
||||
|
||||
Stream Handlers:
|
||||
Disco Info -- Any Iq stanze that includes a query with the
|
||||
namespace http://jabber.org/protocol/disco#info.
|
||||
Disco Items -- Any Iq stanze that includes a query with the
|
||||
namespace http://jabber.org/protocol/disco#items.
|
||||
|
||||
Events:
|
||||
disco_info -- Received a disco#info Iq query result.
|
||||
disco_items -- Received a disco#items Iq query result.
|
||||
disco_info_query -- Received a disco#info Iq query request.
|
||||
disco_items_query -- Received a disco#items Iq query request.
|
||||
|
||||
Attributes:
|
||||
stanza -- A reference to the module containing the
|
||||
stanza classes provided by this plugin.
|
||||
static -- Object containing the default set of
|
||||
static node handlers.
|
||||
default_handlers -- A dictionary mapping operations to the default
|
||||
global handler (by default, the static handlers).
|
||||
xmpp -- The main Slixmpp object.
|
||||
|
||||
Methods:
|
||||
set_node_handler -- Assign a handler to a JID/node combination.
|
||||
del_node_handler -- Remove a handler from a JID/node combination.
|
||||
get_info -- Retrieve disco#info data, locally or remote.
|
||||
get_items -- Retrieve disco#items data, locally or remote.
|
||||
set_identities --
|
||||
set_features --
|
||||
set_items --
|
||||
del_items --
|
||||
del_identity --
|
||||
del_feature --
|
||||
del_item --
|
||||
add_identity --
|
||||
add_feature --
|
||||
add_item --
|
||||
"""
|
||||
|
||||
name = 'xep_0030'
|
||||
description = 'XEP-0030: Service Discovery'
|
||||
dependencies = set()
|
||||
stanza = stanza
|
||||
default_config = {
|
||||
'use_cache': True,
|
||||
'wrap_results': False
|
||||
}
|
||||
|
||||
def plugin_init(self):
|
||||
"""
|
||||
Start the XEP-0030 plugin.
|
||||
"""
|
||||
self.xmpp.register_handler(
|
||||
Callback('Disco Info',
|
||||
StanzaPath('iq/disco_info'),
|
||||
self._handle_disco_info))
|
||||
|
||||
self.xmpp.register_handler(
|
||||
Callback('Disco Items',
|
||||
StanzaPath('iq/disco_items'),
|
||||
self._handle_disco_items))
|
||||
|
||||
register_stanza_plugin(Iq, DiscoInfo)
|
||||
register_stanza_plugin(Iq, DiscoItems)
|
||||
|
||||
self.static = StaticDisco(self.xmpp, self)
|
||||
|
||||
self._disco_ops = [
|
||||
'get_info', 'set_info', 'set_identities', 'set_features',
|
||||
'get_items', 'set_items', 'del_items', 'add_identity',
|
||||
'del_identity', 'add_feature', 'del_feature', 'add_item',
|
||||
'del_item', 'del_identities', 'del_features', 'cache_info',
|
||||
'get_cached_info', 'supports', 'has_identity']
|
||||
|
||||
for op in self._disco_ops:
|
||||
self.api.register(getattr(self.static, op), op, default=True)
|
||||
|
||||
def _add_disco_op(self, op, default_handler):
|
||||
self.api.register(default_handler, op)
|
||||
self.api.register_default(default_handler, op)
|
||||
|
||||
def set_node_handler(self, htype, jid=None, node=None, handler=None):
|
||||
"""
|
||||
Add a node handler for the given hierarchy level and
|
||||
handler type.
|
||||
|
||||
Node handlers are ordered in a hierarchy where the
|
||||
most specific handler is executed. Thus, a fallback,
|
||||
global handler can be used for the majority of cases
|
||||
with a few node specific handler that override the
|
||||
global behavior.
|
||||
|
||||
Node handler hierarchy:
|
||||
JID | Node | Level
|
||||
---------------------
|
||||
None | None | Global
|
||||
Given | None | All nodes for the JID
|
||||
None | Given | Node on self.xmpp.boundjid
|
||||
Given | Given | A single node
|
||||
|
||||
Handler types:
|
||||
get_info
|
||||
get_items
|
||||
set_identities
|
||||
set_features
|
||||
set_items
|
||||
del_items
|
||||
del_identities
|
||||
del_identity
|
||||
del_feature
|
||||
del_features
|
||||
del_item
|
||||
add_identity
|
||||
add_feature
|
||||
add_item
|
||||
|
||||
Arguments:
|
||||
htype -- The operation provided by the handler.
|
||||
jid -- The JID the handler applies to. May be narrowed
|
||||
further if a node is given.
|
||||
node -- The particular node the handler is for. If no JID
|
||||
is given, then the self.xmpp.boundjid.full is
|
||||
assumed.
|
||||
handler -- The handler function to use.
|
||||
"""
|
||||
self.api.register(handler, htype, jid, node)
|
||||
|
||||
def del_node_handler(self, htype, jid, node):
|
||||
"""
|
||||
Remove a handler type for a JID and node combination.
|
||||
|
||||
The next handler in the hierarchy will be used if one
|
||||
exists. If removing the global handler, make sure that
|
||||
other handlers exist to process existing nodes.
|
||||
|
||||
Node handler hierarchy:
|
||||
JID | Node | Level
|
||||
---------------------
|
||||
None | None | Global
|
||||
Given | None | All nodes for the JID
|
||||
None | Given | Node on self.xmpp.boundjid
|
||||
Given | Given | A single node
|
||||
|
||||
Arguments:
|
||||
htype -- The type of handler to remove.
|
||||
jid -- The JID from which to remove the handler.
|
||||
node -- The node from which to remove the handler.
|
||||
"""
|
||||
self.api.unregister(htype, jid, node)
|
||||
|
||||
def restore_defaults(self, jid=None, node=None, handlers=None):
|
||||
"""
|
||||
Change all or some of a node's handlers to the default
|
||||
handlers. Useful for manually overriding the contents
|
||||
of a node that would otherwise be handled by a JID level
|
||||
or global level dynamic handler.
|
||||
|
||||
The default is to use the built-in static handlers, but that
|
||||
may be changed by modifying self.default_handlers.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID owning the node to modify.
|
||||
node -- The node to change to using static handlers.
|
||||
handlers -- Optional list of handlers to change to the
|
||||
default version. If provided, only these
|
||||
handlers will be changed. Otherwise, all
|
||||
handlers will use the default version.
|
||||
"""
|
||||
if handlers is None:
|
||||
handlers = self._disco_ops
|
||||
for op in handlers:
|
||||
self.api.restore_default(op, jid, node)
|
||||
|
||||
def supports(self, jid=None, node=None, feature=None, local=False,
|
||||
cached=True, ifrom=None):
|
||||
"""
|
||||
Check if a JID supports a given feature.
|
||||
|
||||
Return values:
|
||||
True -- The feature is supported
|
||||
False -- The feature is not listed as supported
|
||||
None -- Nothing could be found due to a timeout
|
||||
|
||||
Arguments:
|
||||
jid -- Request info from this JID.
|
||||
node -- The particular node to query.
|
||||
feature -- The name of the feature to check.
|
||||
local -- If true, then the query is for a JID/node
|
||||
combination handled by this Slixmpp instance and
|
||||
no stanzas need to be sent.
|
||||
Otherwise, a disco stanza must be sent to the
|
||||
remove JID to retrieve the info.
|
||||
cached -- If true, then look for the disco info data from
|
||||
the local cache system. If no results are found,
|
||||
send the query as usual. The self.use_cache
|
||||
setting must be set to true for this option to
|
||||
be useful. If set to false, then the cache will
|
||||
be skipped, even if a result has already been
|
||||
cached. Defaults to false.
|
||||
ifrom -- Specifiy the sender's JID.
|
||||
"""
|
||||
data = {'feature': feature,
|
||||
'local': local,
|
||||
'cached': cached}
|
||||
return self.api['supports'](jid, node, ifrom, data)
|
||||
|
||||
def has_identity(self, jid=None, node=None, category=None, itype=None,
|
||||
lang=None, local=False, cached=True, ifrom=None):
|
||||
"""
|
||||
Check if a JID provides a given identity.
|
||||
|
||||
Return values:
|
||||
True -- The identity is provided
|
||||
False -- The identity is not listed
|
||||
None -- Nothing could be found due to a timeout
|
||||
|
||||
Arguments:
|
||||
jid -- Request info from this JID.
|
||||
node -- The particular node to query.
|
||||
category -- The category of the identity to check.
|
||||
itype -- The type of the identity to check.
|
||||
lang -- The language of the identity to check.
|
||||
local -- If true, then the query is for a JID/node
|
||||
combination handled by this Slixmpp instance and
|
||||
no stanzas need to be sent.
|
||||
Otherwise, a disco stanza must be sent to the
|
||||
remove JID to retrieve the info.
|
||||
cached -- If true, then look for the disco info data from
|
||||
the local cache system. If no results are found,
|
||||
send the query as usual. The self.use_cache
|
||||
setting must be set to true for this option to
|
||||
be useful. If set to false, then the cache will
|
||||
be skipped, even if a result has already been
|
||||
cached. Defaults to false.
|
||||
ifrom -- Specifiy the sender's JID.
|
||||
"""
|
||||
data = {'category': category,
|
||||
'itype': itype,
|
||||
'lang': lang,
|
||||
'local': local,
|
||||
'cached': cached}
|
||||
return self.api['has_identity'](jid, node, ifrom, data)
|
||||
|
||||
def get_info(self, jid=None, node=None, local=None,
|
||||
cached=None, **kwargs):
|
||||
"""
|
||||
Retrieve the disco#info results from a given JID/node combination.
|
||||
|
||||
Info may be retrieved from both local resources and remote agents;
|
||||
the local parameter indicates if the information should be gathered
|
||||
by executing the local node handlers, or if a disco#info stanza
|
||||
must be generated and sent.
|
||||
|
||||
If requesting items from a local JID/node, then only a DiscoInfo
|
||||
stanza will be returned. Otherwise, an Iq stanza will be returned.
|
||||
|
||||
Arguments:
|
||||
jid -- Request info from this JID.
|
||||
node -- The particular node to query.
|
||||
local -- If true, then the query is for a JID/node
|
||||
combination handled by this Slixmpp instance and
|
||||
no stanzas need to be sent.
|
||||
Otherwise, a disco stanza must be sent to the
|
||||
remove JID to retrieve the info.
|
||||
cached -- If true, then look for the disco info data from
|
||||
the local cache system. If no results are found,
|
||||
send the query as usual. The self.use_cache
|
||||
setting must be set to true for this option to
|
||||
be useful. If set to false, then the cache will
|
||||
be skipped, even if a result has already been
|
||||
cached. Defaults to false.
|
||||
ifrom -- Specifiy the sender's JID.
|
||||
block -- If true, block and wait for the stanzas' reply.
|
||||
timeout -- The time in seconds to block while waiting for
|
||||
a reply. If None, then wait indefinitely. The
|
||||
timeout value is only used when block=True.
|
||||
callback -- Optional callback to execute when a reply is
|
||||
received instead of blocking and waiting for
|
||||
the reply.
|
||||
timeout_callback -- Optional callback to execute when no result
|
||||
has been received in timeout seconds.
|
||||
"""
|
||||
if local is None:
|
||||
if jid is not None and not isinstance(jid, JID):
|
||||
jid = JID(jid)
|
||||
if self.xmpp.is_component:
|
||||
if jid.domain == self.xmpp.boundjid.domain:
|
||||
local = True
|
||||
else:
|
||||
if str(jid) == str(self.xmpp.boundjid):
|
||||
local = True
|
||||
jid = jid.full
|
||||
elif jid in (None, ''):
|
||||
local = True
|
||||
|
||||
if local:
|
||||
log.debug("Looking up local disco#info data " + \
|
||||
"for %s, node %s.", jid, node)
|
||||
info = self.api['get_info'](jid, node,
|
||||
kwargs.get('ifrom', None),
|
||||
kwargs)
|
||||
info = self._fix_default_info(info)
|
||||
return self._wrap(kwargs.get('ifrom', None), jid, info)
|
||||
|
||||
if cached:
|
||||
log.debug("Looking up cached disco#info data " + \
|
||||
"for %s, node %s.", jid, node)
|
||||
info = self.api['get_cached_info'](jid, node,
|
||||
kwargs.get('ifrom', None),
|
||||
kwargs)
|
||||
if info is not None:
|
||||
return self._wrap(kwargs.get('ifrom', None), jid, info)
|
||||
|
||||
iq = self.xmpp.Iq()
|
||||
# Check dfrom parameter for backwards compatibility
|
||||
iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
|
||||
iq['to'] = jid
|
||||
iq['type'] = 'get'
|
||||
iq['disco_info']['node'] = node if node else ''
|
||||
return iq.send(timeout=kwargs.get('timeout', None),
|
||||
block=kwargs.get('block', True),
|
||||
callback=kwargs.get('callback', None),
|
||||
timeout_callback=kwargs.get('timeout_callback', None))
|
||||
|
||||
def set_info(self, jid=None, node=None, info=None):
|
||||
"""
|
||||
Set the disco#info data for a JID/node based on an existing
|
||||
disco#info stanza.
|
||||
"""
|
||||
if isinstance(info, Iq):
|
||||
info = info['disco_info']
|
||||
self.api['set_info'](jid, node, None, info)
|
||||
|
||||
def get_items(self, jid=None, node=None, local=False, **kwargs):
|
||||
"""
|
||||
Retrieve the disco#items results from a given JID/node combination.
|
||||
|
||||
Items may be retrieved from both local resources and remote agents;
|
||||
the local parameter indicates if the items should be gathered by
|
||||
executing the local node handlers, or if a disco#items stanza must
|
||||
be generated and sent.
|
||||
|
||||
If requesting items from a local JID/node, then only a DiscoItems
|
||||
stanza will be returned. Otherwise, an Iq stanza will be returned.
|
||||
|
||||
Arguments:
|
||||
jid -- Request info from this JID.
|
||||
node -- The particular node to query.
|
||||
local -- If true, then the query is for a JID/node
|
||||
combination handled by this Slixmpp instance and
|
||||
no stanzas need to be sent.
|
||||
Otherwise, a disco stanza must be sent to the
|
||||
remove JID to retrieve the items.
|
||||
ifrom -- Specifiy the sender's JID.
|
||||
block -- If true, block and wait for the stanzas' reply.
|
||||
timeout -- The time in seconds to block while waiting for
|
||||
a reply. If None, then wait indefinitely.
|
||||
callback -- Optional callback to execute when a reply is
|
||||
received instead of blocking and waiting for
|
||||
the reply.
|
||||
iterator -- If True, return a result set iterator using
|
||||
the XEP-0059 plugin, if the plugin is loaded.
|
||||
Otherwise the parameter is ignored.
|
||||
timeout_callback -- Optional callback to execute when no result
|
||||
has been received in timeout seconds.
|
||||
"""
|
||||
if local or local is None and jid is None:
|
||||
items = self.api['get_items'](jid, node,
|
||||
kwargs.get('ifrom', None),
|
||||
kwargs)
|
||||
return self._wrap(kwargs.get('ifrom', None), jid, items)
|
||||
|
||||
iq = self.xmpp.Iq()
|
||||
# Check dfrom parameter for backwards compatibility
|
||||
iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
|
||||
iq['to'] = jid
|
||||
iq['type'] = 'get'
|
||||
iq['disco_items']['node'] = node if node else ''
|
||||
if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
|
||||
return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
|
||||
else:
|
||||
return iq.send(timeout=kwargs.get('timeout', None),
|
||||
block=kwargs.get('block', True),
|
||||
callback=kwargs.get('callback', None),
|
||||
timeout_callback=kwargs.get('timeout_callback', None))
|
||||
|
||||
def set_items(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Set or replace all items for the specified JID/node combination.
|
||||
|
||||
The given items must be in a list or set where each item is a
|
||||
tuple of the form: (jid, node, name).
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- Optional node to modify.
|
||||
items -- A series of items in tuple format.
|
||||
"""
|
||||
self.api['set_items'](jid, node, None, kwargs)
|
||||
|
||||
def del_items(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Remove all items from the given JID/node combination.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- Optional node to modify.
|
||||
"""
|
||||
self.api['del_items'](jid, node, None, kwargs)
|
||||
|
||||
def add_item(self, jid='', name='', node=None, subnode='', ijid=None):
|
||||
"""
|
||||
Add a new item element to the given JID/node combination.
|
||||
|
||||
Each item is required to have a JID, but may also specify
|
||||
a node value to reference non-addressable entities.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID for the item.
|
||||
name -- Optional name for the item.
|
||||
node -- The node to modify.
|
||||
subnode -- Optional node for the item.
|
||||
ijid -- The JID to modify.
|
||||
"""
|
||||
if not jid:
|
||||
jid = self.xmpp.boundjid.full
|
||||
kwargs = {'ijid': jid,
|
||||
'name': name,
|
||||
'inode': subnode}
|
||||
self.api['add_item'](ijid, node, None, kwargs)
|
||||
|
||||
def del_item(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Remove a single item from the given JID/node combination.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
ijid -- The item's JID.
|
||||
inode -- The item's node.
|
||||
"""
|
||||
self.api['del_item'](jid, node, None, kwargs)
|
||||
|
||||
def add_identity(self, category='', itype='', name='',
|
||||
node=None, jid=None, lang=None):
|
||||
"""
|
||||
Add a new identity to the given JID/node combination.
|
||||
|
||||
Each identity must be unique in terms of all four identity
|
||||
components: category, type, name, and language.
|
||||
|
||||
Multiple, identical category/type pairs are allowed only
|
||||
if the xml:lang values are different. Likewise, multiple
|
||||
category/type/xml:lang pairs are allowed so long as the
|
||||
names are different. A category and type is always required.
|
||||
|
||||
Arguments:
|
||||
category -- The identity's category.
|
||||
itype -- The identity's type.
|
||||
name -- Optional name for the identity.
|
||||
lang -- Optional two-letter language code.
|
||||
node -- The node to modify.
|
||||
jid -- The JID to modify.
|
||||
"""
|
||||
kwargs = {'category': category,
|
||||
'itype': itype,
|
||||
'name': name,
|
||||
'lang': lang}
|
||||
self.api['add_identity'](jid, node, None, kwargs)
|
||||
|
||||
def add_feature(self, feature, node=None, jid=None):
|
||||
"""
|
||||
Add a feature to a JID/node combination.
|
||||
|
||||
Arguments:
|
||||
feature -- The namespace of the supported feature.
|
||||
node -- The node to modify.
|
||||
jid -- The JID to modify.
|
||||
"""
|
||||
kwargs = {'feature': feature}
|
||||
self.api['add_feature'](jid, node, None, kwargs)
|
||||
|
||||
def del_identity(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Remove an identity from the given JID/node combination.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
category -- The identity's category.
|
||||
itype -- The identity's type value.
|
||||
name -- Optional, human readable name for the identity.
|
||||
lang -- Optional, the identity's xml:lang value.
|
||||
"""
|
||||
self.api['del_identity'](jid, node, None, kwargs)
|
||||
|
||||
def del_feature(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Remove a feature from a given JID/node combination.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
feature -- The feature's namespace.
|
||||
"""
|
||||
self.api['del_feature'](jid, node, None, kwargs)
|
||||
|
||||
def set_identities(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Add or replace all identities for the given JID/node combination.
|
||||
|
||||
The identities must be in a set where each identity is a tuple
|
||||
of the form: (category, type, lang, name)
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
identities -- A set of identities in tuple form.
|
||||
lang -- Optional, xml:lang value.
|
||||
"""
|
||||
self.api['set_identities'](jid, node, None, kwargs)
|
||||
|
||||
def del_identities(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Remove all identities for a JID/node combination.
|
||||
|
||||
If a language is specified, only identities using that
|
||||
language will be removed.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
lang -- Optional. If given, only remove identities
|
||||
using this xml:lang value.
|
||||
"""
|
||||
self.api['del_identities'](jid, node, None, kwargs)
|
||||
|
||||
def set_features(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Add or replace the set of supported features
|
||||
for a JID/node combination.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
features -- The new set of supported features.
|
||||
"""
|
||||
self.api['set_features'](jid, node, None, kwargs)
|
||||
|
||||
def del_features(self, jid=None, node=None, **kwargs):
|
||||
"""
|
||||
Remove all features from a JID/node combination.
|
||||
|
||||
Arguments:
|
||||
jid -- The JID to modify.
|
||||
node -- The node to modify.
|
||||
"""
|
||||
self.api['del_features'](jid, node, None, kwargs)
|
||||
|
||||
def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}):
|
||||
"""
|
||||
Execute the most specific node handler for the given
|
||||
JID/node combination.
|
||||
|
||||
Arguments:
|
||||
htype -- The handler type to execute.
|
||||
jid -- The JID requested.
|
||||
node -- The node requested.
|
||||
data -- Optional, custom data to pass to the handler.
|
||||
"""
|
||||
return self.api[htype](jid, node, ifrom, data)
|
||||
|
||||
def _handle_disco_info(self, iq):
|
||||
"""
|
||||
Process an incoming disco#info stanza. If it is a get
|
||||
request, find and return the appropriate identities
|
||||
and features. If it is an info result, fire the
|
||||
disco_info event.
|
||||
|
||||
Arguments:
|
||||
iq -- The incoming disco#items stanza.
|
||||
"""
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Received disco info query from " + \
|
||||
"<%s> to <%s>.", iq['from'], iq['to'])
|
||||
info = self.api['get_info'](iq['to'],
|
||||
iq['disco_info']['node'],
|
||||
iq['from'],
|
||||
iq)
|
||||
if isinstance(info, Iq):
|
||||
info['id'] = iq['id']
|
||||
info.send()
|
||||
else:
|
||||
iq.reply()
|
||||
if info:
|
||||
info = self._fix_default_info(info)
|
||||
iq.set_payload(info.xml)
|
||||
iq.send()
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Received disco info result from " + \
|
||||
"<%s> to <%s>.", iq['from'], iq['to'])
|
||||
if self.use_cache:
|
||||
log.debug("Caching disco info result from " \
|
||||
"<%s> to <%s>.", iq['from'], iq['to'])
|
||||
if self.xmpp.is_component:
|
||||
ito = iq['to'].full
|
||||
else:
|
||||
ito = None
|
||||
self.api['cache_info'](iq['from'],
|
||||
iq['disco_info']['node'],
|
||||
ito,
|
||||
iq)
|
||||
self.xmpp.event('disco_info', iq)
|
||||
|
||||
def _handle_disco_items(self, iq):
|
||||
"""
|
||||
Process an incoming disco#items stanza. If it is a get
|
||||
request, find and return the appropriate items. If it
|
||||
is an items result, fire the disco_items event.
|
||||
|
||||
Arguments:
|
||||
iq -- The incoming disco#items stanza.
|
||||
"""
|
||||
if iq['type'] == 'get':
|
||||
log.debug("Received disco items query from " + \
|
||||
"<%s> to <%s>.", iq['from'], iq['to'])
|
||||
items = self.api['get_items'](iq['to'],
|
||||
iq['disco_items']['node'],
|
||||
iq['from'],
|
||||
iq)
|
||||
if isinstance(items, Iq):
|
||||
items.send()
|
||||
else:
|
||||
iq.reply()
|
||||
if items:
|
||||
iq.set_payload(items.xml)
|
||||
iq.send()
|
||||
elif iq['type'] == 'result':
|
||||
log.debug("Received disco items result from " + \
|
||||
"%s to %s.", iq['from'], iq['to'])
|
||||
self.xmpp.event('disco_items', iq)
|
||||
|
||||
def _fix_default_info(self, info):
|
||||
"""
|
||||
Disco#info results for a JID are required to include at least
|
||||
one identity and feature. As a default, if no other identity is
|
||||
provided, Slixmpp will use either the generic component or the
|
||||
bot client identity. A the standard disco#info feature will also be
|
||||
added if no features are provided.
|
||||
|
||||
Arguments:
|
||||
info -- The disco#info quest (not the full Iq stanza) to modify.
|
||||
"""
|
||||
result = info
|
||||
if isinstance(info, Iq):
|
||||
info = info['disco_info']
|
||||
if not info['node']:
|
||||
if not info['identities']:
|
||||
if self.xmpp.is_component:
|
||||
log.debug("No identity found for this entity. " + \
|
||||
"Using default component identity.")
|
||||
info.add_identity('component', 'generic')
|
||||
else:
|
||||
log.debug("No identity found for this entity. " + \
|
||||
"Using default client identity.")
|
||||
info.add_identity('client', 'bot')
|
||||
if not info['features']:
|
||||
log.debug("No features found for this entity. " + \
|
||||
"Using default disco#info feature.")
|
||||
info.add_feature(info.namespace)
|
||||
return result
|
||||
|
||||
def _wrap(self, ito, ifrom, payload, force=False):
|
||||
"""
|
||||
Ensure that results are wrapped in an Iq stanza
|
||||
if self.wrap_results has been set to True.
|
||||
|
||||
Arguments:
|
||||
ito -- The JID to use as the 'to' value
|
||||
ifrom -- The JID to use as the 'from' value
|
||||
payload -- The disco data to wrap
|
||||
force -- Force wrapping, regardless of self.wrap_results
|
||||
"""
|
||||
if (force or self.wrap_results) and not isinstance(payload, Iq):
|
||||
iq = self.xmpp.Iq()
|
||||
# Since we're simulating a result, we have to treat
|
||||
# the 'from' and 'to' values opposite the normal way.
|
||||
iq['to'] = self.xmpp.boundjid if ito is None else ito
|
||||
iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
|
||||
iq['type'] = 'result'
|
||||
iq.append(payload)
|
||||
return iq
|
||||
return payload
|
||||
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Slixmpp: The Slick XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of Slixmpp.
|
||||
|
||||
See the file LICENSE for copying permission.
|
||||
"""
|
||||
|
||||
from slixmpp.plugins.xep_0030.stanza.info import DiscoInfo
|
||||
from slixmpp.plugins.xep_0030.stanza.items import DiscoItems
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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']
|
||||
Reference in New Issue
Block a user