mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-24 03:00:15 +00:00
d8aae88526
Also, need to start working on a replacement for the XEP-30 page in the wiki since the API has changed significantly.
397 lines
16 KiB
Python
397 lines
16 KiB
Python
"""
|
|
SleekXMPP: The Sleek XMPP Library
|
|
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
|
This file is part of SleekXMPP.
|
|
|
|
See the file LICENSE for copying permission.
|
|
"""
|
|
|
|
import logging
|
|
|
|
import sleekxmpp
|
|
from sleekxmpp import Iq
|
|
from sleekxmpp.exceptions import XMPPError
|
|
from sleekxmpp.plugins.base import base_plugin
|
|
from sleekxmpp.xmlstream.handler import Callback
|
|
from sleekxmpp.xmlstream.matcher import StanzaPath
|
|
from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID
|
|
from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class xep_0030(base_plugin):
|
|
|
|
"""
|
|
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>.
|
|
|
|
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.
|
|
|
|
Methods:
|
|
set_node_handler --
|
|
del_node_handler --
|
|
add_identity --
|
|
del_identity --
|
|
add_feature --
|
|
del_feature --
|
|
add_item --
|
|
del_item --
|
|
get_info --
|
|
get_items --
|
|
"""
|
|
|
|
def plugin_init(self):
|
|
self.xep = '0030'
|
|
self.description = 'Service Discovery'
|
|
self.stanza = sleekxmpp.plugins.xep_0030.stanza
|
|
|
|
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._disco_ops = ['get_info', 'set_identities', 'set_features',
|
|
'del_info', 'get_items', 'set_items', 'del_items',
|
|
'add_identity', 'del_identity', 'add_feature',
|
|
'del_feature', 'add_item', 'del_item']
|
|
self.handlers = {}
|
|
for op in self._disco_ops:
|
|
self.handlers[op] = {'global': getattr(self.static, op),
|
|
'jid': {},
|
|
'node': {}}
|
|
|
|
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_info
|
|
del_items
|
|
del_identity
|
|
del_feature
|
|
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.
|
|
"""
|
|
if htype not in self._disco_ops:
|
|
return
|
|
if jid is None and node is None:
|
|
self.handlers[htype]['global'] = handler
|
|
elif node is None:
|
|
self.handlers[htype]['jid'][jid] = handler
|
|
elif jid is None:
|
|
jid = self.xmpp.boundjid.full
|
|
self.handlers[htype]['node'][(jid, node)] = handler
|
|
else:
|
|
self.handlers[htype]['node'][(jid, node)] = handler
|
|
|
|
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.set_node_handler(htype, jid, node, None)
|
|
|
|
def make_static(self, jid=None, node=None, handlers=None):
|
|
"""
|
|
Change all of a node's handlers to the default static
|
|
handlers. Useful for manually overriding the contents
|
|
of a node that would otherwise be handled by a JID level
|
|
or global level dynamic handler.
|
|
|
|
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
|
|
static version. If provided, only these
|
|
handlers will be changed. Otherwise, all
|
|
handlers will use the static version.
|
|
"""
|
|
if handlers is None:
|
|
handlers = self._disco_ops
|
|
for op in handlers:
|
|
self.del_node_handler(op, jid, node)
|
|
self.set_node_handler(op, jid, node, getattr(self.static, op))
|
|
|
|
def get_info(self, jid=None, node=None, local=False, **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.
|
|
|
|
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 Sleek instance and
|
|
no stanzas need to be sent.
|
|
Otherwise, a disco stanza must be sent to the
|
|
remove JID to retrieve the info.
|
|
dfrom -- 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.
|
|
"""
|
|
if local or jid is None:
|
|
log.debug("Looking up local disco#info data " + \
|
|
"for %s, node %s." % (jid, node))
|
|
info = self._run_node_handler('get_info', jid, node, kwargs)
|
|
return self._fix_default_info(info)
|
|
|
|
iq = self.xmpp.Iq()
|
|
iq['from'] = 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', None),
|
|
callback=kwargs.get('callback', None))
|
|
|
|
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.
|
|
|
|
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 Sleek instance and
|
|
no stanzas need to be sent.
|
|
Otherwise, a disco stanza must be sent to the
|
|
remove JID to retrieve the items.
|
|
dfrom -- 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.
|
|
"""
|
|
if local or jid is None:
|
|
return self._run_node_handler('get_items', jid, node, kwargs)
|
|
|
|
iq = self.xmpp.Iq()
|
|
iq['from'] = kwargs.get('dfrom', '')
|
|
iq['to'] = jid
|
|
iq['type'] = 'get'
|
|
iq['disco_items']['node'] = node if node else ''
|
|
return iq.send(timeout=kwargs.get('timeout', None),
|
|
block=kwargs.get('block', None),
|
|
callback=kwargs.get('callback', None))
|
|
|
|
def set_info(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('set_info', jid, node, kwargs)
|
|
|
|
def del_info(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('del_info', jid, node, kwargs)
|
|
|
|
def set_items(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('set_items', jid, node, kwargs)
|
|
|
|
def del_items(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('del_items', jid, node, kwargs)
|
|
|
|
def add_identity(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('add_identity', jid, node, kwargs)
|
|
|
|
def add_feature(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('add_feature', jid, node, kwargs)
|
|
|
|
def del_identity(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('del_identity', jid, node, kwargs)
|
|
|
|
def del_feature(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('del_feature', jid, node, kwargs)
|
|
|
|
def add_item(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('add_item', jid, node, kwargs)
|
|
|
|
def del_item(self, jid=None, node=None, **kwargs):
|
|
self._run_node_handler('del_item', jid, node, kwargs)
|
|
|
|
def _run_node_handler(self, htype, jid, node, data=None):
|
|
"""
|
|
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.
|
|
"""
|
|
if jid is None:
|
|
jid = self.xmpp.boundjid.full
|
|
if node is None:
|
|
node = ''
|
|
|
|
if self.handlers[htype]['node'].get((jid, node), False):
|
|
return self.handlers[htype]['node'][(jid, node)](jid, node, data)
|
|
elif self.handlers[htype]['jid'].get(jid, False):
|
|
return self.handlers[htype]['jid'][jid](jid, node, data)
|
|
elif self.handlers[htype]['global']:
|
|
return self.handlers[htype]['global'](jid, node, data)
|
|
else:
|
|
return None
|
|
|
|
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._run_node_handler('get_info',
|
|
iq['to'].full,
|
|
iq['disco_info']['node'],
|
|
iq)
|
|
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']))
|
|
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._run_node_handler('get_items',
|
|
iq['to'].full,
|
|
iq['disco_items']['node'])
|
|
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, SleekXMPP 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.
|
|
"""
|
|
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 info
|