summaryrefslogtreecommitdiff
path: root/sleekxmpp/plugins/xep_0030/disco.py
blob: c323ba7c868afb8f466b2a269efb1347eaeec965 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
"""
    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

    Stream Handlers:
        Disco Info  --
        Disco Items --

    Events:
        disco_info         --
        disco_items        --
        disco_info_query   --
        disco_items_query  --

    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):
        """
        Arguments:
            htype
            jid
            node
            handler
        """
        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):
        """
        Arguments:
            htype
            jid
            node
        """
        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):
        """
        Arguments:
            jid      --
            node     --
            local    --
            dfrom    --
            block    --
            timeout  --
            callback --
        """
        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):
        """
        Arguments:
            jid      --
            node     --
            local    --
            dfrom    --
            block    --
            timeout  --
            callback --
        """
        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.
            dat   -- 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