From 488f7ed88691d9f7fa756a28702f6bee43d6a260 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 30 Mar 2012 23:02:48 -0700 Subject: Begin experiment with a centralized API callback registry. The API registry generalizes the node handler system from the xep_0030 plugin so that other plugins can use it. --- sleekxmpp/api.py | 191 +++++++++++++++++++++++++++ sleekxmpp/basexmpp.py | 17 +++ sleekxmpp/plugins/base.py | 1 + sleekxmpp/plugins/xep_0030/disco.py | 125 ++++++------------ sleekxmpp/plugins/xep_0115/caps.py | 14 +- sleekxmpp/plugins/xep_0128/extended_disco.py | 9 +- 6 files changed, 260 insertions(+), 97 deletions(-) create mode 100644 sleekxmpp/api.py diff --git a/sleekxmpp/api.py b/sleekxmpp/api.py new file mode 100644 index 00000000..9d8ec201 --- /dev/null +++ b/sleekxmpp/api.py @@ -0,0 +1,191 @@ +from sleekxmpp.xmlstream import JID + + +class APIWrapper(object): + + def __init__(self, api, name): + self.api = api + self.name = 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 curried_handler(handler, op, jid=None, node=None): + register = getattr(self.api, attr) + return register(handler, self.name, op, jid, node) + return curried_handler + elif attr == 'register_default': + def curried_handler(handler, op, jid=None, node=None): + return getattr(self.api, attr)(handler, self.name, op) + return curried_handler + elif attr in ('run', 'restore_default', 'unregister'): + def curried_handler(*args, **kwargs): + return getattr(self.api, attr)(self.name, *args, **kwargs) + return curried_handler + return None + + def __getitem__(self, attr): + def curried_handler(jid=None, node=None, ifrom=None, args=None): + return self.api.run(self.name, attr, jid, node, ifrom, args) + return curried_handler + + +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 jid in (None, ''): + jid = self.xmpp.boundjid + if jid and not isinstance(jid, JID): + jid = JID(jid) + + 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', True): + jid = jid.bare + else: + jid = jid.full + + 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 + + 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) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index dc1f6b94..5fcfbf6d 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -19,6 +19,7 @@ import logging import sleekxmpp from sleekxmpp import plugins, features, roster +from sleekxmpp.api import APIRegistry from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.stanza import Message, Presence, Iq, StreamError @@ -97,6 +98,22 @@ class BaseXMPP(XMLStream): #: ``'to'`` and ``'from'`` JIDs of stanzas. self.is_component = 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. diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index f08023ba..ccea5ce4 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -269,6 +269,7 @@ class BasePlugin(object): def __init__(self, xmpp, config=None): self.xmpp = 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. diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index a5e8fd1c..76824e1c 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -118,16 +118,13 @@ class XEP_0030(BasePlugin): 'del_item', 'del_identities', 'del_features', 'cache_info', 'get_cached_info', 'supports', 'has_identity'] - self.default_handlers = {} - self._handlers = {} for op in self._disco_ops: - self._add_disco_op(op, getattr(self.static, op)) + self.api.register(getattr(self.static, op), op) + self.api.register_default(getattr(self.static, op), op) def _add_disco_op(self, op, default_handler): - self.default_handlers[op] = default_handler - self._handlers[op] = {'global': default_handler, - 'jid': {}, - 'node': {}} + 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): """ @@ -173,20 +170,7 @@ class XEP_0030(BasePlugin): 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: - if self.xmpp.is_component: - jid = self.xmpp.boundjid.full - else: - jid = self.xmpp.boundjid.bare - self._handlers[htype]['node'][(jid, node)] = handler - else: - self._handlers[htype]['node'][(jid, node)] = handler + self.api.register(handler, htype, jid, node) def del_node_handler(self, htype, jid, node): """ @@ -209,7 +193,7 @@ class XEP_0030(BasePlugin): 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) + self.api.unregister(htype, jid, node) def restore_defaults(self, jid=None, node=None, handlers=None): """ @@ -232,8 +216,7 @@ class XEP_0030(BasePlugin): 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, self.default_handlers[op]) + self.api.restore_default(op, jid, node) def supports(self, jid=None, node=None, feature=None, local=False, cached=True, ifrom=None): @@ -266,7 +249,7 @@ class XEP_0030(BasePlugin): data = {'feature': feature, 'local': local, 'cached': cached} - return self._run_node_handler('supports', jid, node, ifrom, data) + 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): @@ -303,7 +286,7 @@ class XEP_0030(BasePlugin): 'lang': lang, 'local': local, 'cached': cached} - return self._run_node_handler('has_identity', jid, node, ifrom, data) + return self.api['has_identity'](jid, node, ifrom, data) def get_info(self, jid=None, node=None, local=False, cached=None, **kwargs): @@ -355,16 +338,18 @@ class XEP_0030(BasePlugin): if local or jid in (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.get('ifrom', None), kwargs) + 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._run_node_handler('get_cached_info', - jid, node, kwargs.get('ifrom', None), kwargs) + 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) @@ -385,7 +370,7 @@ class XEP_0030(BasePlugin): """ if isinstance(info, Iq): info = info['disco_info'] - self._run_node_handler('set_info', jid, node, None, info) + self.api['set_info'](jid, node, None, info) def get_items(self, jid=None, node=None, local=False, **kwargs): """ @@ -419,8 +404,9 @@ class XEP_0030(BasePlugin): Otherwise the parameter is ignored. """ if local or jid is None: - items = self._run_node_handler('get_items', - jid, node, kwargs.get('ifrom', None), kwargs) + 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() @@ -448,7 +434,7 @@ class XEP_0030(BasePlugin): node -- Optional node to modify. items -- A series of items in tuple format. """ - self._run_node_handler('set_items', jid, node, None, kwargs) + self.api['set_items'](jid, node, None, kwargs) def del_items(self, jid=None, node=None, **kwargs): """ @@ -458,7 +444,7 @@ class XEP_0030(BasePlugin): jid -- The JID to modify. node -- Optional node to modify. """ - self._run_node_handler('del_items', jid, node, None, kwargs) + self.api['del_items'](jid, node, None, kwargs) def add_item(self, jid='', name='', node=None, subnode='', ijid=None): """ @@ -479,7 +465,7 @@ class XEP_0030(BasePlugin): kwargs = {'ijid': jid, 'name': name, 'inode': subnode} - self._run_node_handler('add_item', ijid, node, None, kwargs) + self.api['add_item'](ijid, node, None, kwargs) def del_item(self, jid=None, node=None, **kwargs): """ @@ -491,7 +477,7 @@ class XEP_0030(BasePlugin): ijid -- The item's JID. inode -- The item's node. """ - self._run_node_handler('del_item', jid, node, None, kwargs) + self.api['del_item'](jid, node, None, kwargs) def add_identity(self, category='', itype='', name='', node=None, jid=None, lang=None): @@ -518,7 +504,7 @@ class XEP_0030(BasePlugin): 'itype': itype, 'name': name, 'lang': lang} - self._run_node_handler('add_identity', jid, node, None, kwargs) + self.api['add_identity'](jid, node, None, kwargs) def add_feature(self, feature, node=None, jid=None): """ @@ -530,7 +516,7 @@ class XEP_0030(BasePlugin): jid -- The JID to modify. """ kwargs = {'feature': feature} - self._run_node_handler('add_feature', jid, node, None, kwargs) + self.api['add_feature'](jid, node, None, kwargs) def del_identity(self, jid=None, node=None, **kwargs): """ @@ -544,7 +530,7 @@ class XEP_0030(BasePlugin): name -- Optional, human readable name for the identity. lang -- Optional, the identity's xml:lang value. """ - self._run_node_handler('del_identity', jid, node, None, kwargs) + self.api['del_identity'](jid, node, None, kwargs) def del_feature(self, jid=None, node=None, **kwargs): """ @@ -555,7 +541,7 @@ class XEP_0030(BasePlugin): node -- The node to modify. feature -- The feature's namespace. """ - self._run_node_handler('del_feature', jid, node, None, kwargs) + self.api['del_feature'](jid, node, None, kwargs) def set_identities(self, jid=None, node=None, **kwargs): """ @@ -570,7 +556,7 @@ class XEP_0030(BasePlugin): identities -- A set of identities in tuple form. lang -- Optional, xml:lang value. """ - self._run_node_handler('set_identities', jid, node, None, kwargs) + self.api['set_identities'](jid, node, None, kwargs) def del_identities(self, jid=None, node=None, **kwargs): """ @@ -585,7 +571,7 @@ class XEP_0030(BasePlugin): lang -- Optional. If given, only remove identities using this xml:lang value. """ - self._run_node_handler('del_identities', jid, node, None, kwargs) + self.api['del_identities'](jid, node, None, kwargs) def set_features(self, jid=None, node=None, **kwargs): """ @@ -597,7 +583,7 @@ class XEP_0030(BasePlugin): node -- The node to modify. features -- The new set of supported features. """ - self._run_node_handler('set_features', jid, node, None, kwargs) + self.api['set_features'](jid, node, None, kwargs) def del_features(self, jid=None, node=None, **kwargs): """ @@ -607,7 +593,7 @@ class XEP_0030(BasePlugin): jid -- The JID to modify. node -- The node to modify. """ - self._run_node_handler('del_features', jid, node, None, kwargs) + self.api['del_features'](jid, node, None, kwargs) def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}): """ @@ -620,39 +606,7 @@ class XEP_0030(BasePlugin): node -- The node requested. data -- Optional, custom data to pass to the handler. """ - if isinstance(jid, JID): - jid = jid.full - - if jid in (None, ''): - if self.xmpp.is_component: - jid = self.xmpp.boundjid.full - else: - jid = self.xmpp.boundjid.bare - if node is None: - node = '' - - try: - args = (jid, node, ifrom, data) - if self._handlers[htype]['node'].get((jid, node), False): - return self._handlers[htype]['node'][(jid, node)](*args) - elif self._handlers[htype]['jid'].get(jid, False): - return self._handlers[htype]['jid'][jid](*args) - elif self._handlers[htype]['global']: - return self._handlers[htype]['global'](*args) - else: - return None - except TypeError: - # To preserve backward compatibility, drop the ifrom parameter - # for existing handlers that don't understand it. - args = (jid, node, data) - if self._handlers[htype]['node'].get((jid, node), False): - return self._handlers[htype]['node'][(jid, node)](*args) - elif self._handlers[htype]['jid'].get(jid, False): - return self._handlers[htype]['jid'][jid](*args) - elif self._handlers[htype]['global']: - return self._handlers[htype]['global'](*args) - else: - return None + return self.api[htype](jid, node, ifrom, data) def _handle_disco_info(self, iq): """ @@ -671,11 +625,10 @@ class XEP_0030(BasePlugin): jid = iq['to'].full else: jid = iq['to'].bare - info = self._run_node_handler('get_info', - jid, - iq['disco_info']['node'], - iq['from'], - iq) + info = self.api['get_info'](jid, + iq['disco_info']['node'], + iq['from'], + iq) if isinstance(info, Iq): info.send() else: @@ -694,8 +647,7 @@ class XEP_0030(BasePlugin): ito = iq['to'].full else: ito = None - self._run_node_handler('cache_info', - iq['from'].full, + self.api['cache_info'](iq['from'].full, iq['disco_info']['node'], ito, iq) @@ -717,8 +669,7 @@ class XEP_0030(BasePlugin): jid = iq['to'].full else: jid = iq['to'].bare - items = self._run_node_handler('get_items', - jid, + items = self.api['get_items'](jid, iq['disco_items']['node'], iq['from'].full, iq) diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index 3aa0f70f..fee86f5b 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -78,7 +78,9 @@ class XEP_0115(BasePlugin): self.static = StaticCaps(self.xmpp, disco.static) for op in self._disco_ops: - disco._add_disco_op(op, getattr(self.static, op)) + self.api.register(getattr(self.static, op), 'xep_0115', op) + self.api.register_default(getattr(self.static, op), + 'xep_0115', op) self._run_node_handler = disco._run_node_handler @@ -279,19 +281,19 @@ class XEP_0115(BasePlugin): jid = self.xmpp.boundjid.full if isinstance(jid, JID): jid = jid.full - return self._run_node_handler('get_verstring', jid) + return self.api['get_verstring'](jid) def assign_verstring(self, jid=None, verstring=None): if jid in (None, ''): jid = self.xmpp.boundjid.full if isinstance(jid, JID): jid = jid.full - return self._run_node_handler('assign_verstring', jid, - data={'verstring': verstring}) + return self.api['assign_verstring'](jid, args={ + 'verstring': verstring}) def cache_caps(self, verstring=None, info=None): data = {'verstring': verstring, 'info': info} - return self._run_node_handler('cache_caps', None, None, data=data) + return self.api['cache_caps'](args=data) def get_caps(self, jid=None, verstring=None): if verstring is None: @@ -302,4 +304,4 @@ class XEP_0115(BasePlugin): if isinstance(jid, JID): jid = jid.full data = {'verstring': verstring} - return self._run_node_handler('get_caps', jid, None, None, data) + return self.api['get_caps'](jid, args=data) diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py index d49741de..93f8e8f0 100644 --- a/sleekxmpp/plugins/xep_0128/extended_disco.py +++ b/sleekxmpp/plugins/xep_0128/extended_disco.py @@ -61,7 +61,8 @@ class XEP_0128(BasePlugin): self.disco.del_extended_info = self.del_extended_info for op in self._disco_ops: - self.disco._add_disco_op(op, getattr(self.static, op)) + self.api.register(getattr(self.static, op), op) + self.api.register_default(getattr(self.static, op), op) def set_extended_info(self, jid=None, node=None, **kwargs): """ @@ -76,7 +77,7 @@ class XEP_0128(BasePlugin): as extended information, replacing any existing extensions. """ - self.disco._run_node_handler('set_extended_info', jid, node, None, kwargs) + self.api['set_extended_info'](jid, node, None, kwargs) def add_extended_info(self, jid=None, node=None, **kwargs): """ @@ -88,7 +89,7 @@ class XEP_0128(BasePlugin): data -- Either a form, or a list of forms to add as extended information. """ - self.disco._run_node_handler('add_extended_info', jid, node, None, kwargs) + self.api['add_extended_info'](jid, node, None, kwargs) def del_extended_info(self, jid=None, node=None, **kwargs): """ @@ -98,4 +99,4 @@ class XEP_0128(BasePlugin): jid -- The JID to modify. node -- The node to modify. """ - self.disco._run_node_handler('del_extended_info', jid, node, None, kwargs) + self.api['del_extended_info'](jid, node, None, kwargs) -- cgit v1.2.3